diff -Nru snapd-2.45.1+20.04.2/arch/arch.go snapd-2.48.3+20.04/arch/arch.go --- snapd-2.45.1+20.04.2/arch/arch.go 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/arch/arch.go 2021-02-02 08:21:12.000000000 +0000 @@ -61,11 +61,11 @@ "amd64": "amd64", "arm": "armhf", "arm64": "arm64", + "ppc": "powerpc", + "ppc64": "ppc64", // available in debian and other distros "ppc64le": "ppc64el", + "riscv64": "riscv64", "s390x": "s390x", - "ppc": "powerpc", - // available in debian and other distros - "ppc64": "ppc64", } // If we are running on an ARM platform we need to have a @@ -100,16 +100,16 @@ func dpkgArchFromKernelArch(utsMachine string) string { kernelArchMapping := map[string]string{ // kernel dpkg - "i686": "i386", - "x86_64": "amd64", + "aarch64": "arm64", "armv7l": "armhf", "armv8l": "arm64", - "aarch64": "arm64", + "i686": "i386", + "ppc": "powerpc", + "ppc64": "ppc64", // available in debian and other distros "ppc64le": "ppc64el", + "riscv64": "riscv64", "s390x": "s390x", - "ppc": "powerpc", - // available in debian and other distros - "ppc64": "ppc64", + "x86_64": "amd64", } dpkgArch := kernelArchMapping[utsMachine] diff -Nru snapd-2.45.1+20.04.2/arch/arch_test.go snapd-2.48.3+20.04/arch/arch_test.go --- snapd-2.45.1+20.04.2/arch/arch_test.go 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/arch/arch_test.go 2021-02-02 08:21:12.000000000 +0000 @@ -38,11 +38,11 @@ c.Check(dpkgArchFromGoArch("amd64"), Equals, "amd64") c.Check(dpkgArchFromGoArch("arm"), Equals, "armhf") c.Check(dpkgArchFromGoArch("arm64"), Equals, "arm64") - c.Check(dpkgArchFromGoArch("ppc64le"), Equals, "ppc64el") - c.Check(dpkgArchFromGoArch("ppc64"), Equals, "ppc64") - c.Check(dpkgArchFromGoArch("s390x"), Equals, "s390x") c.Check(dpkgArchFromGoArch("ppc"), Equals, "powerpc") c.Check(dpkgArchFromGoArch("ppc64"), Equals, "ppc64") + c.Check(dpkgArchFromGoArch("ppc64le"), Equals, "ppc64el") + c.Check(dpkgArchFromGoArch("riscv64"), Equals, "riscv64") + c.Check(dpkgArchFromGoArch("s390x"), Equals, "s390x") } func (ts *ArchTestSuite) TestArchSetArchitecture(c *C) { diff -Nru snapd-2.45.1+20.04.2/asserts/asserts.go snapd-2.48.3+20.04/asserts/asserts.go --- snapd-2.45.1+20.04.2/asserts/asserts.go 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/asserts/asserts.go 2021-02-02 08:21:12.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2015-2017 Canonical Ltd + * Copyright (C) 2015-2020 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as @@ -34,7 +34,8 @@ type typeFlags int const ( - noAuthority typeFlags = iota + 1 + noAuthority typeFlags = 1 << iota + sequenceForming ) // MetaHeaders is a list of headers in assertions which are about the assertion @@ -65,11 +66,21 @@ return maxSupportedFormat[at.Name] } +// SequencingForming returns true if the assertion type has a positive +// integer >= 1 as the last component (preferably called "sequence") +// of its primary key over which the assertions of the type form +// sequences, usually without gaps, one sequence per sequence key (the +// primary key prefix omitting the sequence number). +// See SequenceMember. +func (at *AssertionType) SequenceForming() bool { + return at.flags&sequenceForming != 0 +} + // 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, 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} @@ -79,6 +90,7 @@ 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} // ... @@ -103,6 +115,7 @@ SnapDeveloperType.Name: SnapDeveloperType, SystemUserType.Name: SystemUserType, ValidationType.Name: ValidationType, + ValidationSetType.Name: ValidationSetType, RepairType.Name: RepairType, StoreType.Name: StoreType, // no authority @@ -138,6 +151,9 @@ // 3: support for on-store/on-brand/on-model device scope constraints // 4: support for plug-names/slot-names constraints maxSupportedFormat[SnapDeclarationType.Name] = 4 + + // 1: support to limit to device serials + maxSupportedFormat[SystemUserType.Name] = 1 } func MockMaxSupportedFormat(assertType *AssertionType, maxFormat int) (restore func()) { @@ -150,6 +166,25 @@ var formatAnalyzer = map[*AssertionType]func(headers map[string]interface{}, body []byte) (formatnum int, err error){ SnapDeclarationType: snapDeclarationFormatAnalyze, + SystemUserType: systemUserFormatAnalyze, +} + +// MaxSupportedFormats returns a mapping between assertion type names +// and corresponding max supported format if it is >= min. Typical +// usage passes 1 or 0 for min. +func MaxSupportedFormats(min int) (maxFormats map[string]int) { + if min == 0 { + maxFormats = make(map[string]int, len(typeRegistry)) + } else { + maxFormats = make(map[string]int) + } + for name := range typeRegistry { + m := maxSupportedFormat[name] + if m >= min { + maxFormats[name] = m + } + } + return maxFormats } // SuggestFormat returns a minimum format that supports the features that would be used by an assertion with the given components. @@ -188,15 +223,19 @@ // corresponding to a primary key under the assertion type, it errors // if there are missing primary key headers. func PrimaryKeyFromHeaders(assertType *AssertionType, headers map[string]string) (primaryKey []string, err error) { - primaryKey = make([]string, len(assertType.PrimaryKey)) - for i, k := range assertType.PrimaryKey { + return keysFromHeaders(assertType.PrimaryKey, headers) +} + +func keysFromHeaders(keys []string, headers 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) } - primaryKey[i] = keyVal + keyValues[i] = keyVal } - return primaryKey, nil + return keyValues, nil } // Ref expresses a reference to an assertion. @@ -237,6 +276,23 @@ return find(ref.Type, headers) } +const RevisionNotKnown = -1 + +// AtRevision represents an assertion at a given revision, possibly +// not known (RevisionNotKnown). +type AtRevision struct { + Ref + Revision int +} + +func (at *AtRevision) String() string { + s := at.Ref.String() + if at.Revision == RevisionNotKnown { + return s + } + return fmt.Sprintf("%s at revision %d", s, at.Revision) +} + // Assertion represents an assertion through its general elements. type Assertion interface { // Type returns the type of this assertion @@ -275,6 +331,17 @@ // Ref returns a reference representing this assertion. Ref() *Ref + + // At returns an AtRevision referencing this assertion at its revision. + At() *AtRevision +} + +// SequenceMember is implemented by assertions of sequence forming types. +type SequenceMember interface { + Assertion + + // Sequence returns the sequence number of this assertion. + Sequence() int } // customSigner represents an assertion with special arrangements for its signing key (e.g. self-signed), rather than the usual case where an assertion is signed by its authority. @@ -380,6 +447,11 @@ } } +// At returns an AtRevision referencing this assertion at its revision. +func (ab *assertionBase) At() *AtRevision { + return &AtRevision{Ref: *ab.Ref(), Revision: ab.Revision()} +} + // sanity check var _ Assertion = (*assertionBase)(nil) diff -Nru snapd-2.45.1+20.04.2/asserts/asserts_test.go snapd-2.48.3+20.04/asserts/asserts_test.go --- snapd-2.45.1+20.04.2/asserts/asserts_test.go 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/asserts/asserts_test.go 2021-02-02 08:21:12.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2015-2016 Canonical Ltd + * Copyright (C) 2015-2020 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,12 +65,37 @@ "system-user", "test-only", "test-only-2", + "test-only-decl", "test-only-no-authority", "test-only-no-authority-pk", + "test-only-rev", + "test-only-seq", "validation", + "validation-set", }) } +func (as *assertsSuite) TestMaxSupportedFormats(c *C) { + snapDeclMaxFormat := asserts.SnapDeclarationType.MaxSupportedFormat() + systemUserMaxFormat := asserts.SystemUserType.MaxSupportedFormat() + // sanity + c.Check(snapDeclMaxFormat >= 4, Equals, true) + c.Check(systemUserMaxFormat >= 1, Equals, true) + c.Check(asserts.MaxSupportedFormats(1), DeepEquals, map[string]int{ + "snap-declaration": snapDeclMaxFormat, + "system-user": systemUserMaxFormat, + "test-only": 1, + "test-only-seq": 2, + }) + + // all + maxFormats := asserts.MaxSupportedFormats(0) + c.Assert(maxFormats, HasLen, len(asserts.TypeNames())) + c.Check(maxFormats["test-only"], Equals, 1) + c.Check(maxFormats["test-only-2"], Equals, 0) + c.Check(maxFormats["snap-declaration"], Equals, snapDeclMaxFormat) +} + func (as *assertsSuite) TestSuggestFormat(c *C) { fmtnum, err := asserts.SuggestFormat(asserts.Type("test-only-2"), nil, nil) c.Assert(err, IsNil) @@ -163,6 +188,24 @@ c.Check(err, ErrorMatches, `"test-only-2" assertion reference primary key has the wrong length \(expected \[pk1 pk2\]\): \[abc\]`) } +func (as *assertsSuite) TestAtRevisionString(c *C) { + ref := asserts.Ref{ + Type: asserts.AccountType, + PrimaryKey: []string{"canonical"}, + } + + at := &asserts.AtRevision{ + Ref: ref, + } + c.Check(at.String(), Equals, "account (canonical) at revision 0") + + at = &asserts.AtRevision{ + Ref: ref, + Revision: asserts.RevisionNotKnown, + } + c.Check(at.String(), Equals, "account (canonical)") +} + const exKeyID = "Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" const exampleEmptyBodyAllDefaults = "type: test-only\n" + @@ -331,7 +374,9 @@ {"type: test-only\n", "type: unknown\n", `unknown assertion type: "unknown"`}, {"revision: 0\n", "revision: Z\n", `assertion: "revision" header is not an integer: Z`}, {"revision: 0\n", "revision:\n - 1\n", `assertion: "revision" header is not an integer: \[1\]`}, + {"revision: 0\n", "revision: 00\n", `assertion: "revision" header has invalid prefix zeros: 00`}, {"revision: 0\n", "revision: -10\n", "assertion: revision should be positive: -10"}, + {"revision: 0\n", "revision: 99999999999999999999\n", `assertion: "revision" header is out of range: 99999999999999999999`}, {"format: 0\n", "format: Z\n", `assertion: "format" header is not an integer: Z`}, {"format: 0\n", "format: -10\n", "assertion: format should be positive: -10"}, {"primary-key: abc\n", "", `assertion test-only: "primary-key" header is mandatory`}, @@ -821,10 +866,19 @@ PrimaryKey: []string{"0"}, }) + c.Check(a1.At(), DeepEquals, &asserts.AtRevision{ + Ref: asserts.Ref{ + Type: asserts.TestOnlyType, + PrimaryKey: []string{"0"}, + }, + Revision: 0, + }) + headers = map[string]interface{}{ "authority-id": "auth-id1", "pk1": "a", "pk2": "b", + "revision": "1", } a2, err := asserts.AssembleAndSignInTest(asserts.TestOnly2Type, headers, nil, testPrivKey1) c.Assert(err, IsNil) @@ -833,6 +887,14 @@ Type: asserts.TestOnly2Type, PrimaryKey: []string{"a", "b"}, }) + + c.Check(a2.At(), DeepEquals, &asserts.AtRevision{ + Ref: asserts.Ref{ + Type: asserts.TestOnly2Type, + PrimaryKey: []string{"a", "b"}, + }, + Revision: 1, + }) } func (as *assertsSuite) TestAssembleHeadersCheck(c *C) { @@ -888,6 +950,7 @@ "serial", "system-user", "validation", + "validation-set", "repair", } c.Check(withAuthority, HasLen, asserts.NumAssertionType-3) // excluding device-session-request, serial-request, account-key-request @@ -897,3 +960,16 @@ c.Check(err, ErrorMatches, `"authority-id" header is mandatory`) } } + +func (as *assertsSuite) TestSequenceForming(c *C) { + sequenceForming := []string{ + "repair", + "validation-set", + } + for _, name := range sequenceForming { + typ := asserts.Type(name) + c.Check(typ.SequenceForming(), Equals, true) + } + + c.Check(asserts.SnapDeclarationType.SequenceForming(), Equals, false) +} diff -Nru snapd-2.45.1+20.04.2/asserts/database.go snapd-2.48.3+20.04/asserts/database.go --- snapd-2.45.1+20.04.2/asserts/database.go 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/asserts/database.go 2021-02-02 08:21:12.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2015-2016 Canonical Ltd + * Copyright (C) 2015-2020 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,6 +65,14 @@ // Search returns assertions matching the given headers. // It invokes foundCb for each found assertion. Search(assertType *AssertionType, headers map[string]string, foundCb func(Assertion), maxFormat int) error + // SequenceMemberAfter returns for a sequence-forming assertType the + // first assertion in the sequence under the given sequenceKey + // with sequence number larger than after. If after==-1 it + // returns the assertion with largest sequence number. If none + // exists it returns a NotFoundError, usually with omitted + // Headers. If assertType is not sequence-forming it can + // panic. + SequenceMemberAfter(assertType *AssertionType, sequenceKey []string, after, maxFormat int) (SequenceMember, error) } type nullBackstore struct{} @@ -81,6 +89,10 @@ return nil } +func (nbs nullBackstore) SequenceMemberAfter(t *AssertionType, kp []string, after, maxFormat int) (SequenceMember, error) { + return nil, &NotFoundError{Type: t} +} + // A KeypairManager is a manager and backstore for private/public key pairs. type KeypairManager interface { // Put stores the given private/public key pair, @@ -574,6 +586,80 @@ return db.findMany([]Backstore{db.trusted, db.predefined}, assertionType, headers) } +// FindSequence finds an assertion for the given headers and after for +// a sequence-forming type. +// The provided headers must contain a sequence key, i.e. a prefix of +// the primary key for the assertion type except for the sequence +// number header. +// The assertion is the first in the sequence under the sequence key +// with sequence number > after. +// If after is -1 it returns instead the assertion with the largest +// sequence number. +// It will constraint itself to assertions with format <= maxFormat +// unless maxFormat is -1. +// It returns a NotFoundError if the assertion cannot be found. +func (db *Database) FindSequence(assertType *AssertionType, sequenceHeaders map[string]string, after, maxFormat int) (SequenceMember, error) { + err := checkAssertType(assertType) + if err != nil { + return nil, err + } + if !assertType.SequenceForming() { + return nil, fmt.Errorf("cannot use FindSequence with non sequence-forming assertion type %q", assertType.Name) + } + maxSupp := assertType.MaxSupportedFormat() + if maxFormat == -1 { + maxFormat = maxSupp + } else { + if maxFormat > maxSupp { + return nil, fmt.Errorf("cannot find %q assertions for format %d higher than supported format %d", assertType.Name, maxFormat, maxSupp) + } + } + + // 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) + if err != nil { + return nil, err + } + + // find the better result across backstores' results + better := func(cur, a SequenceMember) SequenceMember { + if cur == nil { + return a + } + curSeq := cur.Sequence() + aSeq := a.Sequence() + if after == -1 { + if aSeq > curSeq { + return a + } + } else { + if aSeq < curSeq { + return a + } + } + return cur + } + + var assert SequenceMember + for _, bs := range db.backstores { + a, err := bs.SequenceMemberAfter(assertType, seqKey, after, maxFormat) + if err == nil { + assert = better(assert, a) + continue + } + if !IsNotFound(err) { + return nil, err + } + } + + if assert != nil { + return assert, nil + } + + return nil, &NotFoundError{Type: assertType, Headers: sequenceHeaders} +} + // assertion checkers // CheckSigningKeyIsNotExpired checks that the signing key is not expired. diff -Nru snapd-2.45.1+20.04.2/asserts/database_test.go snapd-2.48.3+20.04/asserts/database_test.go --- snapd-2.45.1+20.04.2/asserts/database_test.go 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/asserts/database_test.go 2021-02-02 08:21:12.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2015-2016 Canonical Ltd + * Copyright (C) 2015-2020 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 @@ -1238,6 +1238,117 @@ }) } +func (safs *signAddFindSuite) TestFindSequence(c *C) { + headers := map[string]interface{}{ + "authority-id": "canonical", + "n": "s1", + "sequence": "1", + } + sq1f0, err := safs.signingDB.Sign(asserts.TestOnlySeqType, headers, nil, safs.signingKeyID) + c.Assert(err, IsNil) + + headers = map[string]interface{}{ + "authority-id": "canonical", + "n": "s1", + "sequence": "2", + } + sq2f0, err := safs.signingDB.Sign(asserts.TestOnlySeqType, headers, nil, safs.signingKeyID) + c.Assert(err, IsNil) + + headers = map[string]interface{}{ + "authority-id": "canonical", + "format": "1", + "n": "s1", + "sequence": "2", + "revision": "1", + } + sq2f1, err := safs.signingDB.Sign(asserts.TestOnlySeqType, headers, nil, safs.signingKeyID) + c.Assert(err, IsNil) + + headers = map[string]interface{}{ + "authority-id": "canonical", + "format": "1", + "n": "s1", + "sequence": "3", + } + sq3f1, err := safs.signingDB.Sign(asserts.TestOnlySeqType, headers, nil, safs.signingKeyID) + c.Assert(err, IsNil) + + headers = map[string]interface{}{ + "authority-id": "canonical", + "format": "2", + "n": "s1", + "sequence": "3", + "revision": "1", + } + sq3f2, err := safs.signingDB.Sign(asserts.TestOnlySeqType, headers, nil, safs.signingKeyID) + c.Assert(err, IsNil) + + for _, a := range []asserts.Assertion{sq1f0, sq2f0, sq2f1, sq3f1} { + + err = safs.db.Add(a) + c.Assert(err, IsNil) + } + + // stack a backstore, for test completeness, this is an unlikely + // scenario atm + bs := asserts.NewMemoryBackstore() + db := safs.db.WithStackedBackstore(bs) + err = db.Add(sq3f2) + c.Assert(err, IsNil) + + seqHeaders := map[string]string{ + "n": "s1", + } + tests := []struct { + after int + maxFormat int + sequence int + format int + revision int + }{ + {after: 0, maxFormat: 0, sequence: 1, format: 0, revision: 0}, + {after: 0, maxFormat: 2, sequence: 1, format: 0, revision: 0}, + {after: 1, maxFormat: 0, sequence: 2, format: 0, revision: 0}, + {after: 1, maxFormat: 1, sequence: 2, format: 1, revision: 1}, + {after: 1, maxFormat: 2, sequence: 2, format: 1, revision: 1}, + {after: 2, maxFormat: 0, sequence: -1}, + {after: 2, maxFormat: 1, sequence: 3, format: 1, revision: 0}, + {after: 2, maxFormat: 2, sequence: 3, format: 2, revision: 1}, + {after: 3, maxFormat: 0, sequence: -1}, + {after: 3, maxFormat: 2, sequence: -1}, + {after: 4, maxFormat: 2, sequence: -1}, + {after: -1, maxFormat: 0, sequence: 2, format: 0, revision: 0}, + {after: -1, maxFormat: 1, sequence: 3, format: 1, revision: 0}, + {after: -1, maxFormat: 2, sequence: 3, format: 2, revision: 1}, + } + + for _, t := range tests { + a, err := db.FindSequence(asserts.TestOnlySeqType, seqHeaders, t.after, t.maxFormat) + if t.sequence == -1 { + c.Check(err, DeepEquals, &asserts.NotFoundError{ + Type: asserts.TestOnlySeqType, + Headers: seqHeaders, + }) + } else { + c.Assert(err, IsNil) + c.Assert(a.HeaderString("n"), Equals, "s1") + c.Check(a.Sequence(), Equals, t.sequence) + c.Check(a.Format(), Equals, t.format) + c.Check(a.Revision(), Equals, t.revision) + } + } + + seqHeaders = map[string]string{ + "n": "s2", + } + _, err = db.FindSequence(asserts.TestOnlySeqType, seqHeaders, -1, 2) + c.Check(err, DeepEquals, &asserts.NotFoundError{ + Type: asserts.TestOnlySeqType, Headers: seqHeaders, + }) + +} + type revisionErrorSuite struct{} func (res *revisionErrorSuite) TestErrorText(c *C) { diff -Nru snapd-2.45.1+20.04.2/asserts/export_test.go snapd-2.48.3+20.04/asserts/export_test.go --- snapd-2.45.1+20.04.2/asserts/export_test.go 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/asserts/export_test.go 2021-02-02 08:21:12.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2015-2016 Canonical Ltd + * Copyright (C) 2015-2020 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,8 @@ import ( "io" "time" + + "github.com/snapcore/snapd/asserts/internal" ) // expose test-only things here @@ -112,6 +114,90 @@ var TestOnly2Type = &AssertionType{"test-only-2", []string{"pk1", "pk2"}, assembleTestOnly2, 0} +// TestOnlyDecl is a test-only assertion that mimics snap-declaration +// relations with other assertions. +type TestOnlyDecl struct { + assertionBase +} + +func (dcl *TestOnlyDecl) ID() string { + return dcl.HeaderString("id") +} + +func (dcl *TestOnlyDecl) DevID() string { + return dcl.HeaderString("dev-id") +} + +func (dcl *TestOnlyDecl) Prerequisites() []*Ref { + return []*Ref{ + {Type: AccountType, PrimaryKey: []string{dcl.DevID()}}, + } +} + +func assembleTestOnlyDecl(assert assertionBase) (Assertion, error) { + return &TestOnlyDecl{assert}, nil +} + +var TestOnlyDeclType = &AssertionType{"test-only-decl", []string{"id"}, assembleTestOnlyDecl, 0} + +// TestOnlyRev is a test-only assertion that mimics snap-revision +// relations with other assertions. +type TestOnlyRev struct { + assertionBase +} + +func (rev *TestOnlyRev) H() string { + return rev.HeaderString("h") +} + +func (rev *TestOnlyRev) ID() string { + return rev.HeaderString("id") +} + +func (rev *TestOnlyRev) DevID() string { + return rev.HeaderString("dev-id") +} + +func (rev *TestOnlyRev) Prerequisites() []*Ref { + return []*Ref{ + {Type: TestOnlyDeclType, PrimaryKey: []string{rev.ID()}}, + {Type: AccountType, PrimaryKey: []string{rev.DevID()}}, + } +} + +func assembleTestOnlyRev(assert assertionBase) (Assertion, error) { + return &TestOnlyRev{assert}, nil +} + +var TestOnlyRevType = &AssertionType{"test-only-rev", []string{"h"}, assembleTestOnlyRev, 0} + +// TestOnlySeq is a test-only assertion that is sequence-forming. +type TestOnlySeq struct { + assertionBase + seq int +} + +func (seq *TestOnlyRev) N() string { + return seq.HeaderString("n") +} + +func (seq *TestOnlySeq) Sequence() int { + return seq.seq +} + +func assembleTestOnlySeq(assert assertionBase) (Assertion, error) { + seq, err := checkSequence(assert.headers, "sequence") + if err != nil { + return nil, err + } + return &TestOnlySeq{ + assertionBase: assert, + seq: seq, + }, nil +} + +var TestOnlySeqType = &AssertionType{"test-only-seq", []string{"n", "sequence"}, assembleTestOnlySeq, sequenceForming} + type TestOnlyNoAuthority struct { assertionBase } @@ -147,6 +233,10 @@ } return 0, nil } + typeRegistry[TestOnlyDeclType.Name] = TestOnlyDeclType + typeRegistry[TestOnlyRevType.Name] = TestOnlyRevType + typeRegistry[TestOnlySeqType.Name] = TestOnlySeqType + maxSupportedFormat[TestOnlySeqType.Name] = 2 } // AccountKeyIsKeyValidAt exposes isKeyValidAt on AccountKey for tests @@ -196,3 +286,9 @@ func (b *Batch) DoPrecheck(db *Database) error { return b.precheck(db) } + +// pool tests + +func MakePoolGrouping(elems ...uint16) Grouping { + return Grouping(internal.Serialize(elems)) +} diff -Nru snapd-2.45.1+20.04.2/asserts/findwildcard.go snapd-2.48.3+20.04/asserts/findwildcard.go --- snapd-2.45.1+20.04.2/asserts/findwildcard.go 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/asserts/findwildcard.go 2021-02-02 08:21:12.000000000 +0000 @@ -21,8 +21,11 @@ import ( "fmt" + "io" "os" "path/filepath" + "sort" + "strconv" "strings" ) @@ -31,14 +34,21 @@ //... -where each descendantWithWildcard component can contain the * wildcard; +where each descendantWithWildcard component can contain the * wildcard. + +One of the descendantWithWildcard components except the last +can be "#>" or "#<", in which case that level is assumed to have names +that can be parsed as positive integers, which will be enumerated in +ascending (#>) or descending order respectively (#<); if seqnum != -1 +then only the values >seqnum or respectively " || k == "#<" { + if len(descendantWithWildcard) == 1 { + return fmt.Errorf("findWildcard: sequence wildcard (#>|<#) cannot be the last component") + } + return findWildcardSequence(top, current, k, descendantWithWildcard[1:], seqnum, foundCb) + } if len(descendantWithWildcard) > 1 && strings.IndexByte(k, '*') == -1 { - return findWildcardDescend(top, filepath.Join(current, k), descendantWithWildcard[1:], foundCb) + return findWildcardDescend(top, filepath.Join(current, k), descendantWithWildcard[1:], seqnum, foundCb) } d, err := os.Open(current) + // ignore missing directory, higher level will produce + // NotFoundError as needed if os.IsNotExist(err) { return nil } @@ -101,11 +119,69 @@ return fmt.Errorf("findWildcard: invoked with malformed wildcard: %v", err) } if ok { - err = findWildcardDescend(top, filepath.Join(current, name), descendantWithWildcard[1:], foundCb) + err = findWildcardDescend(top, filepath.Join(current, name), descendantWithWildcard[1:], seqnum, foundCb) if err != nil { return err } } } return nil +} + +func findWildcardSequence(top, current, seqWildcard string, descendantWithWildcard []string, seqnum int, foundCb func(relpath []string) error) error { + filter := func(i int) bool { return true } + if seqnum != -1 { + if seqWildcard == "#>" { + filter = func(i int) bool { return i > seqnum } + } else { // "#<", guaranteed by the caller + filter = func(i int) bool { return i < seqnum } + } + } + + d, err := os.Open(current) + // ignore missing directory, higher level will produce + // NotFoundError as needed + if os.IsNotExist(err) { + return nil + } + if err != nil { + return err + } + defer d.Close() + var seq []int + for { + names, err := d.Readdirnames(100) + if err == io.EOF { + break + } + if err != nil { + return err + } + for _, n := range names { + sqn, err := strconv.Atoi(n) + if err != nil || sqn < 0 || prefixZeros(n) { + return fmt.Errorf("cannot parse %q name as a valid sequence number", filepath.Join(current, n)) + } + if filter(sqn) { + seq = append(seq, sqn) + } + } + } + sort.Ints(seq) + + var start, direction int + if seqWildcard == "#>" { + start = 0 + direction = 1 + } else { + start = len(seq) - 1 + direction = -1 + } + for i := start; i >= 0 && i < len(seq); i += direction { + err = findWildcardDescend(top, filepath.Join(current, strconv.Itoa(seq[i])), descendantWithWildcard, -1, foundCb) + if err != nil { + return err + } + } + return nil } diff -Nru snapd-2.45.1+20.04.2/asserts/findwildcard_test.go snapd-2.48.3+20.04/asserts/findwildcard_test.go --- snapd-2.45.1+20.04.2/asserts/findwildcard_test.go 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/asserts/findwildcard_test.go 2021-02-02 08:21:12.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2015 Canonical Ltd + * Copyright (C) 2015-2020 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 @@ -64,40 +64,40 @@ return nil } - err = findWildcard(top, []string{"*", "*", "active"}, foundCb) + err = findWildcard(top, []string{"*", "*", "active"}, 0, foundCb) c.Assert(err, check.IsNil) sort.Strings(res) c.Check(res, check.DeepEquals, []string{"acc-id1/abcd/active", "acc-id1/e5cd/active", "acc-id2/f444/active"}) res = nil - err = findWildcard(top, []string{"*", "*", "active*"}, foundCb) + err = findWildcard(top, []string{"*", "*", "active*"}, 0, foundCb) c.Assert(err, check.IsNil) sort.Strings(res) c.Check(res, check.DeepEquals, []string{"acc-id1/abcd/active", "acc-id1/abcd/active.1", "acc-id1/e5cd/active", "acc-id2/f444/active"}) res = nil - err = findWildcard(top, []string{"zoo", "*", "active"}, foundCb) + err = findWildcard(top, []string{"zoo", "*", "active"}, 0, foundCb) c.Assert(err, check.IsNil) c.Check(res, check.HasLen, 0) res = nil - err = findWildcard(top, []string{"zoo", "*", "active*"}, foundCb) + err = findWildcard(top, []string{"zoo", "*", "active*"}, 0, foundCb) c.Assert(err, check.IsNil) c.Check(res, check.HasLen, 0) res = nil - err = findWildcard(top, []string{"a*", "zoo", "active"}, foundCb) + err = findWildcard(top, []string{"a*", "zoo", "active"}, 0, foundCb) c.Assert(err, check.IsNil) c.Check(res, check.HasLen, 0) res = nil - err = findWildcard(top, []string{"acc-id1", "*cd", "active"}, foundCb) + err = findWildcard(top, []string{"acc-id1", "*cd", "active"}, 0, foundCb) c.Assert(err, check.IsNil) sort.Strings(res) c.Check(res, check.DeepEquals, []string{"acc-id1/abcd/active", "acc-id1/e5cd/active"}) res = nil - err = findWildcard(top, []string{"acc-id1", "*cd", "active*"}, foundCb) + err = findWildcard(top, []string{"acc-id1", "*cd", "active*"}, 0, foundCb) c.Assert(err, check.IsNil) sort.Strings(res) c.Check(res, check.DeepEquals, []string{"acc-id1/abcd/active", "acc-id1/abcd/active.1", "acc-id1/e5cd/active"}) @@ -129,11 +129,154 @@ myErr := errors.New("boom") retErr = myErr - err = findWildcard(top, []string{"acc-id1", "*"}, foundCb) + err = findWildcard(top, []string{"acc-id1", "*"}, 0, foundCb) c.Check(err, check.Equals, myErr) retErr = nil res = nil - err = findWildcard(top, []string{"acc-id2", "*"}, foundCb) + err = findWildcard(top, []string{"acc-id2", "*"}, 0, foundCb) c.Check(err, check.ErrorMatches, "expected a regular file: .*") } + +func (fs *findWildcardSuite) TestFindWildcardSequence(c *check.C) { + top := filepath.Join(c.MkDir(), "top") + + err := os.MkdirAll(top, os.ModePerm) + c.Assert(err, check.IsNil) + + files := []string{ + "s1/3/active.1", + "s1/3/active.2", + "s1/2/active", + "s1/2/active.1", + "s1/1/active", + } + for _, fn := range files { + err := os.MkdirAll(filepath.Dir(filepath.Join(top, fn)), os.ModePerm) + c.Assert(err, check.IsNil) + err = ioutil.WriteFile(filepath.Join(top, fn), nil, os.ModePerm) + c.Assert(err, check.IsNil) + } + + var res [][]string + foundCb := func(relpath []string) error { + res = append(res, relpath) + return nil + } + + sort := func() { + for _, r := range res { + sort.Strings(r) + } + } + + // ascending + + err = findWildcard(top, []string{"s1", "#>", "active*"}, 1, foundCb) + c.Assert(err, check.IsNil) + sort() + c.Check(res, check.DeepEquals, [][]string{ + {"s1/2/active", "s1/2/active.1"}, + {"s1/3/active.1", "s1/3/active.2"}, + }) + + res = nil + err = findWildcard(top, []string{"s1", "#>", "active*"}, 2, foundCb) + c.Assert(err, check.IsNil) + sort() + c.Check(res, check.DeepEquals, [][]string{ + {"s1/3/active.1", "s1/3/active.2"}, + }) + + res = nil + err = findWildcard(top, []string{"s1", "#>", "active*"}, 3, foundCb) + c.Assert(err, check.IsNil) + c.Check(res, check.HasLen, 0) + + res = nil + err = findWildcard(top, []string{"s1", "#>", "active*"}, -1, foundCb) + c.Assert(err, check.IsNil) + sort() + c.Check(res, check.DeepEquals, [][]string{ + {"s1/1/active"}, + {"s1/2/active", "s1/2/active.1"}, + {"s1/3/active.1", "s1/3/active.2"}, + }) + + // descending + + res = nil + err = findWildcard(top, []string{"s1", "#<", "active*"}, -1, foundCb) + c.Assert(err, check.IsNil) + sort() + c.Check(res, check.DeepEquals, [][]string{ + {"s1/3/active.1", "s1/3/active.2"}, + {"s1/2/active", "s1/2/active.1"}, + {"s1/1/active"}, + }) + + res = nil + err = findWildcard(top, []string{"s1", "#<", "active*"}, 3, foundCb) + c.Assert(err, check.IsNil) + sort() + c.Check(res, check.DeepEquals, [][]string{ + {"s1/2/active", "s1/2/active.1"}, + {"s1/1/active"}, + }) + + res = nil + err = findWildcard(top, []string{"s1", "#<", "active*"}, 2, foundCb) + c.Assert(err, check.IsNil) + sort() + c.Check(res, check.DeepEquals, [][]string{ + {"s1/1/active"}, + }) + + res = nil + err = findWildcard(top, []string{"s1", "#<", "active*"}, 1, foundCb) + c.Assert(err, check.IsNil) + c.Check(res, check.HasLen, 0) + + // missing dir + res = nil + err = findWildcard(top, []string{"s2", "#<", "active*"}, 1, foundCb) + c.Assert(err, check.IsNil) + c.Check(res, check.HasLen, 0) +} + +func (fs *findWildcardSuite) TestFindWildcardSequenceSomeErrors(c *check.C) { + top := filepath.Join(c.MkDir(), "top-errors") + + err := os.MkdirAll(top, os.ModePerm) + c.Assert(err, check.IsNil) + + files := []string{ + "s1/1/active", + "s2/a/active.1", + "s3/-9/active.1", + "s4/01/active", + } + for _, fn := range files { + err := os.MkdirAll(filepath.Dir(filepath.Join(top, fn)), os.ModePerm) + c.Assert(err, check.IsNil) + err = ioutil.WriteFile(filepath.Join(top, fn), nil, os.ModePerm) + c.Assert(err, check.IsNil) + } + + myErr := errors.New("boom") + foundCb := func(relpath []string) error { + return myErr + } + + err = findWildcard(top, []string{"s1", "#>", "active*"}, -1, foundCb) + c.Assert(err, check.Equals, myErr) + + err = findWildcard(top, []string{"s2", "#>", "active*"}, -1, foundCb) + c.Assert(err, check.ErrorMatches, `cannot parse ".*/top-errors/s2/a" name as a valid sequence number`) + + err = findWildcard(top, []string{"s3", "#>", "active*"}, -1, foundCb) + c.Assert(err, check.ErrorMatches, `cannot parse ".*/top-errors/s3/-9" name as a valid sequence number`) + + err = findWildcard(top, []string{"s4", "#>", "active*"}, -1, foundCb) + c.Assert(err, check.ErrorMatches, `cannot parse ".*/top-errors/s4/01" name as a valid sequence number`) +} diff -Nru snapd-2.45.1+20.04.2/asserts/fsbackstore.go snapd-2.48.3+20.04/asserts/fsbackstore.go --- snapd-2.45.1+20.04.2/asserts/fsbackstore.go 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/asserts/fsbackstore.go 2021-02-02 08:21:12.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2015-2016 Canonical Ltd + * Copyright (C) 2015-2020 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,7 @@ package asserts import ( + "errors" "fmt" "net/url" "os" @@ -123,7 +124,7 @@ comps := diskPrimaryPathComps(primaryPath, "active*") assertTypeTop := filepath.Join(fsbs.top, assertType.Name) - err := findWildcard(assertTypeTop, comps, namesCb) + err := findWildcard(assertTypeTop, comps, 0, namesCb) if err != nil { return nil, fmt.Errorf("broken assertion storage, looking for %s: %v", assertType.Name, err) } @@ -189,7 +190,7 @@ foundCb(a) return nil } - err := findWildcard(assertTypeTop, diskPattern, candCb) + err := findWildcard(assertTypeTop, diskPattern, 0, candCb) if err != nil { return fmt.Errorf("broken assertion storage, searching for %s: %v", assertType.Name, err) } @@ -219,3 +220,56 @@ } return fsbs.search(assertType, diskPattern, candCb, maxFormat) } + +// errFound marks the case an assertion was found +var errFound = errors.New("found") + +func (fsbs *filesystemBackstore) SequenceMemberAfter(assertType *AssertionType, sequenceKey []string, after, maxFormat int) (SequenceMember, error) { + if !assertType.SequenceForming() { + panic(fmt.Sprintf("internal error: SequenceMemberAfter on non sequence-forming assertion type %s", assertType.Name)) + } + if len(sequenceKey) != len(assertType.PrimaryKey)-1 { + return nil, fmt.Errorf("internal error: SequenceMemberAfter's sequence key argument length must be exactly 1 less than the assertion type primary key") + } + + fsbs.mu.RLock() + defer fsbs.mu.RUnlock() + + n := len(assertType.PrimaryKey) + diskPattern := make([]string, n+1) + for i, k := range sequenceKey { + diskPattern[i] = url.QueryEscape(k) + } + seqWildcard := "#>" // ascending sequence wildcard + if after == -1 { + // find the latest in sequence + // use descending sequence wildcard + seqWildcard = "#<" + } + diskPattern[n-1] = seqWildcard + diskPattern[n] = "active*" + + var a Assertion + candCb := func(diskPrimaryPaths []string) error { + var err error + a, err = fsbs.pickLatestAssertion(assertType, diskPrimaryPaths, maxFormat) + if err == errNotFound { + return nil + } + if err != nil { + return err + } + return errFound + } + + assertTypeTop := filepath.Join(fsbs.top, assertType.Name) + err := findWildcard(assertTypeTop, diskPattern, after, candCb) + if err == errFound { + return a.(SequenceMember), nil + } + if err != nil { + return nil, fmt.Errorf("broken assertion storage, searching for %s: %v", assertType.Name, err) + } + + return nil, &NotFoundError{Type: assertType} +} diff -Nru snapd-2.45.1+20.04.2/asserts/fsbackstore_test.go snapd-2.48.3+20.04/asserts/fsbackstore_test.go --- snapd-2.45.1+20.04.2/asserts/fsbackstore_test.go 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/asserts/fsbackstore_test.go 2021-02-02 08:21:12.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2016 Canonical Ltd + * Copyright (C) 2016-2020 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 @@ -256,3 +256,117 @@ c.Check(as[0].Revision(), Equals, 1) } + +func (fsbss *fsBackstoreSuite) TestSequenceMemberAfter(c *C) { + topDir := filepath.Join(c.MkDir(), "asserts-db") + bs, err := asserts.OpenFSBackstore(topDir) + c.Assert(err, IsNil) + + other1, err := asserts.Decode([]byte("type: test-only-seq\n" + + "authority-id: auth-id1\n" + + "n: other\n" + + "sequence: 1\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==")) + c.Assert(err, IsNil) + + sq1f0, err := asserts.Decode([]byte("type: test-only-seq\n" + + "authority-id: auth-id1\n" + + "n: s1\n" + + "sequence: 1\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==")) + c.Assert(err, IsNil) + + sq2f0, err := asserts.Decode([]byte("type: test-only-seq\n" + + "authority-id: auth-id1\n" + + "n: s1\n" + + "sequence: 2\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==")) + c.Assert(err, IsNil) + + sq2f1, err := asserts.Decode([]byte("type: test-only-seq\n" + + "authority-id: auth-id1\n" + + "format: 1\n" + + "n: s1\n" + + "sequence: 2\n" + + "revision: 1\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==")) + c.Assert(err, IsNil) + + sq3f1, err := asserts.Decode([]byte("type: test-only-seq\n" + + "authority-id: auth-id1\n" + + "format: 1\n" + + "n: s1\n" + + "sequence: 3\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==")) + c.Assert(err, IsNil) + + sq3f2, err := asserts.Decode([]byte("type: test-only-seq\n" + + "authority-id: auth-id1\n" + + "format: 2\n" + + "n: s1\n" + + "sequence: 3\n" + + "revision: 1\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==")) + c.Assert(err, IsNil) + + for _, a := range []asserts.Assertion{other1, sq1f0, sq2f0, sq2f1, sq3f1, sq3f2} { + err = bs.Put(asserts.TestOnlySeqType, a) + c.Assert(err, IsNil) + } + + seqKey := []string{"s1"} + tests := []struct { + after int + maxFormat int + sequence int + format int + revision int + }{ + {after: 0, maxFormat: 0, sequence: 1, format: 0, revision: 0}, + {after: 0, maxFormat: 2, sequence: 1, format: 0, revision: 0}, + {after: 1, maxFormat: 0, sequence: 2, format: 0, revision: 0}, + {after: 1, maxFormat: 1, sequence: 2, format: 1, revision: 1}, + {after: 1, maxFormat: 2, sequence: 2, format: 1, revision: 1}, + {after: 2, maxFormat: 0, sequence: -1}, + {after: 2, maxFormat: 1, sequence: 3, format: 1, revision: 0}, + {after: 2, maxFormat: 2, sequence: 3, format: 2, revision: 1}, + {after: 3, maxFormat: 0, sequence: -1}, + {after: 3, maxFormat: 2, sequence: -1}, + {after: 4, maxFormat: 2, sequence: -1}, + {after: -1, maxFormat: 0, sequence: 2, format: 0, revision: 0}, + {after: -1, maxFormat: 1, sequence: 3, format: 1, revision: 0}, + {after: -1, maxFormat: 2, sequence: 3, format: 2, revision: 1}, + } + + for _, t := range tests { + a, err := bs.SequenceMemberAfter(asserts.TestOnlySeqType, seqKey, t.after, t.maxFormat) + if t.sequence == -1 { + c.Check(err, DeepEquals, &asserts.NotFoundError{ + Type: asserts.TestOnlySeqType, + }) + } else { + c.Assert(err, IsNil) + c.Assert(a.HeaderString("n"), Equals, "s1") + c.Check(a.Sequence(), Equals, t.sequence) + c.Check(a.Format(), Equals, t.format) + c.Check(a.Revision(), Equals, t.revision) + } + } + + _, err = bs.SequenceMemberAfter(asserts.TestOnlySeqType, []string{"s2"}, -1, 2) + c.Check(err, DeepEquals, &asserts.NotFoundError{ + Type: asserts.TestOnlySeqType, + }) +} diff -Nru snapd-2.45.1+20.04.2/asserts/gpgkeypairmgr.go snapd-2.48.3+20.04/asserts/gpgkeypairmgr.go --- snapd-2.45.1+20.04.2/asserts/gpgkeypairmgr.go 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/asserts/gpgkeypairmgr.go 2021-02-02 08:21:12.000000000 +0000 @@ -32,7 +32,7 @@ ) func ensureGPGHomeDirectory() (string, error) { - real, err := osutil.RealUser() + real, err := osutil.UserMaybeSudoUser() if err != nil { return "", err } diff -Nru snapd-2.45.1+20.04.2/asserts/header_checks.go snapd-2.48.3+20.04/asserts/header_checks.go --- snapd-2.45.1+20.04.2/asserts/header_checks.go 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/asserts/header_checks.go 2021-02-02 08:21:12.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2015-2016 Canonical Ltd + * Copyright (C) 2015-2020 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,25 +116,54 @@ if !ok { return -1, fmt.Errorf("%q header is not an integer: %v", name, value) } - m, err := strconv.Atoi(s) + m, err := atoi(s, "%q %s", name, "header") if err != nil { - return -1, fmt.Errorf("%q header is not an integer: %v", name, s) + return -1, err } return m, nil } func checkInt(headers map[string]interface{}, name string) (int, error) { - valueStr, err := checkNotEmptyString(headers, name) + return checkIntWhat(headers, name, "header") +} + +func checkIntWhat(headers map[string]interface{}, name, what string) (int, error) { + valueStr, err := checkNotEmptyStringWhat(headers, name, what) if err != nil { return -1, err } + value, err := atoi(valueStr, "%q %s", name, what) + if err != nil { + return -1, err + } + return value, nil +} + +type intSyntaxError string + +func (e intSyntaxError) Error() string { + return string(e) +} + +func atoi(valueStr, whichFmt string, whichArgs ...interface{}) (int, error) { value, err := strconv.Atoi(valueStr) if err != nil { - return -1, fmt.Errorf("%q header is not an integer: %v", name, valueStr) + which := fmt.Sprintf(whichFmt, whichArgs...) + if ne, ok := err.(*strconv.NumError); ok && ne.Err == strconv.ErrRange { + return -1, fmt.Errorf("%s is out of range: %v", which, valueStr) + } + return -1, intSyntaxError(fmt.Sprintf("%s is not an integer: %v", which, valueStr)) + } + if prefixZeros(valueStr) { + return -1, fmt.Errorf("%s has invalid prefix zeros: %s", fmt.Sprintf(whichFmt, whichArgs...), valueStr) } return value, nil } +func prefixZeros(s string) bool { + return strings.HasPrefix(s, "0") && s != "0" +} + func checkRFC3339Date(headers map[string]interface{}, name string) (time.Time, error) { return checkRFC3339DateWhat(headers, name, "header") } @@ -176,11 +205,16 @@ if err != nil { return 0, err } - value, err := strconv.ParseUint(valueStr, 10, bitSize) if err != nil { + if ne, ok := err.(*strconv.NumError); ok && ne.Err == strconv.ErrRange { + return 0, fmt.Errorf("%q header is out of range: %v", name, valueStr) + } return 0, fmt.Errorf("%q header is not an unsigned integer: %v", name, valueStr) } + if prefixZeros(valueStr) { + return 0, fmt.Errorf("%q header has invalid prefix zeros: %s", name, valueStr) + } return value, nil } diff -Nru snapd-2.45.1+20.04.2/asserts/ifacedecls.go snapd-2.48.3+20.04/asserts/ifacedecls.go --- snapd-2.45.1+20.04.2/asserts/ifacedecls.go 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/asserts/ifacedecls.go 2021-02-02 08:21:12.000000000 +0000 @@ -398,9 +398,14 @@ if x == "*" { return SideArityConstraint{N: -1}, nil } - n, err := strconv.Atoi(x) - if err != nil || n < 1 { + n, err := atoi(x, "%s in %s", which, context) + switch _, syntax := err.(intSyntaxError); { + case err == nil && n < 1: + fallthrough + case syntax: return a, fmt.Errorf("%s in %s must be an integer >=1 or *", which, context) + case err != nil: + return a, err } return SideArityConstraint{N: n}, nil } diff -Nru snapd-2.45.1+20.04.2/asserts/ifacedecls_test.go snapd-2.48.3+20.04/asserts/ifacedecls_test.go --- snapd-2.45.1+20.04.2/asserts/ifacedecls_test.go 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/asserts/ifacedecls_test.go 2021-02-02 08:21:12.000000000 +0000 @@ -1334,6 +1334,12 @@ plugs-per-slot: any`, `plugs-per-slot in allow-auto-connection in plug rule for interface "iface" must be an integer >=1 or \*`}, {`iface: allow-auto-connection: + slots-per-plug: 00`, `slots-per-plug in allow-auto-connection in plug rule for interface "iface" has invalid prefix zeros: 00`}, + {`iface: + allow-auto-connection: + slots-per-plug: 99999999999999999999`, `slots-per-plug in allow-auto-connection in plug rule for interface "iface" is out of range: 99999999999999999999`}, + {`iface: + allow-auto-connection: slots-per-plug: 0`, `slots-per-plug in allow-auto-connection in plug rule for interface "iface" must be an integer >=1 or \*`}, {`iface: allow-auto-connection: @@ -2165,6 +2171,12 @@ plugs-per-slot: any`, `plugs-per-slot in allow-auto-connection in slot rule for interface "iface" must be an integer >=1 or \*`}, {`iface: allow-auto-connection: + slots-per-plug: 00`, `slots-per-plug in allow-auto-connection in slot rule for interface "iface" has invalid prefix zeros: 00`}, + {`iface: + allow-auto-connection: + slots-per-plug: 99999999999999999999`, `slots-per-plug in allow-auto-connection in slot rule for interface "iface" is out of range: 99999999999999999999`}, + {`iface: + allow-auto-connection: slots-per-plug: 0`, `slots-per-plug in allow-auto-connection in slot rule for interface "iface" must be an integer >=1 or \*`}, {`iface: allow-auto-connection: diff -Nru snapd-2.45.1+20.04.2/asserts/internal/grouping.go snapd-2.48.3+20.04/asserts/internal/grouping.go --- snapd-2.45.1+20.04.2/asserts/internal/grouping.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48.3+20.04/asserts/internal/grouping.go 2021-02-02 08:21:12.000000000 +0000 @@ -0,0 +1,286 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2020 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 internal + +import ( + "bytes" + "encoding/base64" + "encoding/binary" + "fmt" + "sort" +) + +// Groupings maintain labels to identify membership to one or more groups. +// Labels are implemented as subsets of integers from 0 +// up to an excluded maximum, where the integers represent the groups. +// Assumptions: +// - most labels are for one group or very few +// - a few labels are sparse with more groups in them +// - very few comprise the universe of all groups +type Groupings struct { + n uint + maxGroup uint16 + bitsetThreshold uint16 +} + +// NewGroupings creates a new Groupings supporting labels for membership +// to up n groups. n must be a positive multiple of 16 and <=65536. +func NewGroupings(n int) (*Groupings, error) { + if n <= 0 || n > 65536 { + return nil, fmt.Errorf("n=%d groups is outside of valid range (0, 65536]", n) + } + if n%16 != 0 { + return nil, fmt.Errorf("n=%d groups is not a multiple of 16", n) + } + return &Groupings{n: uint(n), bitsetThreshold: uint16(n / 16)}, nil +} + +// N returns up to how many groups are supported. +// That is the value that was passed to NewGroupings. +func (gr *Groupings) N() int { + return int(gr.n) +} + +// WithinRange checks whether group is within the admissible range for +// labeling otherwise it returns an error. +func (gr *Groupings) WithinRange(group uint16) error { + if uint(group) >= gr.n { + return fmt.Errorf("group exceeds admissible maximum: %d >= %d", group, gr.n) + } + return nil +} + +type Grouping struct { + size uint16 + elems []uint16 +} + +func (g Grouping) Copy() Grouping { + elems2 := make([]uint16, len(g.elems), cap(g.elems)) + copy(elems2[:], g.elems[:]) + g.elems = elems2 + return g +} + +// search locates group among the sorted Grouping elements, it returns: +// * true if found +// * false if not found +// * the index at which group should be inserted to keep the +// elements sorted if not found and the bit-set representation is not in use +func (gr *Groupings) search(g *Grouping, group uint16) (found bool, j uint16) { + if g.size > gr.bitsetThreshold { + return bitsetContains(g, group), 0 + } + j = uint16(sort.Search(int(g.size), func(i int) bool { return g.elems[i] >= group })) + if j < g.size && g.elems[j] == group { + return true, 0 + } + return false, j +} + +func bitsetContains(g *Grouping, group uint16) bool { + return (g.elems[group/16] & (1 << (group % 16))) != 0 +} + +// AddTo adds the given group to the grouping. +func (gr *Groupings) AddTo(g *Grouping, group uint16) error { + if err := gr.WithinRange(group); err != nil { + return err + } + if group > gr.maxGroup { + gr.maxGroup = group + } + if g.size == 0 { + g.size = 1 + g.elems = []uint16{group} + return nil + } + found, j := gr.search(g, group) + if found { + return nil + } + newsize := g.size + 1 + if newsize > gr.bitsetThreshold { + // switching to a bit-set representation after the size point + // where the space cost is the same, the representation uses + // bitsetThreshold-many 16-bits words stored in elems. + // We don't always use the bit-set representation because + // * we expect small groupings and iteration to be common, + // iteration is more costly over the bit-set representation + // * serialization matches more or less what we do in memory, + // so again is more efficient for small groupings in the + // extensive representation. + if g.size == gr.bitsetThreshold { + prevelems := g.elems + g.elems = make([]uint16, gr.bitsetThreshold) + for _, e := range prevelems { + bitsetAdd(g, e) + } + } + g.size = newsize + bitsetAdd(g, group) + return nil + } + var newelems []uint16 + if int(g.size) == cap(g.elems) { + newelems = make([]uint16, newsize, cap(g.elems)*2) + copy(newelems, g.elems[:j]) + } else { + newelems = g.elems[:newsize] + } + if j < g.size { + copy(newelems[j+1:], g.elems[j:]) + } + // inserting new group at j index keeping the elements sorted + newelems[j] = group + g.size = newsize + g.elems = newelems + return nil +} + +func bitsetAdd(g *Grouping, group uint16) { + g.elems[group/16] |= 1 << (group % 16) +} + +// Contains returns whether the given group is a member of the grouping. +func (gr *Groupings) Contains(g *Grouping, group uint16) bool { + found, _ := gr.search(g, group) + return found +} + +// Serialize produces a string encoding the given integers. +func Serialize(elems []uint16) string { + b := bytes.NewBuffer(make([]byte, 0, len(elems)*2)) + binary.Write(b, binary.LittleEndian, elems) + return base64.RawURLEncoding.EncodeToString(b.Bytes()) +} + +// Serialize produces a string representing the grouping label. +func (gr *Groupings) Serialize(g *Grouping) string { + // groupings are serialized as: + // * the actual element groups if there are up to + // bitsetThreshold elements: elems[0], elems[1], ... + // * otherwise the number of elements, followed by the bitset + // representation comprised of bitsetThreshold-many 16-bits words + // (stored using elems as well) + if g.size > gr.bitsetThreshold { + return gr.bitsetSerialize(g) + } + return Serialize(g.elems) +} + +func (gr *Groupings) bitsetSerialize(g *Grouping) string { + b := bytes.NewBuffer(make([]byte, 0, (gr.bitsetThreshold+1)*2)) + binary.Write(b, binary.LittleEndian, g.size) + binary.Write(b, binary.LittleEndian, g.elems) + return base64.RawURLEncoding.EncodeToString(b.Bytes()) +} + +const errSerializedLabelFmt = "invalid serialized grouping label: %v" + +// Deserialize reconstructs a grouping out of the serialized label. +func (gr *Groupings) Deserialize(label string) (*Grouping, error) { + b, err := base64.RawURLEncoding.DecodeString(label) + if err != nil { + return nil, fmt.Errorf(errSerializedLabelFmt, err) + } + if len(b)%2 != 0 { + return nil, fmt.Errorf(errSerializedLabelFmt, "not divisible into 16-bits words") + } + m := len(b) / 2 + var g Grouping + if m == int(gr.bitsetThreshold+1) { + // deserialize number of elements + bitset representation + // comprising bitsetThreshold-many 16-bits words + return gr.bitsetDeserialize(&g, b) + } + if m > int(gr.bitsetThreshold) { + return nil, fmt.Errorf(errSerializedLabelFmt, "too large") + } + g.size = uint16(m) + esz := uint16(1) + for esz < g.size { + esz *= 2 + } + g.elems = make([]uint16, g.size, esz) + binary.Read(bytes.NewBuffer(b), binary.LittleEndian, g.elems) + for i, e := range g.elems { + if e > gr.maxGroup { + return nil, fmt.Errorf(errSerializedLabelFmt, "element larger than maximum group") + } + if i > 0 && g.elems[i-1] >= e { + return nil, fmt.Errorf(errSerializedLabelFmt, "not sorted") + } + } + return &g, nil +} + +func (gr *Groupings) bitsetDeserialize(g *Grouping, b []byte) (*Grouping, error) { + buf := bytes.NewBuffer(b) + binary.Read(buf, binary.LittleEndian, &g.size) + if g.size > gr.maxGroup+1 { + return nil, fmt.Errorf(errSerializedLabelFmt, "bitset size cannot be possibly larger than maximum group plus 1") + } + if g.size <= gr.bitsetThreshold { + // should not have used a bitset repr for so few elements + return nil, fmt.Errorf(errSerializedLabelFmt, "bitset for too few elements") + } + g.elems = make([]uint16, gr.bitsetThreshold) + binary.Read(buf, binary.LittleEndian, g.elems) + return g, nil +} + +// Iter iterates over the groups in the grouping and calls f with each of +// them. If f returns an error Iter immediately returns with it. +func (gr *Groupings) Iter(g *Grouping, f func(group uint16) error) error { + if g.size > gr.bitsetThreshold { + return gr.bitsetIter(g, f) + } + for _, e := range g.elems { + if err := f(e); err != nil { + return err + } + } + return nil +} + +func (gr *Groupings) bitsetIter(g *Grouping, f func(group uint16) error) error { + c := g.size + for i := uint16(0); i <= gr.maxGroup/16; i++ { + w := g.elems[i] + if w == 0 { + continue + } + for j := uint16(0); w != 0; j++ { + if w&1 != 0 { + if err := f(i*16 + j); err != nil { + return err + } + c-- + if c == 0 { + // found all elements + return nil + } + } + w >>= 1 + } + } + return nil +} diff -Nru snapd-2.45.1+20.04.2/asserts/internal/grouping_test.go snapd-2.48.3+20.04/asserts/internal/grouping_test.go --- snapd-2.45.1+20.04.2/asserts/internal/grouping_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48.3+20.04/asserts/internal/grouping_test.go 2021-02-02 08:21:12.000000000 +0000 @@ -0,0 +1,713 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2020 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 internal_test + +import ( + "encoding/base64" + "errors" + "testing" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/asserts/internal" +) + +func TestInternal(t *testing.T) { TestingT(t) } + +type groupingsSuite struct{} + +var _ = Suite(&groupingsSuite{}) + +func (s *groupingsSuite) TestNewGroupings(c *C) { + tests := []struct { + n int + err string + }{ + {-10, `n=-10 groups is outside of valid range \(0, 65536\]`}, + {0, `n=0 groups is outside of valid range \(0, 65536\]`}, + {9, "n=9 groups is not a multiple of 16"}, + {16, ""}, + {255, "n=255 groups is not a multiple of 16"}, + {256, ""}, + {1024, ""}, + {65536, ""}, + {65537, `n=65537 groups is outside of valid range \(0, 65536\]`}, + } + + for _, t := range tests { + comm := Commentf("%d", t.n) + gr, err := internal.NewGroupings(t.n) + if t.err == "" { + c.Check(err, IsNil, comm) + c.Check(gr, NotNil, comm) + c.Check(gr.N(), Equals, t.n) + } else { + c.Check(gr, IsNil, comm) + c.Check(err, ErrorMatches, t.err, comm) + } + } +} + +func (s *groupingsSuite) TestAddToAndContains(c *C) { + var g internal.Grouping + + gr, err := internal.NewGroupings(16) + c.Assert(err, IsNil) + + err = gr.AddTo(&g, 1) + c.Assert(err, IsNil) + err = gr.AddTo(&g, 3) + c.Assert(err, IsNil) + err = gr.AddTo(&g, 0) + c.Assert(err, IsNil) + err = gr.AddTo(&g, 4) + c.Assert(err, IsNil) + err = gr.AddTo(&g, 2) + c.Assert(err, IsNil) + + for i := uint16(0); i < 5; i++ { + c.Check(gr.Contains(&g, i), Equals, true) + } + + c.Check(gr.Contains(&g, 5), Equals, false) +} + +func (s *groupingsSuite) TestOutsideRange(c *C) { + var g internal.Grouping + + gr, err := internal.NewGroupings(16) + c.Assert(err, IsNil) + + // sanity + err = gr.AddTo(&g, 15) + c.Assert(err, IsNil) + + err = gr.AddTo(&g, 16) + c.Check(err, ErrorMatches, "group exceeds admissible maximum: 16 >= 16") + + err = gr.AddTo(&g, 99) + c.Check(err, ErrorMatches, "group exceeds admissible maximum: 99 >= 16") +} + +func (s *groupingsSuite) TestSerializeLabel(c *C) { + var g internal.Grouping + + gr, err := internal.NewGroupings(128) + c.Assert(err, IsNil) + + err = gr.AddTo(&g, 1) + c.Assert(err, IsNil) + err = gr.AddTo(&g, 3) + c.Assert(err, IsNil) + err = gr.AddTo(&g, 0) + c.Assert(err, IsNil) + err = gr.AddTo(&g, 4) + c.Assert(err, IsNil) + err = gr.AddTo(&g, 2) + c.Assert(err, IsNil) + + l := gr.Serialize(&g) + + g1, err := gr.Deserialize(l) + c.Check(err, IsNil) + + c.Check(g1, DeepEquals, &g) +} + +func (s *groupingsSuite) TestDeserializeLabelErrors(c *C) { + var g internal.Grouping + + gr, err := internal.NewGroupings(64) + c.Assert(err, IsNil) + + err = gr.AddTo(&g, 0) + c.Assert(err, IsNil) + err = gr.AddTo(&g, 1) + c.Assert(err, IsNil) + err = gr.AddTo(&g, 2) + c.Assert(err, IsNil) + err = gr.AddTo(&g, 3) + c.Assert(err, IsNil) + err = gr.AddTo(&g, 4) + c.Assert(err, IsNil) + + const errPrefix = "invalid serialized grouping label: " + + invalidLabels := []struct { + invalid, errSuffix string + }{ + // not base64 + {"\x0a\x02\xf4", `illegal base64 data.*`}, + // wrong length + {base64.RawURLEncoding.EncodeToString([]byte{1}), `not divisible into 16-bits words`}, + // not a known group + {internal.Serialize([]uint16{5}), `element larger than maximum group`}, + // not in order + {internal.Serialize([]uint16{0, 2, 1}), `not sorted`}, + // bitset: too many words + {internal.Serialize([]uint16{0, 0, 0, 0, 0, 0}), `too large`}, + // bitset: larger than maxgroup + {internal.Serialize([]uint16{6, 0, 0, 0, 0}), `bitset size cannot be possibly larger than maximum group plus 1`}, + // bitset: grouping size is too small + {internal.Serialize([]uint16{0, 0, 0, 0, 0}), `bitset for too few elements`}, + {internal.Serialize([]uint16{1, 0, 0, 0, 0}), `bitset for too few elements`}, + {internal.Serialize([]uint16{4, 0, 0, 0, 0}), `bitset for too few elements`}, + } + + for _, il := range invalidLabels { + _, err := gr.Deserialize(il.invalid) + c.Check(err, ErrorMatches, errPrefix+il.errSuffix) + } +} + +func (s *groupingsSuite) TestIter(c *C) { + var g internal.Grouping + + gr, err := internal.NewGroupings(128) + c.Assert(err, IsNil) + + err = gr.AddTo(&g, 1) + c.Assert(err, IsNil) + err = gr.AddTo(&g, 3) + c.Assert(err, IsNil) + err = gr.AddTo(&g, 0) + c.Assert(err, IsNil) + err = gr.AddTo(&g, 4) + c.Assert(err, IsNil) + err = gr.AddTo(&g, 2) + c.Assert(err, IsNil) + + elems := []uint16{} + f := func(group uint16) error { + elems = append(elems, group) + return nil + } + + err = gr.Iter(&g, f) + c.Assert(err, IsNil) + c.Check(elems, DeepEquals, []uint16{0, 1, 2, 3, 4}) +} + +func (s *groupingsSuite) TestIterError(c *C) { + var g internal.Grouping + + gr, err := internal.NewGroupings(32) + c.Assert(err, IsNil) + + err = gr.AddTo(&g, 1) + c.Assert(err, IsNil) + err = gr.AddTo(&g, 3) + c.Assert(err, IsNil) + + errBoom := errors.New("boom") + n := 0 + f := func(group uint16) error { + n++ + return errBoom + } + + err = gr.Iter(&g, f) + c.Check(err, Equals, errBoom) + c.Check(n, Equals, 1) +} + +func (s *groupingsSuite) TestRepeated(c *C) { + var g internal.Grouping + + gr, err := internal.NewGroupings(64) + c.Assert(err, IsNil) + + err = gr.AddTo(&g, 1) + c.Assert(err, IsNil) + err = gr.AddTo(&g, 0) + c.Assert(err, IsNil) + err = gr.AddTo(&g, 2) + c.Assert(err, IsNil) + + err = gr.AddTo(&g, 1) + c.Assert(err, IsNil) + err = gr.AddTo(&g, 0) + c.Assert(err, IsNil) + + elems := []uint16{} + f := func(group uint16) error { + elems = append(elems, group) + return nil + } + + err = gr.Iter(&g, f) + c.Assert(err, IsNil) + c.Check(elems, DeepEquals, []uint16{0, 1, 2}) +} + +func (s *groupingsSuite) TestCopy(c *C) { + var g internal.Grouping + + gr, err := internal.NewGroupings(16) + c.Assert(err, IsNil) + + err = gr.AddTo(&g, 1) + c.Assert(err, IsNil) + err = gr.AddTo(&g, 3) + c.Assert(err, IsNil) + err = gr.AddTo(&g, 0) + c.Assert(err, IsNil) + err = gr.AddTo(&g, 4) + c.Assert(err, IsNil) + err = gr.AddTo(&g, 2) + c.Assert(err, IsNil) + + g2 := g.Copy() + c.Check(g2, DeepEquals, g) + + err = gr.AddTo(&g2, 7) + c.Assert(err, IsNil) + + c.Check(gr.Contains(&g, 7), Equals, false) + c.Check(gr.Contains(&g2, 7), Equals, true) + + c.Check(g2, Not(DeepEquals), g) +} +func (s *groupingsSuite) TestBitsetSerializeAndIterSimple(c *C) { + gr, err := internal.NewGroupings(32) + c.Assert(err, IsNil) + + var elems []uint16 + f := func(group uint16) error { + elems = append(elems, group) + return nil + } + + var g internal.Grouping + err = gr.AddTo(&g, 1) + c.Assert(err, IsNil) + err = gr.AddTo(&g, 5) + c.Assert(err, IsNil) + err = gr.AddTo(&g, 17) + c.Assert(err, IsNil) + err = gr.AddTo(&g, 24) + c.Assert(err, IsNil) + + l := gr.Serialize(&g) + c.Check(l, DeepEquals, + internal.Serialize([]uint16{4, + uint16(1<<1 | 1<<5), + uint16(1<<(17-16) | 1<<(24-16)), + })) + + err = gr.Iter(&g, f) + c.Assert(err, IsNil) + c.Check(elems, DeepEquals, []uint16{1, 5, 17, 24}) +} + +func (s *groupingsSuite) TestBitSet(c *C) { + var g internal.Grouping + + gr, err := internal.NewGroupings(64) + c.Assert(err, IsNil) + + for i := uint16(0); i < 64; i++ { + err := gr.AddTo(&g, i) + c.Assert(err, IsNil) + c.Check(gr.Contains(&g, i), Equals, true) + + l := gr.Serialize(&g) + + switch i { + case 4: + c.Check(l, Equals, internal.Serialize([]uint16{5, 0x1f, 0, 0, 0})) + case 15: + c.Check(l, Equals, internal.Serialize([]uint16{16, 0xffff, 0, 0, 0})) + case 16: + c.Check(l, Equals, internal.Serialize([]uint16{17, 0xffff, 0x1, 0, 0})) + case 63: + c.Check(l, Equals, internal.Serialize([]uint16{64, 0xffff, 0xffff, 0xffff, 0xffff})) + } + + g1, err := gr.Deserialize(l) + c.Check(err, IsNil) + + c.Check(g1, DeepEquals, &g) + } + + for i := uint16(63); ; i-- { + err := gr.AddTo(&g, i) + c.Assert(err, IsNil) + c.Check(gr.Contains(&g, i), Equals, true) + if i == 0 { + break + } + + l := gr.Serialize(&g) + + g1, err := gr.Deserialize(l) + c.Check(err, IsNil) + + c.Check(g1, DeepEquals, &g) + } +} + +func (s *groupingsSuite) TestBitsetIter(c *C) { + gr, err := internal.NewGroupings(32) + c.Assert(err, IsNil) + + var elems []uint16 + f := func(group uint16) error { + elems = append(elems, group) + return nil + } + + for i := uint16(2); i < 32; i++ { + var g internal.Grouping + + err := gr.AddTo(&g, i-2) + c.Assert(err, IsNil) + err = gr.AddTo(&g, i-1) + c.Assert(err, IsNil) + err = gr.AddTo(&g, i) + c.Assert(err, IsNil) + + err = gr.Iter(&g, f) + c.Assert(err, IsNil) + c.Check(elems, DeepEquals, []uint16{i - 2, i - 1, i}) + + elems = nil + } + + var g internal.Grouping + for i := uint16(0); i < 32; i++ { + err = gr.AddTo(&g, i) + c.Assert(err, IsNil) + } + + err = gr.Iter(&g, f) + c.Assert(err, IsNil) + c.Check(elems, HasLen, 32) +} + +func (s *groupingsSuite) TestBitsetIterError(c *C) { + gr, err := internal.NewGroupings(16) + c.Assert(err, IsNil) + + var g internal.Grouping + + err = gr.AddTo(&g, 0) + c.Assert(err, IsNil) + err = gr.AddTo(&g, 1) + c.Assert(err, IsNil) + + errBoom := errors.New("boom") + n := 0 + f := func(group uint16) error { + n++ + return errBoom + } + + err = gr.Iter(&g, f) + c.Check(err, Equals, errBoom) + c.Check(n, Equals, 1) +} + +func BenchmarkIterBaseline(b *testing.B) { + b.StopTimer() + + n := 0 + f := func(group uint16) error { + n++ + return nil + } + + b.StartTimer() + for i := 0; i < b.N; i++ { + n = 0 + for j := uint16(0); j < 64; j++ { + f(j) + } + if n != 64 { + b.FailNow() + } + } +} + +func BenchmarkIter4Elems(b *testing.B) { + b.StopTimer() + + gr, err := internal.NewGroupings(64) + if err != nil { + b.FailNow() + } + + n := 0 + f := func(group uint16) error { + n++ + return nil + } + + var g internal.Grouping + gr.AddTo(&g, 1) + gr.AddTo(&g, 5) + gr.AddTo(&g, 17) + gr.AddTo(&g, 24) + + b.StartTimer() + for i := 0; i < b.N; i++ { + n = 0 + gr.Iter(&g, f) + if n != 4 { + b.FailNow() + } + } +} + +func BenchmarkIterBitset5Elems(b *testing.B) { + b.StopTimer() + + gr, err := internal.NewGroupings(64) + if err != nil { + b.FailNow() + } + + n := 0 + f := func(group uint16) error { + n++ + return nil + } + + var g internal.Grouping + gr.AddTo(&g, 1) + gr.AddTo(&g, 5) + gr.AddTo(&g, 17) + gr.AddTo(&g, 24) + gr.AddTo(&g, 33) + + b.StartTimer() + for i := 0; i < b.N; i++ { + n = 0 + gr.Iter(&g, f) + if n != 5 { + b.FailNow() + } + } +} + +func BenchmarkIterBitsetEmptyStretches(b *testing.B) { + b.StopTimer() + + gr, err := internal.NewGroupings(64) + if err != nil { + b.FailNow() + } + + n := 0 + f := func(group uint16) error { + n++ + return nil + } + + var g internal.Grouping + gr.AddTo(&g, 0) + gr.AddTo(&g, 15) + gr.AddTo(&g, 16) + gr.AddTo(&g, 31) + gr.AddTo(&g, 32) + + b.StartTimer() + for i := 0; i < b.N; i++ { + n = 0 + gr.Iter(&g, f) + if n != 5 { + b.FailNow() + } + } +} + +func BenchmarkIterBitsetEven(b *testing.B) { + b.StopTimer() + + gr, err := internal.NewGroupings(64) + if err != nil { + b.FailNow() + } + + n := 0 + f := func(group uint16) error { + n++ + return nil + } + + var g internal.Grouping + for i := 0; i <= 63; i += 2 { + gr.AddTo(&g, uint16(i)) + } + + b.StartTimer() + for i := 0; i < b.N; i++ { + n = 0 + gr.Iter(&g, f) + if n != 32 { + b.FailNow() + } + } +} + +func BenchmarkIterBitsetOdd(b *testing.B) { + b.StopTimer() + + gr, err := internal.NewGroupings(64) + if err != nil { + b.FailNow() + } + + n := 0 + f := func(group uint16) error { + n++ + return nil + } + + var g internal.Grouping + for i := 1; i <= 63; i += 2 { + gr.AddTo(&g, uint16(i)) + } + + b.StartTimer() + for i := 0; i < b.N; i++ { + n = 0 + gr.Iter(&g, f) + if n != 32 { + b.FailNow() + } + } +} + +func BenchmarkIterBitset0Inc3(b *testing.B) { + b.StopTimer() + + gr, err := internal.NewGroupings(64) + if err != nil { + b.FailNow() + } + + n := 0 + f := func(group uint16) error { + n++ + return nil + } + + var g internal.Grouping + for i := 0; i <= 63; i += 3 { + gr.AddTo(&g, uint16(i)) + } + + b.StartTimer() + for i := 0; i < b.N; i++ { + n = 0 + gr.Iter(&g, f) + if n != 22 { + b.FailNow() + } + } +} + +func BenchmarkIterBitset1Inc3(b *testing.B) { + b.StopTimer() + + gr, err := internal.NewGroupings(64) + if err != nil { + b.FailNow() + } + + n := 0 + f := func(group uint16) error { + n++ + return nil + } + + var g internal.Grouping + for i := 1; i <= 63; i += 3 { + gr.AddTo(&g, uint16(i)) + } + + b.StartTimer() + for i := 0; i < b.N; i++ { + n = 0 + gr.Iter(&g, f) + if n != 21 { + b.FailNow() + } + } +} + +func BenchmarkIterBitset0Inc4(b *testing.B) { + b.StopTimer() + + gr, err := internal.NewGroupings(64) + if err != nil { + b.FailNow() + } + + n := 0 + f := func(group uint16) error { + n++ + return nil + } + + var g internal.Grouping + for i := 0; i <= 63; i += 4 { + gr.AddTo(&g, uint16(i)) + } + + b.StartTimer() + for i := 0; i < b.N; i++ { + n = 0 + gr.Iter(&g, f) + if n != 16 { + b.FailNow() + } + } +} + +func BenchmarkIterBitsetComplete(b *testing.B) { + b.StopTimer() + + gr, err := internal.NewGroupings(64) + if err != nil { + b.FailNow() + } + + n := 0 + f := func(group uint16) error { + n++ + return nil + } + + var g internal.Grouping + for i := 0; i <= 63; i++ { + gr.AddTo(&g, uint16(i)) + } + + b.StartTimer() + for i := 0; i < b.N; i++ { + n = 0 + gr.Iter(&g, f) + if n != 64 { + b.FailNow() + } + } +} diff -Nru snapd-2.45.1+20.04.2/asserts/membackstore.go snapd-2.48.3+20.04/asserts/membackstore.go --- snapd-2.45.1+20.04.2/asserts/membackstore.go 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/asserts/membackstore.go 2021-02-02 08:21:12.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2016 Canonical Ltd + * Copyright (C) 2016-2020 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as @@ -21,6 +21,9 @@ import ( "errors" + "fmt" + "sort" + "strconv" "sync" ) @@ -33,12 +36,18 @@ put(assertType *AssertionType, key []string, assert Assertion) error get(key []string, maxFormat int) (Assertion, error) search(hint []string, found func(Assertion), maxFormat int) + sequenceMemberAfter(prefix []string, after, maxFormat int) (Assertion, error) } type memBSBranch map[string]memBSNode type memBSLeaf map[string]map[int]Assertion +type memBSSeqLeaf struct { + memBSLeaf + sequence []int +} + func (br memBSBranch) put(assertType *AssertionType, key []string, assert Assertion) error { key0 := key[0] down := br[key0] @@ -46,7 +55,12 @@ if len(key) > 2 { down = make(memBSBranch) } else { - down = make(memBSLeaf) + leaf := make(memBSLeaf) + if assertType.SequenceForming() { + down = &memBSSeqLeaf{memBSLeaf: leaf} + } else { + down = leaf + } } br[key0] = down } @@ -81,6 +95,23 @@ return nil } +func (leaf *memBSSeqLeaf) put(assertType *AssertionType, key []string, assert Assertion) error { + if err := leaf.memBSLeaf.put(assertType, key, assert); err != nil { + return err + } + if len(leaf.memBSLeaf) != len(leaf.sequence) { + seqnum := assert.(SequenceMember).Sequence() + inspos := sort.SearchInts(leaf.sequence, seqnum) + n := len(leaf.sequence) + leaf.sequence = append(leaf.sequence, seqnum) + if inspos != n { + copy(leaf.sequence[inspos+1:n+1], leaf.sequence[inspos:n]) + leaf.sequence[inspos] = seqnum + } + } + return nil +} + // errNotFound is used internally by backends, it is converted to the richer // NotFoundError only at their public interface boundary var errNotFound = errors.New("assertion not found") @@ -136,6 +167,52 @@ } } +func (br memBSBranch) sequenceMemberAfter(prefix []string, after, maxFormat int) (Assertion, error) { + prefix0 := prefix[0] + down := br[prefix0] + if down == nil { + return nil, errNotFound + } + return down.sequenceMemberAfter(prefix[1:], after, maxFormat) +} + +func (left memBSLeaf) sequenceMemberAfter(prefix []string, after, maxFormat int) (Assertion, error) { + panic("internal error: unexpected sequenceMemberAfter on memBSLeaf") +} + +func (leaf *memBSSeqLeaf) sequenceMemberAfter(prefix []string, after, maxFormat int) (Assertion, error) { + n := len(leaf.sequence) + dir := 1 + var start int + if after == -1 { + // search for the latest in sequence compatible with + // maxFormat: consider all sequence numbers in + // sequence backward + dir = -1 + start = n - 1 + } else { + // search for the first in sequence with sequence number + // > after and compatible with maxFormat + start = sort.SearchInts(leaf.sequence, after) + if start == n { + // nothing + return nil, errNotFound + } + if leaf.sequence[start] == after { + // skip after itself + start += 1 + } + } + for j := start; j >= 0 && j < n; j += dir { + seqkey := strconv.Itoa(leaf.sequence[j]) + cur := leaf.cur(seqkey, maxFormat) + if cur != nil { + return cur, nil + } + } + return nil, errNotFound +} + // NewMemoryBackstore creates a memory backed assertions backstore. func NewMemoryBackstore() Backstore { return &memoryBackstore{ @@ -189,3 +266,25 @@ mbs.top.search(hint, candCb, maxFormat) return nil } + +func (mbs *memoryBackstore) SequenceMemberAfter(assertType *AssertionType, sequenceKey []string, after, maxFormat int) (SequenceMember, error) { + if !assertType.SequenceForming() { + panic(fmt.Sprintf("internal error: SequenceMemberAfter on non sequence-forming assertion type %q", assertType.Name)) + } + if len(sequenceKey) != len(assertType.PrimaryKey)-1 { + return nil, fmt.Errorf("internal error: SequenceMemberAfter's sequence key argument length must be exactly 1 less than the assertion type primary key") + } + + mbs.mu.RLock() + defer mbs.mu.RUnlock() + + internalPrefix := make([]string, len(assertType.PrimaryKey)) + internalPrefix[0] = assertType.Name + copy(internalPrefix[1:], sequenceKey) + + a, err := mbs.top.sequenceMemberAfter(internalPrefix, after, maxFormat) + if err == errNotFound { + return nil, &NotFoundError{Type: assertType} + } + return a.(SequenceMember), err +} diff -Nru snapd-2.45.1+20.04.2/asserts/membackstore_test.go snapd-2.48.3+20.04/asserts/membackstore_test.go --- snapd-2.45.1+20.04.2/asserts/membackstore_test.go 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/asserts/membackstore_test.go 2021-02-02 08:21:12.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2016 Canonical Ltd + * Copyright (C) 2016-2020 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 @@ -349,3 +349,191 @@ c.Check(as[0].Revision(), Equals, 1) } + +func (mbss *memBackstoreSuite) TestPutSequence(c *C) { + bs := asserts.NewMemoryBackstore() + + sq1f0, err := asserts.Decode([]byte("type: test-only-seq\n" + + "authority-id: auth-id1\n" + + "n: s1\n" + + "sequence: 1\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==")) + c.Assert(err, IsNil) + + sq2f0, err := asserts.Decode([]byte("type: test-only-seq\n" + + "authority-id: auth-id1\n" + + "n: s1\n" + + "sequence: 2\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==")) + c.Assert(err, IsNil) + + sq2f1, err := asserts.Decode([]byte("type: test-only-seq\n" + + "authority-id: auth-id1\n" + + "format: 1\n" + + "n: s1\n" + + "sequence: 2\n" + + "revision: 1\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==")) + c.Assert(err, IsNil) + + sq3f1, err := asserts.Decode([]byte("type: test-only-seq\n" + + "authority-id: auth-id1\n" + + "format: 1\n" + + "n: s1\n" + + "sequence: 3\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==")) + c.Assert(err, IsNil) + + for _, a := range []asserts.Assertion{sq3f1, sq1f0, sq2f0, sq2f1} { + err = bs.Put(asserts.TestOnlySeqType, a) + c.Assert(err, IsNil) + } + + a, err := bs.Get(asserts.TestOnlySeqType, []string{"s1", "1"}, 0) + c.Assert(err, IsNil) + c.Check(a.(asserts.SequenceMember).Sequence(), Equals, 1) + c.Check(a.Format(), Equals, 0) + + a, err = bs.Get(asserts.TestOnlySeqType, []string{"s1", "2"}, 0) + c.Assert(err, IsNil) + c.Check(a.(asserts.SequenceMember).Sequence(), Equals, 2) + c.Check(a.Format(), Equals, 0) + + a, err = bs.Get(asserts.TestOnlySeqType, []string{"s1", "2"}, 1) + c.Assert(err, IsNil) + c.Check(a.(asserts.SequenceMember).Sequence(), Equals, 2) + c.Check(a.Format(), Equals, 1) + + a, err = bs.Get(asserts.TestOnlySeqType, []string{"s1", "3"}, 0) + c.Assert(err, DeepEquals, &asserts.NotFoundError{ + Type: asserts.TestOnlySeqType, + }) + + a, err = bs.Get(asserts.TestOnlySeqType, []string{"s1", "3"}, 1) + c.Assert(err, IsNil) + c.Check(a.(asserts.SequenceMember).Sequence(), Equals, 3) + c.Check(a.Format(), Equals, 1) + + err = bs.Put(asserts.TestOnlySeqType, sq2f0) + c.Check(err, DeepEquals, &asserts.RevisionError{Current: 1, Used: 0}) +} + +func (mbss *memBackstoreSuite) TestSequenceMemberAfter(c *C) { + bs := asserts.NewMemoryBackstore() + + other1, err := asserts.Decode([]byte("type: test-only-seq\n" + + "authority-id: auth-id1\n" + + "n: other\n" + + "sequence: 1\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==")) + c.Assert(err, IsNil) + + sq1f0, err := asserts.Decode([]byte("type: test-only-seq\n" + + "authority-id: auth-id1\n" + + "n: s1\n" + + "sequence: 1\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==")) + c.Assert(err, IsNil) + + sq2f0, err := asserts.Decode([]byte("type: test-only-seq\n" + + "authority-id: auth-id1\n" + + "n: s1\n" + + "sequence: 2\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==")) + c.Assert(err, IsNil) + + sq2f1, err := asserts.Decode([]byte("type: test-only-seq\n" + + "authority-id: auth-id1\n" + + "format: 1\n" + + "n: s1\n" + + "sequence: 2\n" + + "revision: 1\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==")) + c.Assert(err, IsNil) + + sq3f1, err := asserts.Decode([]byte("type: test-only-seq\n" + + "authority-id: auth-id1\n" + + "format: 1\n" + + "n: s1\n" + + "sequence: 3\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==")) + c.Assert(err, IsNil) + + sq3f2, err := asserts.Decode([]byte("type: test-only-seq\n" + + "authority-id: auth-id1\n" + + "format: 2\n" + + "n: s1\n" + + "sequence: 3\n" + + "revision: 1\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==")) + c.Assert(err, IsNil) + + for _, a := range []asserts.Assertion{other1, sq1f0, sq2f0, sq2f1, sq3f1, sq3f2} { + err = bs.Put(asserts.TestOnlySeqType, a) + c.Assert(err, IsNil) + } + + seqKey := []string{"s1"} + tests := []struct { + after int + maxFormat int + sequence int + format int + revision int + }{ + {after: 0, maxFormat: 0, sequence: 1, format: 0, revision: 0}, + {after: 0, maxFormat: 2, sequence: 1, format: 0, revision: 0}, + {after: 1, maxFormat: 0, sequence: 2, format: 0, revision: 0}, + {after: 1, maxFormat: 1, sequence: 2, format: 1, revision: 1}, + {after: 1, maxFormat: 2, sequence: 2, format: 1, revision: 1}, + {after: 2, maxFormat: 0, sequence: -1}, + {after: 2, maxFormat: 1, sequence: 3, format: 1, revision: 0}, + {after: 2, maxFormat: 2, sequence: 3, format: 2, revision: 1}, + {after: 3, maxFormat: 0, sequence: -1}, + {after: 3, maxFormat: 2, sequence: -1}, + {after: 4, maxFormat: 2, sequence: -1}, + {after: -1, maxFormat: 0, sequence: 2, format: 0, revision: 0}, + {after: -1, maxFormat: 1, sequence: 3, format: 1, revision: 0}, + {after: -1, maxFormat: 2, sequence: 3, format: 2, revision: 1}, + } + + for _, t := range tests { + a, err := bs.SequenceMemberAfter(asserts.TestOnlySeqType, seqKey, t.after, t.maxFormat) + if t.sequence == -1 { + c.Check(err, DeepEquals, &asserts.NotFoundError{ + Type: asserts.TestOnlySeqType, + }) + } else { + c.Assert(err, IsNil) + c.Assert(a.HeaderString("n"), Equals, "s1") + c.Check(a.Sequence(), Equals, t.sequence) + c.Check(a.Format(), Equals, t.format) + c.Check(a.Revision(), Equals, t.revision) + } + } + + _, err = bs.SequenceMemberAfter(asserts.TestOnlySeqType, []string{"s2"}, -1, 2) + c.Check(err, DeepEquals, &asserts.NotFoundError{ + Type: asserts.TestOnlySeqType, + }) +} diff -Nru snapd-2.45.1+20.04.2/asserts/model.go snapd-2.48.3+20.04/asserts/model.go --- snapd-2.45.1+20.04.2/asserts/model.go 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/asserts/model.go 2021-02-02 08:21:12.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2016-2019 Canonical Ltd + * Copyright (C) 2016-2020 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 @@ -339,6 +339,25 @@ ModelDangerous ModelGrade = "dangerous" ) +// StorageSafety characterizes the requested storage safety of +// the model which then controls what encryption is used +type StorageSafety string + +const ( + StorageSafetyUnset StorageSafety = "unset" + // StorageSafetyEncrypted implies mandatory full disk encryption. + StorageSafetyEncrypted StorageSafety = "encrypted" + // StorageSafetyPreferEncrypted implies full disk + // encryption when the system supports it. + StorageSafetyPreferEncrypted StorageSafety = "prefer-encrypted" + // StorageSafetyPreferUnencrypted implies no full disk + // encryption by default even if the system supports + // encryption. + StorageSafetyPreferUnencrypted StorageSafety = "prefer-unencrypted" +) + +var validStorageSafeties = []string{string(StorageSafetyEncrypted), string(StorageSafetyPreferEncrypted), string(StorageSafetyPreferUnencrypted)} + var validModelGrades = []string{string(ModelSecured), string(ModelSigned), string(ModelDangerous)} // gradeToCode encodes grades into 32 bits, trying to be slightly future-proof: @@ -374,6 +393,8 @@ grade ModelGrade + storageSafety StorageSafety + allSnaps []*ModelSnap // consumers of this info should care only about snap identity => // snapRef @@ -415,17 +436,23 @@ return mod.classic } -// Architecture returns the archicteture the model is based on. +// Architecture returns the architecture the model is based on. func (mod *Model) Architecture() string { return mod.HeaderString("architecture") } -// Grade returns the stability grade of the model. Will be ModeGradeUnset +// Grade returns the stability grade of the model. Will be ModelGradeUnset // for Core 16/18 models. func (mod *Model) Grade() ModelGrade { return mod.grade } +// StorageSafety returns the storage safety for the model. Will be +// StorageSafetyUnset for Core 16/18 models. +func (mod *Model) StorageSafety() StorageSafety { + return mod.storageSafety +} + // GadgetSnap returns the details of the gadget snap the model uses. func (mod *Model) GadgetSnap() *ModelSnap { return mod.gadgetSnap @@ -486,19 +513,29 @@ return mod.HeaderString("store") } -// RequiredNoEssentialSnaps returns the snaps that must be installed at all times and cannot be removed for this model, excluding the essential snaps (gadget, kernel, boot base). +// RequiredNoEssentialSnaps returns the snaps that must be installed at all times and cannot be removed for this model, excluding the essential snaps (gadget, kernel, boot base, snapd). func (mod *Model) RequiredNoEssentialSnaps() []naming.SnapRef { return mod.requiredWithEssentialSnaps[mod.numEssentialSnaps:] } -// RequiredWithEssentialSnaps returns the snaps that must be installed at all times and cannot be removed for this model, including the essential snaps (gadget, kernel, boot base). +// RequiredWithEssentialSnaps returns the snaps that must be installed at all times and cannot be removed for this model, including any essential snaps (gadget, kernel, boot base, snapd). func (mod *Model) RequiredWithEssentialSnaps() []naming.SnapRef { return mod.requiredWithEssentialSnaps } -// AllSnaps returns all the snap listed by the model. -func (mod *Model) AllSnaps() []*ModelSnap { - return mod.allSnaps +// EssentialSnaps returns all essential snaps explicitly mentioned by +// the model. +// They are always returned according to this order with some skipped +// if not mentioned: snapd, kernel, boot base, gadget. +func (mod *Model) EssentialSnaps() []*ModelSnap { + return mod.allSnaps[:mod.numEssentialSnaps] +} + +// SnapsWithoutEssential returns all the snaps listed by the model +// without any of the essential snaps (as returned by EssentialSnaps). +// They are returned in the order of mention by the model. +func (mod *Model) SnapsWithoutEssential() []*ModelSnap { + return mod.allSnaps[mod.numEssentialSnaps:] } // SerialAuthority returns the authority ids that are accepted as @@ -633,6 +670,9 @@ if _, ok := assert.headers["grade"]; ok { return nil, fmt.Errorf("cannot specify a grade for model without the extended snaps header") } + if _, ok := assert.headers["storage-safety"]; ok { + return nil, fmt.Errorf("cannot specify storage-safety for model without the extended snaps header") + } } if classic { @@ -684,6 +724,7 @@ var modSnaps *modelSnaps grade := ModelGradeUnset + storageSafety := StorageSafetyUnset if extended { gradeStr, err := checkOptionalString(assert.headers, "grade") if err != nil { @@ -697,6 +738,27 @@ grade = ModelGrade(gradeStr) } + storageSafetyStr, err := checkOptionalString(assert.headers, "storage-safety") + if err != nil { + return nil, err + } + if storageSafetyStr != "" && !strutil.ListContains(validStorageSafeties, storageSafetyStr) { + return nil, fmt.Errorf("storage-safety for model must be %s, not %q", strings.Join(validStorageSafeties, "|"), storageSafetyStr) + } + if storageSafetyStr != "" { + storageSafety = StorageSafety(storageSafetyStr) + } else { + if grade == ModelSecured { + storageSafety = StorageSafetyEncrypted + } else { + storageSafety = StorageSafetyPreferEncrypted + } + } + + if grade == ModelSecured && storageSafety != StorageSafetyEncrypted { + return nil, fmt.Errorf(`secured grade model must not have storage-safety overridden, only "encrypted" is valid`) + } + modSnaps, err = checkExtendedSnaps(extendedSnaps, base, grade) if err != nil { return nil, err @@ -784,6 +846,7 @@ gadgetSnap: modSnaps.gadget, kernelSnap: modSnaps.kernel, grade: grade, + storageSafety: storageSafety, allSnaps: allSnaps, requiredWithEssentialSnaps: requiredWithEssentialSnaps, numEssentialSnaps: numEssentialSnaps, diff -Nru snapd-2.45.1+20.04.2/asserts/model_test.go snapd-2.48.3+20.04/asserts/model_test.go --- snapd-2.45.1+20.04.2/asserts/model_test.go 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/asserts/model_test.go 2021-02-02 08:21:12.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2016-2019 Canonical Ltd + * Copyright (C) 2016-2020 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 @@ -133,6 +133,7 @@ type: app presence: optional OTHERgrade: secured +storage-safety: encrypted ` + "TSLINE" + "body-length: 0\n" + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + @@ -178,11 +179,15 @@ }) c.Check(model.Store(), Equals, "brand-store") c.Check(model.Grade(), Equals, asserts.ModelGradeUnset) - allSnaps := model.AllSnaps() - c.Check(allSnaps, DeepEquals, []*asserts.ModelSnap{ + c.Check(model.StorageSafety(), Equals, asserts.StorageSafetyUnset) + essentialSnaps := model.EssentialSnaps() + c.Check(essentialSnaps, DeepEquals, []*asserts.ModelSnap{ model.KernelSnap(), model.BaseSnap(), model.GadgetSnap(), + }) + snaps := model.SnapsWithoutEssential() + c.Check(snaps, DeepEquals, []*asserts.ModelSnap{ { Name: "foo", Modes: []string{"run"}, @@ -195,14 +200,24 @@ }, }) // essential snaps included - reqSnaps := model.RequiredWithEssentialSnaps() - c.Check(reqSnaps, HasLen, len(allSnaps)) - for i, r := range reqSnaps { - c.Check(r.SnapName(), Equals, allSnaps[i].Name) - c.Check(r.ID(), Equals, "") + reqSnaps := naming.NewSnapSet(model.RequiredWithEssentialSnaps()) + for _, e := range essentialSnaps { + c.Check(reqSnaps.Contains(e), Equals, true) + } + for _, s := range snaps { + c.Check(reqSnaps.Contains(s), Equals, true) } + c.Check(reqSnaps.Size(), Equals, len(essentialSnaps)+len(snaps)) // essential snaps excluded - c.Check(model.RequiredNoEssentialSnaps(), DeepEquals, reqSnaps[3:]) + noEssential := naming.NewSnapSet(model.RequiredNoEssentialSnaps()) + for _, e := range essentialSnaps { + c.Check(noEssential.Contains(e), Equals, false) + } + for _, s := range snaps { + c.Check(noEssential.Contains(s), Equals, true) + } + c.Check(noEssential.Size(), Equals, len(snaps)) + c.Check(model.SystemUserAuthority(), HasLen, 0) c.Check(model.SerialAuthority(), DeepEquals, []string{"brand-id1", "generic"}) } @@ -536,9 +551,12 @@ c.Check(model.Base(), Equals, "") c.Check(model.BaseSnap(), IsNil) c.Check(model.Store(), Equals, "brand-store") - allSnaps := model.AllSnaps() - c.Check(allSnaps, DeepEquals, []*asserts.ModelSnap{ + essentialSnaps := model.EssentialSnaps() + c.Check(essentialSnaps, DeepEquals, []*asserts.ModelSnap{ model.GadgetSnap(), + }) + snaps := model.SnapsWithoutEssential() + c.Check(snaps, DeepEquals, []*asserts.ModelSnap{ { Name: "foo", Modes: []string{"run"}, @@ -551,14 +569,23 @@ }, }) // gadget included - reqSnaps := model.RequiredWithEssentialSnaps() - c.Check(reqSnaps, HasLen, len(allSnaps)) - for i, r := range reqSnaps { - c.Check(r.SnapName(), Equals, allSnaps[i].Name) - c.Check(r.ID(), Equals, "") + reqSnaps := naming.NewSnapSet(model.RequiredWithEssentialSnaps()) + for _, e := range essentialSnaps { + c.Check(reqSnaps.Contains(e), Equals, true) } + for _, s := range snaps { + c.Check(reqSnaps.Contains(s), Equals, true) + } + c.Check(reqSnaps.Size(), Equals, len(essentialSnaps)+len(snaps)) // gadget excluded - c.Check(model.RequiredNoEssentialSnaps(), DeepEquals, reqSnaps[1:]) + noEssential := naming.NewSnapSet(model.RequiredNoEssentialSnaps()) + for _, e := range essentialSnaps { + c.Check(noEssential.Contains(e), Equals, false) + } + for _, s := range snaps { + c.Check(noEssential.Contains(s), Equals, true) + } + c.Check(noEssential.Size(), Equals, len(snaps)) } func (mods *modelSuite) TestClassicDecodeInvalid(c *C) { @@ -640,11 +667,15 @@ }) c.Check(model.Store(), Equals, "brand-store") c.Check(model.Grade(), Equals, asserts.ModelSecured) - allSnaps := model.AllSnaps() - c.Check(allSnaps, DeepEquals, []*asserts.ModelSnap{ + c.Check(model.StorageSafety(), Equals, asserts.StorageSafetyEncrypted) + essentialSnaps := model.EssentialSnaps() + c.Check(essentialSnaps, DeepEquals, []*asserts.ModelSnap{ model.KernelSnap(), model.BaseSnap(), model.GadgetSnap(), + }) + snaps := model.SnapsWithoutEssential() + c.Check(snaps, DeepEquals, []*asserts.ModelSnap{ { Name: "other-base", SnapID: "otherbasedididididididididididid", @@ -679,14 +710,24 @@ }, }) // essential snaps included - reqSnaps := model.RequiredWithEssentialSnaps() - c.Check(reqSnaps, HasLen, len(allSnaps)-1) - for i, r := range reqSnaps { - c.Check(r.SnapName(), Equals, allSnaps[i].Name) - c.Check(r.ID(), Equals, allSnaps[i].SnapID) + reqSnaps := naming.NewSnapSet(model.RequiredWithEssentialSnaps()) + for _, e := range essentialSnaps { + c.Check(reqSnaps.Contains(e), Equals, true) + } + for _, s := range snaps { + c.Check(reqSnaps.Contains(s), Equals, s.Presence == "required") } + c.Check(reqSnaps.Size(), Equals, len(essentialSnaps)+len(snaps)-1) // essential snaps excluded - c.Check(model.RequiredNoEssentialSnaps(), DeepEquals, reqSnaps[3:]) + noEssential := naming.NewSnapSet(model.RequiredNoEssentialSnaps()) + for _, e := range essentialSnaps { + c.Check(noEssential.Contains(e), Equals, false) + } + for _, s := range snaps { + c.Check(noEssential.Contains(s), Equals, s.Presence == "required") + } + c.Check(noEssential.Size(), Equals, len(snaps)-1) + c.Check(model.SystemUserAuthority(), HasLen, 0) c.Check(model.SerialAuthority(), DeepEquals, []string{"brand-id1"}) } @@ -725,7 +766,7 @@ c.Assert(err, IsNil) c.Check(a.Type(), Equals, asserts.ModelType) model := a.(*asserts.Model) - snapdSnap := model.AllSnaps()[0] + snapdSnap := model.EssentialSnaps()[0] c.Check(snapdSnap, DeepEquals, &asserts.ModelSnap{ Name: "snapd", SnapID: "snapdidididididididididididididd", @@ -792,8 +833,8 @@ c.Check(a.Type(), Equals, asserts.ModelType) model := a.(*asserts.Model) c.Check(model.Grade(), Equals, asserts.ModelDangerous) - allSnaps := model.AllSnaps() - c.Check(allSnaps[len(allSnaps)-2], DeepEquals, &asserts.ModelSnap{ + snaps := model.SnapsWithoutEssential() + c.Check(snaps[len(snaps)-2], DeepEquals, &asserts.ModelSnap{ Name: "myapp", SnapType: "app", Modes: []string{"run"}, @@ -802,6 +843,55 @@ }) } +func (mods *modelSuite) TestCore20ValidStorageSafety(c *C) { + encoded := strings.Replace(core20ModelExample, "TSLINE", mods.tsLine, 1) + encoded = strings.Replace(encoded, "OTHER", "", 1) + encoded = strings.Replace(encoded, "grade: secured\n", "grade: signed\n", 1) + + for _, tc := range []struct { + ss asserts.StorageSafety + sss string + }{ + {asserts.StorageSafetyPreferEncrypted, "prefer-encrypted"}, + {asserts.StorageSafetyPreferUnencrypted, "prefer-unencrypted"}, + {asserts.StorageSafetyEncrypted, "encrypted"}, + } { + ex := strings.Replace(encoded, "storage-safety: encrypted\n", fmt.Sprintf("storage-safety: %s\n", tc.sss), 1) + a, err := asserts.Decode([]byte(ex)) + c.Assert(err, IsNil) + c.Check(a.Type(), Equals, asserts.ModelType) + model := a.(*asserts.Model) + c.Check(model.StorageSafety(), Equals, tc.ss) + } +} + +func (mods *modelSuite) TestCore20DefaultStorageSafetySecured(c *C) { + encoded := strings.Replace(core20ModelExample, "TSLINE", mods.tsLine, 1) + encoded = strings.Replace(encoded, "OTHER", "", 1) + ex := strings.Replace(encoded, "storage-safety: encrypted\n", "", 1) + + a, err := asserts.Decode([]byte(ex)) + c.Assert(err, IsNil) + c.Check(a.Type(), Equals, asserts.ModelType) + model := a.(*asserts.Model) + c.Check(model.StorageSafety(), Equals, asserts.StorageSafetyEncrypted) +} + +func (mods *modelSuite) TestCore20DefaultStorageSafetySignedDangerous(c *C) { + encoded := strings.Replace(core20ModelExample, "TSLINE", mods.tsLine, 1) + encoded = strings.Replace(encoded, "OTHER", "", 1) + encoded = strings.Replace(encoded, "storage-safety: encrypted\n", "", 1) + + for _, grade := range []string{"dangerous", "signed"} { + ex := strings.Replace(encoded, "grade: secured\n", fmt.Sprintf("grade: %s\n", grade), 1) + a, err := asserts.Decode([]byte(ex)) + c.Assert(err, IsNil) + c.Check(a.Type(), Equals, asserts.ModelType) + model := a.(*asserts.Model) + c.Check(model.StorageSafety(), Equals, asserts.StorageSafetyPreferEncrypted) + } +} + func (mods *modelSuite) TestCore20DecodeInvalid(c *C) { encoded := strings.Replace(core20ModelExample, "TSLINE", mods.tsLine, 1) @@ -840,6 +930,8 @@ {"OTHER", "gadget: foo\n", `cannot specify separate "gadget" header once using the extended snaps header`}, {"OTHER", "required-snaps:\n - foo\n", `cannot specify separate "required-snaps" header once using the extended snaps header`}, {"grade: secured\n", "grade: foo\n", `grade for model must be secured|signed|dangerous`}, + {"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`}, } for _, test := range invalidTests { invalid := strings.Replace(encoded, test.original, test.invalid, 1) diff -Nru snapd-2.45.1+20.04.2/asserts/pool.go snapd-2.48.3+20.04/asserts/pool.go --- snapd-2.45.1+20.04.2/asserts/pool.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48.3+20.04/asserts/pool.go 2021-02-02 08:21:12.000000000 +0000 @@ -0,0 +1,775 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2020 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 asserts + +import ( + "errors" + "fmt" + + "github.com/snapcore/snapd/asserts/internal" +) + +// A Grouping identifies opaquely a grouping of assertions. +// Pool uses it to label the interesection between a set of groups. +type Grouping string + +// A pool helps holding and tracking a set of assertions and their +// prerequisites as they need to be updated or resolved. The +// assertions can be organized in groups. Failure can be tracked +// isolated to groups, conversely any error related to a single group +// alone will stop any work to resolve it. Independent assertions +// should not be grouped. Assertions and prerequisites that are part +// of more than one group are tracked properly only once. +// +// Typical usage involves specifying the initial assertions needing to +// be resolved or updated using AddUnresolved and AddToUpdate. At this +// point ToResolve can be called to get them organized in groupings +// ready for fetching. Fetched assertions can then be provided with +// Add or AddBatch. Because these can have prerequisites calling +// ToResolve and fetching needs to be repeated until ToResolve's +// result is empty. Between any two ToResolve invocations but after +// any Add or AddBatch AddUnresolved/AddToUpdate can also be used +// again. +// +// V +// | +// /-> AddUnresolved, AddToUpdate +// | | +// | V +// |------> ToResolve -> empty? done +// | | +// | V +// \ __________ Add +// +// +// If errors prevent from fulfilling assertions from a ToResolve, +// AddError and AddGroupingError can be used to report the errors so +// that they can be associated with groups. +// +// All the resolved assertions in a Pool from groups not in error can +// be committed to a destination database with CommitTo. +type Pool struct { + groundDB RODatabase + + numbering map[string]uint16 + groupings *internal.Groupings + + unresolved map[string]*unresolvedRec + prerequisites map[string]*unresolvedRec + + bs Backstore + unchanged map[string]bool + + groups map[uint16]*groupRec + + curPhase poolPhase +} + +// NewPool creates a new Pool, groundDB is used to resolve trusted and +// predefined assertions and to provide the current revision for +// assertions to update and their prerequisites. Up to n groups can be +// used to organize the assertions. +func NewPool(groundDB RODatabase, n int) *Pool { + groupings, err := internal.NewGroupings(n) + if err != nil { + panic(fmt.Sprintf("NewPool: %v", err)) + } + return &Pool{ + groundDB: groundDB, + numbering: make(map[string]uint16), + groupings: groupings, + unresolved: make(map[string]*unresolvedRec), + prerequisites: make(map[string]*unresolvedRec), + bs: NewMemoryBackstore(), + unchanged: make(map[string]bool), + groups: make(map[uint16]*groupRec), + } +} + +func (p *Pool) groupNum(group string) (gnum uint16, err error) { + if gnum, ok := p.numbering[group]; ok { + return gnum, nil + } + gnum = uint16(len(p.numbering)) + if err = p.groupings.WithinRange(gnum); err != nil { + return 0, err + } + p.numbering[group] = gnum + return gnum, nil +} + +func (p *Pool) ensureGroup(group string) (gnum uint16, err error) { + gnum, err = p.groupNum(group) + if err != nil { + return 0, err + } + if gRec := p.groups[gnum]; gRec == nil { + p.groups[gnum] = &groupRec{ + name: group, + } + } + return gnum, nil +} + +// Singleton returns a grouping containing only the given group. +// It is useful mainly for tests and to drive Add are AddBatch when the +// server is pushing assertions (instead of the usual pull scenario). +func (p *Pool) Singleton(group string) (Grouping, error) { + gnum, err := p.ensureGroup(group) + if err != nil { + return Grouping(""), nil + } + + var grouping internal.Grouping + p.groupings.AddTo(&grouping, gnum) + return Grouping(p.groupings.Serialize(&grouping)), nil +} + +// An unresolvedRec tracks a single unresolved assertion until it is +// resolved or there is an error doing so. The field 'grouping' will +// grow to contain all the groups requiring this assertion while it +// is unresolved. +type unresolvedRec struct { + at *AtRevision + grouping internal.Grouping + + serializedLabel Grouping + + err error +} + +func (u *unresolvedRec) exportTo(r map[Grouping][]*AtRevision, gr *internal.Groupings) { + serLabel := Grouping(gr.Serialize(&u.grouping)) + // remember serialized label + u.serializedLabel = serLabel + r[serLabel] = append(r[serLabel], u.at) +} + +func (u *unresolvedRec) merge(at *AtRevision, gnum uint16, gr *internal.Groupings) { + gr.AddTo(&u.grouping, gnum) + // assume we want to resolve/update wrt the highest revision + if at.Revision > u.at.Revision { + u.at.Revision = at.Revision + } +} + +// A groupRec keeps track of all the resolved assertions in a group +// or whether the group should be considered in error (err != nil). +type groupRec struct { + name string + err error + resolved []Ref +} + +func (gRec *groupRec) hasErr() bool { + return gRec.err != nil +} + +func (gRec *groupRec) setErr(e error) { + if gRec.err == nil { + gRec.err = e + } +} + +func (gRec *groupRec) markResolved(ref *Ref) (marked bool) { + if gRec.hasErr() { + return false + } + gRec.resolved = append(gRec.resolved, *ref) + return true +} + +// markResolved marks the assertion referenced by ref as resolved +// in all the groups in grouping, except those already in error. +func (p *Pool) markResolved(grouping *internal.Grouping, resolved *Ref) (marked bool) { + p.groupings.Iter(grouping, func(gnum uint16) error { + if p.groups[gnum].markResolved(resolved) { + marked = true + } + return nil + }) + return marked +} + +// setErr marks all the groups in grouping as in error with error err +// except those already in error. +func (p *Pool) setErr(grouping *internal.Grouping, err error) { + p.groupings.Iter(grouping, func(gnum uint16) error { + p.groups[gnum].setErr(err) + return nil + }) +} + +func (p *Pool) isPredefined(ref *Ref) (bool, error) { + _, err := ref.Resolve(p.groundDB.FindPredefined) + if err == nil { + return true, nil + } + if !IsNotFound(err) { + return false, err + } + return false, nil +} + +func (p *Pool) isResolved(ref *Ref) (bool, error) { + if p.unchanged[ref.Unique()] { + return true, nil + } + _, err := p.bs.Get(ref.Type, ref.PrimaryKey, ref.Type.MaxSupportedFormat()) + if err == nil { + return true, nil + } + if !IsNotFound(err) { + return false, err + } + return false, nil +} + +func (p *Pool) curRevision(ref *Ref) (int, error) { + a, err := ref.Resolve(p.groundDB.Find) + if err != nil && !IsNotFound(err) { + return 0, err + } + if err == nil { + return a.Revision(), nil + } + return RevisionNotKnown, nil +} + +type poolPhase int + +const ( + poolPhaseAddUnresolved = iota + poolPhaseAdd +) + +func (p *Pool) phase(ph poolPhase) error { + if ph == p.curPhase { + return nil + } + if ph == poolPhaseAdd { + return fmt.Errorf("internal error: cannot switch to Pool add phase without invoking ToResolve first") + } + // ph == poolPhaseAddUnresolved + p.unresolvedBookkeeping() + p.curPhase = poolPhaseAddUnresolved + return nil +} + +// AddUnresolved adds the assertion referenced by unresolved +// AtRevision to the Pool as unresolved and as required by the given group. +// Usually unresolved.Revision will have been set to RevisionNotKnown. +func (p *Pool) AddUnresolved(unresolved *AtRevision, group string) error { + if err := p.phase(poolPhaseAddUnresolved); err != nil { + return err + } + gnum, err := p.ensureGroup(group) + if err != nil { + return err + } + u := *unresolved + ok, err := p.isPredefined(&u.Ref) + if err != nil { + return err + } + if ok { + // predefined, nothing to do + return nil + } + return p.addUnresolved(&u, gnum) +} + +func (p *Pool) addUnresolved(unresolved *AtRevision, gnum uint16) error { + ok, err := p.isResolved(&unresolved.Ref) + if err != nil { + return err + } + if ok { + // We assume that either the resolving of + // prerequisites for the already resolved assertion in + // progress has succeeded or will. If that's not the + // case we will fail at CommitTo time. We could + // instead recurse into its prerequisites again but the + // complexity isn't clearly worth it. + // See TestParallelPartialResolutionFailure + // Mark this as resolved in the group. + p.groups[gnum].markResolved(&unresolved.Ref) + return nil + } + uniq := unresolved.Ref.Unique() + var u *unresolvedRec + if u = p.unresolved[uniq]; u == nil { + u = &unresolvedRec{ + at: unresolved, + } + p.unresolved[uniq] = u + } + u.merge(unresolved, gnum, p.groupings) + return nil +} + +// ToResolve returns all the currently unresolved assertions in the +// Pool, organized in opaque groupings based on which set of groups +// requires each of them. +// At the next ToResolve any unresolved assertion with not known +// revision that was not added via Add or AddBatch will result in all +// groups requiring it being in error with ErrUnresolved. +// Conversely, the remaining unresolved assertions originally added +// via AddToUpdate will be assumed to still be at their current +// revisions. +func (p *Pool) ToResolve() (map[Grouping][]*AtRevision, error) { + if p.curPhase == poolPhaseAdd { + p.unresolvedBookkeeping() + } else { + p.curPhase = poolPhaseAdd + } + r := make(map[Grouping][]*AtRevision) + for _, u := range p.unresolved { + if u.at.Revision == RevisionNotKnown { + rev, err := p.curRevision(&u.at.Ref) + if err != nil { + return nil, err + } + if rev != RevisionNotKnown { + u.at.Revision = rev + } + } + u.exportTo(r, p.groupings) + } + return r, nil +} + +func (p *Pool) addPrerequisite(pref *Ref, g *internal.Grouping) error { + uniq := pref.Unique() + u := p.unresolved[uniq] + at := &AtRevision{ + Ref: *pref, + Revision: RevisionNotKnown, + } + if u == nil { + u = p.prerequisites[uniq] + } + if u != nil { + gr := p.groupings + gr.Iter(g, func(gnum uint16) error { + u.merge(at, gnum, gr) + return nil + }) + return nil + } + ok, err := p.isPredefined(pref) + if err != nil { + return err + } + if ok { + // nothing to do + return nil + } + ok, err = p.isResolved(pref) + if err != nil { + return err + } + if ok { + // nothing to do, it is anyway implied + return nil + } + p.prerequisites[uniq] = &unresolvedRec{ + at: at, + grouping: g.Copy(), + } + return nil +} + +func (p *Pool) add(a Assertion, g *internal.Grouping) error { + if err := p.bs.Put(a.Type(), a); err != nil { + if revErr, ok := err.(*RevisionError); ok { + if revErr.Current >= a.Revision() { + // we already got something more recent + return nil + } + } + + return err + } + for _, pref := range a.Prerequisites() { + if err := p.addPrerequisite(pref, g); err != nil { + return err + } + } + keyRef := &Ref{ + Type: AccountKeyType, + PrimaryKey: []string{a.SignKeyID()}, + } + if err := p.addPrerequisite(keyRef, g); err != nil { + return err + } + return nil +} + +func (p *Pool) resolveWith(unresolved map[string]*unresolvedRec, uniq string, u *unresolvedRec, a Assertion, extrag *internal.Grouping) (ok bool, err error) { + if a.Revision() > u.at.Revision { + if extrag == nil { + extrag = &u.grouping + } else { + p.groupings.Iter(&u.grouping, func(gnum uint16) error { + p.groupings.AddTo(extrag, gnum) + return nil + }) + } + ref := a.Ref() + if p.markResolved(extrag, ref) { + // remove from tracking - + // remove u from unresolved only if the assertion + // is added to the resolved backstore; + // otherwise it might resurface as unresolved; + // it will be ultimately handled in + // unresolvedBookkeeping if it stays around + delete(unresolved, uniq) + if err := p.add(a, extrag); err != nil { + p.setErr(extrag, err) + return false, err + } + } + } + return true, nil +} + +// Add adds the given assertion associated with the given grouping to the +// Pool as resolved in all the groups requiring it. +// Any not already resolved prerequisites of the assertion will +// be implicitly added as unresolved and required by all of those groups. +// The grouping will usually have been associated with the assertion +// in a ToResolve's result. Otherwise the union of all groups +// requiring the assertion plus the groups in grouping will be considered. +// The latter is mostly relevant in scenarios where the server is pushing +// assertions. +// If an error is returned it refers to an immediate or local error. +// Errors related to the assertions are associated with the relevant groups +// and can be retrieved with Err, in which case ok is set to false. +func (p *Pool) Add(a Assertion, grouping Grouping) (ok bool, err error) { + if err := p.phase(poolPhaseAdd); err != nil { + return false, err + } + + if !a.SupportedFormat() { + e := &UnsupportedFormatError{Ref: a.Ref(), Format: a.Format()} + p.AddGroupingError(e, grouping) + return false, nil + } + + return p.addToGrouping(a, grouping, p.groupings.Deserialize) +} + +func (p *Pool) addToGrouping(a Assertion, grouping Grouping, deserializeGrouping func(string) (*internal.Grouping, error)) (ok bool, err error) { + uniq := a.Ref().Unique() + var u *unresolvedRec + var extrag *internal.Grouping + var unresolved map[string]*unresolvedRec + if u = p.unresolved[uniq]; u != nil { + unresolved = p.unresolved + } else if u = p.prerequisites[uniq]; u != nil { + unresolved = p.prerequisites + } else { + ok, err := p.isPredefined(a.Ref()) + if err != nil { + return false, err + } + if ok { + // nothing to do + return true, nil + } + // a is not tracked as unresolved in any way so far, + // this is an atypical scenario where something gets + // pushed but we still want to add it to the resolved + // lists of the relevant groups; in case it is + // actually already resolved most of resolveWith below will + // be a nop + u = &unresolvedRec{ + at: a.At(), + } + u.at.Revision = RevisionNotKnown + } + + if u.serializedLabel != grouping { + var err error + extrag, err = deserializeGrouping(string(grouping)) + if err != nil { + return false, err + } + } + + return p.resolveWith(unresolved, uniq, u, a, extrag) +} + +// AddBatch adds all the assertions in the Batch to the Pool, +// associated with the given grouping and as resolved in all the +// groups requiring them. It is equivalent to using Add on each of them. +// If an error is returned it refers to an immediate or local error. +// Errors related to the assertions are associated with the relevant groups +// and can be retrieved with Err, in which case ok set to false. +func (p *Pool) AddBatch(b *Batch, grouping Grouping) (ok bool, err error) { + if err := p.phase(poolPhaseAdd); err != nil { + return false, err + } + + // b dealt with unsupported formats already + + // deserialize grouping if needed only once + var cachedGrouping *internal.Grouping + deser := func(_ string) (*internal.Grouping, error) { + if cachedGrouping != nil { + // do a copy as addToGrouping and resolveWith + // might add to their input + g := cachedGrouping.Copy() + return &g, nil + } + var err error + cachedGrouping, err = p.groupings.Deserialize(string(grouping)) + return cachedGrouping, err + } + + inError := false + for _, a := range b.added { + ok, err := p.addToGrouping(a, grouping, deser) + if err != nil { + return false, err + } + if !ok { + inError = true + } + } + + return !inError, nil +} + +var ( + ErrUnresolved = errors.New("unresolved assertion") + ErrUnknownPoolGroup = errors.New("unknown pool group") +) + +// unresolvedBookkeeping processes any left over unresolved assertions +// since the last ToResolve invocation and intervening calls to Add/AddBatch, +// * they were either marked as in error which will be propagated +// to all groups requiring them +// * simply unresolved, which will be propagated to groups requiring them +// as ErrUnresolved +// * unchanged (update case) +// unresolvedBookkeeping will also promote any recorded prerequisites +// into actively unresolved, as long as not all the groups requiring them +// are in error. +func (p *Pool) unresolvedBookkeeping() { + // any left over unresolved are either: + // * in error + // * unchanged + // * or unresolved + for uniq, u := range p.unresolved { + e := u.err + if e == nil { + if u.at.Revision == RevisionNotKnown { + e = ErrUnresolved + } else { + // unchanged + p.unchanged[uniq] = true + } + } + if e != nil { + p.setErr(&u.grouping, e) + } + delete(p.unresolved, uniq) + } + + // prerequisites will become the new unresolved but drop them + // if all their groups are in error + for uniq, prereq := range p.prerequisites { + useful := false + p.groupings.Iter(&prereq.grouping, func(gnum uint16) error { + if !p.groups[gnum].hasErr() { + useful = true + } + return nil + }) + if !useful { + delete(p.prerequisites, uniq) + continue + } + } + + // prerequisites become the new unresolved, the emptied + // unresolved is used for prerequisites in the next round + p.unresolved, p.prerequisites = p.prerequisites, p.unresolved +} + +// Err returns the error for group if group is in error, nil otherwise. +func (p *Pool) Err(group string) error { + gnum, err := p.groupNum(group) + if err != nil { + return err + } + gRec := p.groups[gnum] + if gRec == nil { + return ErrUnknownPoolGroup + } + return gRec.err +} + +// Errors returns a mapping of groups in error to their errors. +func (p *Pool) Errors() map[string]error { + res := make(map[string]error) + for _, gRec := range p.groups { + if err := gRec.err; err != nil { + res[gRec.name] = err + } + } + if len(res) == 0 { + return nil + } + return res +} + +// AddError associates error e with the unresolved assertion. +// The error will be propagated to all the affected groups at +// the next ToResolve. +func (p *Pool) AddError(e error, ref *Ref) error { + if err := p.phase(poolPhaseAdd); err != nil { + return err + } + uniq := ref.Unique() + if u := p.unresolved[uniq]; u != nil && u.err == nil { + u.err = e + } + return nil +} + +// AddGroupingError puts all the groups of grouping in error, with error e. +func (p *Pool) AddGroupingError(e error, grouping Grouping) error { + if err := p.phase(poolPhaseAdd); err != nil { + return err + } + + g, err := p.groupings.Deserialize(string(grouping)) + if err != nil { + return err + } + + p.setErr(g, e) + return nil +} + +// AddToUpdate adds the assertion referenced by toUpdate and all its +// prerequisites to the Pool as unresolved and as required by the +// given group. It is assumed that the assertion is currently in the +// ground database of the Pool, otherwise this will error. +// The current revisions of the assertion and its prerequisites will +// be recorded and only higher revisions will then resolve them, +// otherwise if ultimately unresolved they will be assumed to still be +// at their current ones. +func (p *Pool) AddToUpdate(toUpdate *Ref, group string) error { + if err := p.phase(poolPhaseAddUnresolved); err != nil { + return err + } + gnum, err := p.ensureGroup(group) + if err != nil { + return err + } + retrieve := func(ref *Ref) (Assertion, error) { + return ref.Resolve(p.groundDB.Find) + } + add := func(a Assertion) error { + return p.addUnresolved(a.At(), gnum) + } + f := NewFetcher(p.groundDB, retrieve, add) + if err := f.Fetch(toUpdate); err != nil { + return err + } + return nil +} + +// CommitTo adds the assertions from groups without errors to the +// given assertion database. Commit errors can be retrieved via Err +// per group. An error is returned directly only if CommitTo is called +// with possible pending unresolved assertions. +func (p *Pool) CommitTo(db *Database) error { + if p.curPhase == poolPhaseAddUnresolved { + return fmt.Errorf("internal error: cannot commit Pool during add unresolved phase") + } + p.unresolvedBookkeeping() + + retrieve := func(ref *Ref) (Assertion, error) { + a, err := p.bs.Get(ref.Type, ref.PrimaryKey, ref.Type.MaxSupportedFormat()) + if IsNotFound(err) { + // fallback to pre-existing assertions + a, err = ref.Resolve(db.Find) + } + if err != nil { + return nil, resolveError("cannot resolve prerequisite assertion: %s", ref, err) + } + return a, nil + } + save := func(a Assertion) error { + err := db.Add(a) + if IsUnaccceptedUpdate(err) { + // unsupported format case is handled before. + // be idempotent, db has already the same or + // newer. + return nil + } + return err + } + +NextGroup: + for _, gRec := range p.groups { + if gRec.hasErr() { + // already in error, ignore + continue + } + // TODO: try to reuse fetcher + f := NewFetcher(db, retrieve, save) + for i := range gRec.resolved { + if err := f.Fetch(&gRec.resolved[i]); err != nil { + gRec.setErr(err) + continue NextGroup + } + } + } + + return nil +} + +// ClearGroups clears the pool in terms of information associated with groups +// while preserving information about already resolved or unchanged assertions. +// It is useful for reusing a pool once the maximum of usable groups +// that was set with NewPool has been exhausted. Group errors must be +// queried before calling it otherwise they are lost. It is an error +// to call it when there are still pending unresolved assertions in +// the pool. +func (p *Pool) ClearGroups() error { + if len(p.unresolved) != 0 || len(p.prerequisites) != 0 { + return fmt.Errorf("internal error: trying to clear groups of asserts.Pool with pending unresolved or prerequisites") + } + + p.numbering = make(map[string]uint16) + // use a fresh Groupings as well so that max group tracking starts + // from scratch. + // NewGroupings cannot fail on a value accepted by it previously + p.groupings, _ = internal.NewGroupings(p.groupings.N()) + p.groups = make(map[uint16]*groupRec) + p.curPhase = poolPhaseAdd + return nil +} diff -Nru snapd-2.45.1+20.04.2/asserts/pool_test.go snapd-2.48.3+20.04/asserts/pool_test.go --- snapd-2.45.1+20.04.2/asserts/pool_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48.3+20.04/asserts/pool_test.go 2021-02-02 08:21:12.000000000 +0000 @@ -0,0 +1,1019 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2020 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 asserts_test + +import ( + "errors" + "sort" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/asserts/assertstest" + "github.com/snapcore/snapd/testutil" +) + +type poolSuite struct { + testutil.BaseTest + + hub *assertstest.StoreStack + dev1Acct *asserts.Account + dev2Acct *asserts.Account + + decl1 *asserts.TestOnlyDecl + decl1_1 *asserts.TestOnlyDecl + rev1_1111 *asserts.TestOnlyRev + rev1_3333 *asserts.TestOnlyRev + + decl2 *asserts.TestOnlyDecl + rev2_2222 *asserts.TestOnlyRev + + db *asserts.Database +} + +var _ = Suite(&poolSuite{}) + +func (s *poolSuite) SetUpTest(c *C) { + s.BaseTest.SetUpTest(c) + + s.hub = assertstest.NewStoreStack("hub", nil) + s.dev1Acct = assertstest.NewAccount(s.hub, "developer1", map[string]interface{}{ + "account-id": "developer1", + }, "") + s.dev2Acct = assertstest.NewAccount(s.hub, "developer2", map[string]interface{}{ + "account-id": "developer2", + }, "") + + a, err := s.hub.Sign(asserts.TestOnlyDeclType, map[string]interface{}{ + "id": "one", + "dev-id": "developer1", + }, nil, "") + c.Assert(err, IsNil) + s.decl1 = a.(*asserts.TestOnlyDecl) + + a, err = s.hub.Sign(asserts.TestOnlyDeclType, map[string]interface{}{ + "id": "one", + "dev-id": "developer1", + "revision": "1", + }, nil, "") + c.Assert(err, IsNil) + s.decl1_1 = a.(*asserts.TestOnlyDecl) + + a, err = s.hub.Sign(asserts.TestOnlyDeclType, map[string]interface{}{ + "id": "two", + "dev-id": "developer2", + }, nil, "") + c.Assert(err, IsNil) + s.decl2 = a.(*asserts.TestOnlyDecl) + + a, err = s.hub.Sign(asserts.TestOnlyRevType, map[string]interface{}{ + "h": "1111", + "id": "one", + "dev-id": "developer1", + }, nil, "") + c.Assert(err, IsNil) + s.rev1_1111 = a.(*asserts.TestOnlyRev) + + a, err = s.hub.Sign(asserts.TestOnlyRevType, map[string]interface{}{ + "h": "3333", + "id": "one", + "dev-id": "developer1", + }, nil, "") + c.Assert(err, IsNil) + s.rev1_3333 = a.(*asserts.TestOnlyRev) + + a, err = s.hub.Sign(asserts.TestOnlyRevType, map[string]interface{}{ + "h": "2222", + "id": "two", + "dev-id": "developer2", + }, nil, "") + c.Assert(err, IsNil) + s.rev2_2222 = a.(*asserts.TestOnlyRev) + + db, err := asserts.OpenDatabase(&asserts.DatabaseConfig{ + Backstore: asserts.NewMemoryBackstore(), + Trusted: s.hub.Trusted, + }) + c.Assert(err, IsNil) + s.db = db +} + +func (s *poolSuite) TestAddUnresolved(c *C) { + pool := asserts.NewPool(s.db, 64) + + at1 := &asserts.AtRevision{ + Ref: asserts.Ref{Type: asserts.TestOnlyRevType, PrimaryKey: []string{"1111"}}, + Revision: asserts.RevisionNotKnown, + } + err := pool.AddUnresolved(at1, "for_one") // group num: 0 + c.Assert(err, IsNil) + + toResolve, err := pool.ToResolve() + c.Assert(err, IsNil) + c.Check(toResolve, DeepEquals, map[asserts.Grouping][]*asserts.AtRevision{ + asserts.MakePoolGrouping(0): {at1}, + }) +} + +func (s *poolSuite) TestAddUnresolvedPredefined(c *C) { + pool := asserts.NewPool(s.db, 64) + + at := s.hub.TrustedAccount.At() + at.Revision = asserts.RevisionNotKnown + err := pool.AddUnresolved(at, "for_one") + c.Assert(err, IsNil) + + // nothing to resolve + toResolve, err := pool.ToResolve() + c.Assert(err, IsNil) + c.Check(toResolve, HasLen, 0) +} + +func (s *poolSuite) TestAddUnresolvedGrouping(c *C) { + pool := asserts.NewPool(s.db, 64) + + storeKeyAt := s.hub.StoreAccountKey("").At() + + pool.AddUnresolved(storeKeyAt, "for_two") // group num: 0 + pool.AddUnresolved(storeKeyAt, "for_one") // group num: 1 + + toResolve, err := pool.ToResolve() + c.Assert(err, IsNil) + c.Check(toResolve, DeepEquals, map[asserts.Grouping][]*asserts.AtRevision{ + asserts.MakePoolGrouping(0, 1): {storeKeyAt}, + }) +} + +func (s *poolSuite) TestAddUnresolvedDup(c *C) { + pool := asserts.NewPool(s.db, 64) + + storeKeyAt := s.hub.StoreAccountKey("").At() + + pool.AddUnresolved(storeKeyAt, "for_one") // group num: 0 + pool.AddUnresolved(storeKeyAt, "for_one") // group num: 0 + + toResolve, err := pool.ToResolve() + c.Assert(err, IsNil) + c.Check(toResolve, DeepEquals, map[asserts.Grouping][]*asserts.AtRevision{ + asserts.MakePoolGrouping(0): {storeKeyAt}, + }) +} + +type byAtRevision []*asserts.AtRevision + +func (ats byAtRevision) Len() int { + return len(ats) +} + +func (ats byAtRevision) Less(i, j int) bool { + return ats[i].Ref.Unique() < ats[j].Ref.Unique() +} + +func (ats byAtRevision) Swap(i, j int) { + ats[i], ats[j] = ats[j], ats[i] +} + +func sortToResolve(toResolve map[asserts.Grouping][]*asserts.AtRevision) { + for _, ats := range toResolve { + sort.Sort(byAtRevision(ats)) + } +} + +func (s *poolSuite) TestFetch(c *C) { + pool := asserts.NewPool(s.db, 64) + + at1111 := &asserts.AtRevision{ + Ref: asserts.Ref{Type: asserts.TestOnlyRevType, PrimaryKey: []string{"1111"}}, + Revision: asserts.RevisionNotKnown, + } + err := pool.AddUnresolved(at1111, "for_one") + c.Assert(err, IsNil) + + toResolve, err := pool.ToResolve() + c.Assert(err, IsNil) + c.Check(toResolve, DeepEquals, map[asserts.Grouping][]*asserts.AtRevision{ + asserts.MakePoolGrouping(0): {at1111}, + }) + + ok, err := pool.Add(s.rev1_1111, asserts.MakePoolGrouping(0)) + c.Assert(err, IsNil) + c.Assert(ok, Equals, true) + + toResolve, err = pool.ToResolve() + c.Assert(err, IsNil) + sortToResolve(toResolve) + dev1AcctAt := s.dev1Acct.At() + dev1AcctAt.Revision = asserts.RevisionNotKnown + decl1At := s.decl1.At() + decl1At.Revision = asserts.RevisionNotKnown + storeKeyAt := s.hub.StoreAccountKey("").At() + storeKeyAt.Revision = asserts.RevisionNotKnown + c.Check(toResolve, DeepEquals, map[asserts.Grouping][]*asserts.AtRevision{ + asserts.MakePoolGrouping(0): {storeKeyAt, dev1AcctAt, decl1At}, + }) + + c.Check(pool.Err("for_one"), IsNil) +} + +func (s *poolSuite) TestCompleteFetch(c *C) { + pool := asserts.NewPool(s.db, 64) + + at1111 := &asserts.AtRevision{ + Ref: asserts.Ref{Type: asserts.TestOnlyRevType, PrimaryKey: []string{"1111"}}, + Revision: asserts.RevisionNotKnown, + } + err := pool.AddUnresolved(at1111, "for_one") + c.Assert(err, IsNil) + + toResolve, err := pool.ToResolve() + c.Assert(err, IsNil) + c.Check(toResolve, DeepEquals, map[asserts.Grouping][]*asserts.AtRevision{ + asserts.MakePoolGrouping(0): {at1111}, + }) + + ok, err := pool.Add(s.rev1_1111, asserts.MakePoolGrouping(0)) + c.Assert(err, IsNil) + c.Assert(ok, Equals, true) + + toResolve, err = pool.ToResolve() + c.Assert(err, IsNil) + sortToResolve(toResolve) + dev1AcctAt := s.dev1Acct.At() + dev1AcctAt.Revision = asserts.RevisionNotKnown + decl1At := s.decl1.At() + decl1At.Revision = asserts.RevisionNotKnown + storeKey := s.hub.StoreAccountKey("") + storeKeyAt := storeKey.At() + storeKeyAt.Revision = asserts.RevisionNotKnown + c.Check(toResolve, DeepEquals, map[asserts.Grouping][]*asserts.AtRevision{ + asserts.MakePoolGrouping(0): {storeKeyAt, dev1AcctAt, decl1At}, + }) + + b := asserts.NewBatch(nil) + err = b.Add(s.decl1) + c.Assert(err, IsNil) + err = b.Add(storeKey) + c.Assert(err, IsNil) + err = b.Add(s.dev1Acct) + c.Assert(err, IsNil) + + ok, err = pool.AddBatch(b, asserts.MakePoolGrouping(0)) + c.Assert(err, IsNil) + c.Assert(ok, Equals, true) + + toResolve, err = pool.ToResolve() + c.Assert(err, IsNil) + c.Check(toResolve, HasLen, 0) + + c.Check(pool.Err("for_one"), IsNil) + + err = pool.CommitTo(s.db) + c.Check(err, IsNil) + c.Assert(pool.Err("for_one"), IsNil) + + a, err := at1111.Ref.Resolve(s.db.Find) + c.Assert(err, IsNil) + c.Check(a.(*asserts.TestOnlyRev).H(), Equals, "1111") +} + +func (s *poolSuite) TestPushSuggestionForPrerequisite(c *C) { + assertstest.AddMany(s.db, s.hub.StoreAccountKey("")) + + pool := asserts.NewPool(s.db, 64) + + at1111 := &asserts.AtRevision{ + Ref: asserts.Ref{Type: asserts.TestOnlyRevType, PrimaryKey: []string{"1111"}}, + Revision: asserts.RevisionNotKnown, + } + err := pool.AddUnresolved(at1111, "for_one") + c.Assert(err, IsNil) + + toResolve, err := pool.ToResolve() + c.Assert(err, IsNil) + c.Check(toResolve, DeepEquals, map[asserts.Grouping][]*asserts.AtRevision{ + asserts.MakePoolGrouping(0): {at1111}, + }) + + ok, err := pool.Add(s.rev1_1111, asserts.MakePoolGrouping(0)) + c.Assert(err, IsNil) + c.Assert(ok, Equals, true) + + // push prerequisite suggestion + ok, err = pool.Add(s.decl1, asserts.MakePoolGrouping(0)) + c.Assert(err, IsNil) + c.Assert(ok, Equals, true) + + toResolve, err = pool.ToResolve() + c.Assert(err, IsNil) + sortToResolve(toResolve) + dev1AcctAt := s.dev1Acct.At() + dev1AcctAt.Revision = asserts.RevisionNotKnown + storeKey := s.hub.StoreAccountKey("") + c.Check(toResolve, DeepEquals, map[asserts.Grouping][]*asserts.AtRevision{ + asserts.MakePoolGrouping(0): {storeKey.At(), dev1AcctAt}, + }) + + c.Check(pool.Err("for_one"), IsNil) + + ok, err = pool.Add(s.dev1Acct, asserts.MakePoolGrouping(0)) + c.Assert(err, IsNil) + c.Assert(ok, Equals, true) + + toResolve, err = pool.ToResolve() + c.Assert(err, IsNil) + c.Check(toResolve, HasLen, 0) + + c.Check(pool.Err("for_one"), IsNil) + + err = pool.CommitTo(s.db) + c.Check(err, IsNil) + c.Assert(pool.Err("for_one"), IsNil) + + a, err := at1111.Ref.Resolve(s.db.Find) + c.Assert(err, IsNil) + c.Check(a.(*asserts.TestOnlyRev).H(), Equals, "1111") +} + +func (s *poolSuite) TestPushSuggestionForNew(c *C) { + assertstest.AddMany(s.db, s.hub.StoreAccountKey("")) + + pool := asserts.NewPool(s.db, 64) + + atOne := &asserts.AtRevision{ + Ref: asserts.Ref{Type: asserts.TestOnlyDeclType, PrimaryKey: []string{"one"}}, + Revision: asserts.RevisionNotKnown, + } + err := pool.AddUnresolved(atOne, "for_one") + c.Assert(err, IsNil) + + toResolve, err := pool.ToResolve() + c.Assert(err, IsNil) + c.Check(toResolve, DeepEquals, map[asserts.Grouping][]*asserts.AtRevision{ + asserts.MakePoolGrouping(0): {atOne}, + }) + + ok, err := pool.Add(s.decl1, asserts.MakePoolGrouping(0)) + c.Assert(err, IsNil) + c.Assert(ok, Equals, true) + + // new push suggestion + ok, err = pool.Add(s.rev1_1111, asserts.MakePoolGrouping(0)) + c.Assert(err, IsNil) + c.Assert(ok, Equals, true) + + toResolve, err = pool.ToResolve() + c.Assert(err, IsNil) + sortToResolve(toResolve) + dev1AcctAt := s.dev1Acct.At() + dev1AcctAt.Revision = asserts.RevisionNotKnown + storeKeyAt := s.hub.StoreAccountKey("").At() + c.Check(toResolve, DeepEquals, map[asserts.Grouping][]*asserts.AtRevision{ + asserts.MakePoolGrouping(0): {storeKeyAt, dev1AcctAt}, + }) + + c.Check(pool.Err("for_one"), IsNil) + + ok, err = pool.Add(s.dev1Acct, asserts.MakePoolGrouping(0)) + c.Assert(err, IsNil) + c.Assert(ok, Equals, true) + + toResolve, err = pool.ToResolve() + c.Assert(err, IsNil) + c.Check(toResolve, HasLen, 0) + + c.Check(pool.Err("for_one"), IsNil) + + err = pool.CommitTo(s.db) + c.Check(err, IsNil) + c.Assert(pool.Err("for_one"), IsNil) + + a, err := s.rev1_1111.Ref().Resolve(s.db.Find) + c.Assert(err, IsNil) + c.Check(a.(*asserts.TestOnlyRev).H(), Equals, "1111") +} + +func (s *poolSuite) TestPushSuggestionForNewViaBatch(c *C) { + assertstest.AddMany(s.db, s.hub.StoreAccountKey("")) + + pool := asserts.NewPool(s.db, 64) + + atOne := &asserts.AtRevision{ + Ref: asserts.Ref{Type: asserts.TestOnlyDeclType, PrimaryKey: []string{"one"}}, + Revision: asserts.RevisionNotKnown, + } + err := pool.AddUnresolved(atOne, "for_one") + c.Assert(err, IsNil) + + toResolve, err := pool.ToResolve() + c.Assert(err, IsNil) + c.Check(toResolve, DeepEquals, map[asserts.Grouping][]*asserts.AtRevision{ + asserts.MakePoolGrouping(0): {atOne}, + }) + + b := asserts.NewBatch(nil) + err = b.Add(s.decl1) + c.Assert(err, IsNil) + + // new push suggestions + err = b.Add(s.rev1_1111) + c.Assert(err, IsNil) + err = b.Add(s.rev1_3333) + c.Assert(err, IsNil) + + ok, err := pool.AddBatch(b, asserts.MakePoolGrouping(0)) + c.Assert(err, IsNil) + c.Assert(ok, Equals, true) + + toResolve, err = pool.ToResolve() + c.Assert(err, IsNil) + sortToResolve(toResolve) + dev1AcctAt := s.dev1Acct.At() + dev1AcctAt.Revision = asserts.RevisionNotKnown + storeKeyAt := s.hub.StoreAccountKey("").At() + c.Check(toResolve, DeepEquals, map[asserts.Grouping][]*asserts.AtRevision{ + asserts.MakePoolGrouping(0): {storeKeyAt, dev1AcctAt}, + }) + + c.Check(pool.Err("for_one"), IsNil) + + ok, err = pool.Add(s.dev1Acct, asserts.MakePoolGrouping(0)) + c.Assert(err, IsNil) + c.Assert(ok, Equals, true) + + toResolve, err = pool.ToResolve() + c.Assert(err, IsNil) + c.Check(toResolve, HasLen, 0) + + c.Check(pool.Err("for_one"), IsNil) + + err = pool.CommitTo(s.db) + c.Check(err, IsNil) + c.Assert(pool.Err("for_one"), IsNil) + + a, err := s.rev1_1111.Ref().Resolve(s.db.Find) + c.Assert(err, IsNil) + c.Check(a.(*asserts.TestOnlyRev).H(), Equals, "1111") + + a, err = s.rev1_3333.Ref().Resolve(s.db.Find) + c.Assert(err, IsNil) + c.Check(a.(*asserts.TestOnlyRev).H(), Equals, "3333") +} + +func (s *poolSuite) TestAddUnresolvedUnresolved(c *C) { + pool := asserts.NewPool(s.db, 64) + + at1 := &asserts.AtRevision{ + Ref: asserts.Ref{Type: asserts.TestOnlyRevType, PrimaryKey: []string{"1111"}}, + Revision: asserts.RevisionNotKnown, + } + err := pool.AddUnresolved(at1, "for_one") + c.Assert(err, IsNil) + + toResolve, err := pool.ToResolve() + c.Assert(err, IsNil) + c.Check(toResolve, DeepEquals, map[asserts.Grouping][]*asserts.AtRevision{ + asserts.MakePoolGrouping(0): {at1}, + }) + + toResolve, err = pool.ToResolve() + c.Assert(err, IsNil) + c.Check(toResolve, HasLen, 0) + + c.Check(pool.Err("for_one"), Equals, asserts.ErrUnresolved) +} + +func (s *poolSuite) TestAddFormatTooNew(c *C) { + pool := asserts.NewPool(s.db, 64) + + _, err := pool.ToResolve() + c.Assert(err, IsNil) + + var a asserts.Assertion + (func() { + restore := asserts.MockMaxSupportedFormat(asserts.TestOnlyDeclType, 2) + defer restore() + + a, err = s.hub.Sign(asserts.TestOnlyDeclType, map[string]interface{}{ + "id": "three", + "dev-id": "developer1", + "format": "2", + }, nil, "") + c.Assert(err, IsNil) + })() + + gSuggestion, err := pool.Singleton("suggestion") + c.Assert(err, IsNil) + + ok, err := pool.Add(a, gSuggestion) + c.Check(err, IsNil) + c.Check(ok, Equals, false) + c.Assert(pool.Err("suggestion"), ErrorMatches, `proposed "test-only-decl" assertion has format 2 but 0 is latest supported`) +} + +func (s *poolSuite) TestAddOlderIgnored(c *C) { + pool := asserts.NewPool(s.db, 64) + + _, err := pool.ToResolve() + c.Assert(err, IsNil) + + gSuggestion, err := pool.Singleton("suggestion") + c.Assert(err, IsNil) + + ok, err := pool.Add(s.decl1_1, gSuggestion) + c.Assert(err, IsNil) + c.Assert(ok, Equals, true) + + ok, err = pool.Add(s.decl1, gSuggestion) + c.Assert(err, IsNil) + c.Assert(ok, Equals, true) + + toResolve, err := pool.ToResolve() + c.Assert(err, IsNil) + sortToResolve(toResolve) + dev1AcctAt := s.dev1Acct.At() + dev1AcctAt.Revision = asserts.RevisionNotKnown + storeKeyAt := s.hub.StoreAccountKey("").At() + storeKeyAt.Revision = asserts.RevisionNotKnown + + c.Check(toResolve, DeepEquals, map[asserts.Grouping][]*asserts.AtRevision{ + gSuggestion: {storeKeyAt, dev1AcctAt}, + }) +} + +func (s *poolSuite) TestUnknownGroup(c *C) { + pool := asserts.NewPool(s.db, 64) + + _, err := pool.Singleton("suggestion") + c.Assert(err, IsNil) + // sanity + c.Check(pool.Err("suggestion"), IsNil) + + c.Check(pool.Err("foo"), Equals, asserts.ErrUnknownPoolGroup) +} + +func (s *poolSuite) TestAddCurrentRevision(c *C) { + assertstest.AddMany(s.db, s.hub.StoreAccountKey(""), s.dev1Acct, s.decl1) + + pool := asserts.NewPool(s.db, 64) + + atDev1Acct := s.dev1Acct.At() + atDev1Acct.Revision = asserts.RevisionNotKnown + err := pool.AddUnresolved(atDev1Acct, "one") + c.Assert(err, IsNil) + + atDecl1 := s.decl1.At() + atDecl1.Revision = asserts.RevisionNotKnown + err = pool.AddUnresolved(atDecl1, "one") + c.Assert(err, IsNil) + + toResolve, err := pool.ToResolve() + c.Assert(err, IsNil) + sortToResolve(toResolve) + + c.Check(toResolve, DeepEquals, map[asserts.Grouping][]*asserts.AtRevision{ + asserts.MakePoolGrouping(0): {s.dev1Acct.At(), s.decl1.At()}, + }) + + // re-adding of current revisions, is not what we expect + // but needs not to produce unneeded roundtrips + + ok, err := pool.Add(s.hub.StoreAccountKey(""), asserts.MakePoolGrouping(0)) + c.Assert(err, IsNil) + c.Assert(ok, Equals, true) + + // this will be kept marked as unresolved until the ToResolve + ok, err = pool.Add(s.dev1Acct, asserts.MakePoolGrouping(0)) + c.Assert(err, IsNil) + c.Assert(ok, Equals, true) + + ok, err = pool.Add(s.decl1_1, asserts.MakePoolGrouping(0)) + c.Assert(err, IsNil) + c.Assert(ok, Equals, true) + + toResolve, err = pool.ToResolve() + c.Assert(err, IsNil) + c.Assert(toResolve, HasLen, 0) + + c.Check(pool.Err("one"), IsNil) +} + +func (s *poolSuite) TestUpdate(c *C) { + assertstest.AddMany(s.db, s.hub.StoreAccountKey("")) + assertstest.AddMany(s.db, s.dev1Acct, s.decl1, s.rev1_1111) + assertstest.AddMany(s.db, s.dev2Acct, s.decl2, s.rev2_2222) + + pool := asserts.NewPool(s.db, 64) + + err := pool.AddToUpdate(s.decl1.Ref(), "for_one") // group num: 0 + c.Assert(err, IsNil) + err = pool.AddToUpdate(s.decl2.Ref(), "for_two") // group num: 1 + c.Assert(err, IsNil) + + storeKeyAt := s.hub.StoreAccountKey("").At() + + toResolve, err := pool.ToResolve() + c.Assert(err, IsNil) + sortToResolve(toResolve) + c.Check(toResolve, DeepEquals, map[asserts.Grouping][]*asserts.AtRevision{ + asserts.MakePoolGrouping(0, 1): {storeKeyAt}, + asserts.MakePoolGrouping(0): {s.dev1Acct.At(), s.decl1.At()}, + asserts.MakePoolGrouping(1): {s.dev2Acct.At(), s.decl2.At()}, + }) + + ok, err := pool.Add(s.decl1_1, asserts.MakePoolGrouping(0)) + c.Assert(err, IsNil) + c.Assert(ok, Equals, true) + + toResolve, err = pool.ToResolve() + c.Assert(err, IsNil) + c.Check(toResolve, HasLen, 0) + + at2222 := &asserts.AtRevision{ + Ref: asserts.Ref{Type: asserts.TestOnlyRevType, PrimaryKey: []string{"2222"}}, + Revision: asserts.RevisionNotKnown, + } + err = pool.AddUnresolved(at2222, "for_two") + c.Assert(err, IsNil) + + toResolve, err = pool.ToResolve() + c.Assert(err, IsNil) + c.Check(toResolve, DeepEquals, map[asserts.Grouping][]*asserts.AtRevision{ + asserts.MakePoolGrouping(1): {&asserts.AtRevision{ + Ref: asserts.Ref{Type: asserts.TestOnlyRevType, PrimaryKey: []string{"2222"}}, + Revision: 0, + }}, + }) + + c.Check(pool.Err("for_one"), IsNil) + c.Check(pool.Err("for_two"), IsNil) +} + +var errBoom = errors.New("boom") + +func (s *poolSuite) TestAddErrorEarly(c *C) { + assertstest.AddMany(s.db, s.hub.StoreAccountKey("")) + + pool := asserts.NewPool(s.db, 64) + + storeKey := s.hub.StoreAccountKey("") + err := pool.AddToUpdate(storeKey.Ref(), "store_key") + c.Assert(err, IsNil) + + at1111 := &asserts.AtRevision{ + Ref: asserts.Ref{Type: asserts.TestOnlyRevType, PrimaryKey: []string{"1111"}}, + Revision: asserts.RevisionNotKnown, + } + err = pool.AddUnresolved(at1111, "for_one") + c.Assert(err, IsNil) + + toResolve, err := pool.ToResolve() + c.Assert(err, IsNil) + c.Check(toResolve, DeepEquals, map[asserts.Grouping][]*asserts.AtRevision{ + asserts.MakePoolGrouping(0): {storeKey.At()}, + asserts.MakePoolGrouping(1): {at1111}, + }) + + err = pool.AddError(errBoom, storeKey.Ref()) + c.Assert(err, IsNil) + + ok, err := pool.Add(s.rev1_1111, asserts.MakePoolGrouping(1)) + c.Assert(err, IsNil) + c.Assert(ok, Equals, true) + + toResolve, err = pool.ToResolve() + c.Assert(err, IsNil) + c.Check(toResolve, HasLen, 0) + + c.Check(pool.Err("store_key"), Equals, errBoom) + c.Check(pool.Err("for_one"), Equals, errBoom) +} + +func (s *poolSuite) TestAddErrorLater(c *C) { + assertstest.AddMany(s.db, s.hub.StoreAccountKey("")) + + pool := asserts.NewPool(s.db, 64) + + storeKey := s.hub.StoreAccountKey("") + err := pool.AddToUpdate(storeKey.Ref(), "store_key") + c.Assert(err, IsNil) + + at1111 := &asserts.AtRevision{ + Ref: asserts.Ref{Type: asserts.TestOnlyRevType, PrimaryKey: []string{"1111"}}, + Revision: asserts.RevisionNotKnown, + } + err = pool.AddUnresolved(at1111, "for_one") + c.Assert(err, IsNil) + + toResolve, err := pool.ToResolve() + c.Assert(err, IsNil) + c.Check(toResolve, DeepEquals, map[asserts.Grouping][]*asserts.AtRevision{ + asserts.MakePoolGrouping(0): {storeKey.At()}, + asserts.MakePoolGrouping(1): {at1111}, + }) + + ok, err := pool.Add(s.rev1_1111, asserts.MakePoolGrouping(1)) + c.Assert(err, IsNil) + c.Assert(ok, Equals, true) + + err = pool.AddError(errBoom, storeKey.Ref()) + c.Assert(err, IsNil) + + toResolve, err = pool.ToResolve() + c.Assert(err, IsNil) + c.Check(toResolve, HasLen, 0) + + c.Check(pool.Err("store_key"), Equals, errBoom) + c.Check(pool.Err("for_one"), Equals, errBoom) +} + +func (s *poolSuite) TestNopUpdatePlusFetchOfPushed(c *C) { + storeKey := s.hub.StoreAccountKey("") + assertstest.AddMany(s.db, storeKey) + assertstest.AddMany(s.db, s.dev1Acct) + assertstest.AddMany(s.db, s.decl1) + assertstest.AddMany(s.db, s.rev1_1111) + + pool := asserts.NewPool(s.db, 64) + + atOne := s.decl1.At() + err := pool.AddToUpdate(&atOne.Ref, "for_one") + c.Assert(err, IsNil) + + toResolve, err := pool.ToResolve() + c.Assert(err, IsNil) + sortToResolve(toResolve) + c.Check(toResolve, DeepEquals, map[asserts.Grouping][]*asserts.AtRevision{ + asserts.MakePoolGrouping(0): {storeKey.At(), s.dev1Acct.At(), atOne}, + }) + + // no updates but + // new push suggestion + + gSuggestion, err := pool.Singleton("suggestion") + c.Assert(err, IsNil) + + ok, err := pool.Add(s.rev1_3333, gSuggestion) + c.Assert(err, IsNil) + c.Assert(ok, Equals, true) + + toResolve, err = pool.ToResolve() + c.Assert(err, IsNil) + c.Assert(toResolve, HasLen, 0) + + c.Check(pool.Err("for_one"), IsNil) + + pool.AddGroupingError(errBoom, gSuggestion) + + c.Assert(pool.Err("for_one"), IsNil) + c.Assert(pool.Err("suggestion"), Equals, errBoom) + + at3333 := s.rev1_3333.At() + at3333.Revision = asserts.RevisionNotKnown + err = pool.AddUnresolved(at3333, at3333.Unique()) + c.Assert(err, IsNil) + + toResolve, err = pool.ToResolve() + c.Assert(err, IsNil) + c.Assert(toResolve, HasLen, 0) + + err = pool.CommitTo(s.db) + c.Check(err, IsNil) + + c.Assert(pool.Err(at3333.Unique()), IsNil) + + a, err := s.rev1_3333.Ref().Resolve(s.db.Find) + c.Assert(err, IsNil) + c.Check(a.(*asserts.TestOnlyRev).H(), Equals, "3333") +} + +func (s *poolSuite) TestAddToUpdateThenUnresolved(c *C) { + assertstest.AddMany(s.db, s.hub.StoreAccountKey("")) + + pool := asserts.NewPool(s.db, 64) + + storeKey := s.hub.StoreAccountKey("") + storeKeyAt := storeKey.At() + storeKeyAt.Revision = asserts.RevisionNotKnown + + err := pool.AddToUpdate(storeKey.Ref(), "for_one") + c.Assert(err, IsNil) + err = pool.AddUnresolved(storeKeyAt, "for_one") + c.Assert(err, IsNil) + + toResolve, err := pool.ToResolve() + c.Assert(err, IsNil) + c.Check(toResolve, DeepEquals, map[asserts.Grouping][]*asserts.AtRevision{ + asserts.MakePoolGrouping(0): {storeKey.At()}, + }) +} + +func (s *poolSuite) TestAddUnresolvedThenToUpdate(c *C) { + assertstest.AddMany(s.db, s.hub.StoreAccountKey("")) + + pool := asserts.NewPool(s.db, 64) + + storeKey := s.hub.StoreAccountKey("") + storeKeyAt := storeKey.At() + storeKeyAt.Revision = asserts.RevisionNotKnown + + err := pool.AddUnresolved(storeKeyAt, "for_one") + c.Assert(err, IsNil) + err = pool.AddToUpdate(storeKey.Ref(), "for_one") + c.Assert(err, IsNil) + + toResolve, err := pool.ToResolve() + c.Assert(err, IsNil) + c.Check(toResolve, DeepEquals, map[asserts.Grouping][]*asserts.AtRevision{ + asserts.MakePoolGrouping(0): {storeKey.At()}, + }) +} + +func (s *poolSuite) TestNopUpdatePlusFetch(c *C) { + assertstest.AddMany(s.db, s.hub.StoreAccountKey("")) + + pool := asserts.NewPool(s.db, 64) + + storeKey := s.hub.StoreAccountKey("") + err := pool.AddToUpdate(storeKey.Ref(), "store_key") + c.Assert(err, IsNil) + + at1111 := &asserts.AtRevision{ + Ref: asserts.Ref{Type: asserts.TestOnlyRevType, PrimaryKey: []string{"1111"}}, + Revision: asserts.RevisionNotKnown, + } + err = pool.AddUnresolved(at1111, "for_one") + c.Assert(err, IsNil) + + toResolve, err := pool.ToResolve() + c.Assert(err, IsNil) + c.Check(toResolve, DeepEquals, map[asserts.Grouping][]*asserts.AtRevision{ + asserts.MakePoolGrouping(0): {storeKey.At()}, + asserts.MakePoolGrouping(1): {at1111}, + }) + + ok, err := pool.Add(s.rev1_1111, asserts.MakePoolGrouping(1)) + c.Assert(err, IsNil) + c.Assert(ok, Equals, true) + + toResolve, err = pool.ToResolve() + c.Assert(err, IsNil) + sortToResolve(toResolve) + dev1AcctAt := s.dev1Acct.At() + dev1AcctAt.Revision = asserts.RevisionNotKnown + decl1At := s.decl1.At() + decl1At.Revision = asserts.RevisionNotKnown + c.Check(toResolve, DeepEquals, map[asserts.Grouping][]*asserts.AtRevision{ + asserts.MakePoolGrouping(1): {dev1AcctAt, decl1At}, + }) + + c.Check(pool.Err("store_key"), IsNil) + c.Check(pool.Err("for_one"), IsNil) +} + +func (s *poolSuite) TestParallelPartialResolutionFailure(c *C) { + pool := asserts.NewPool(s.db, 64) + + atOne := &asserts.AtRevision{ + Ref: asserts.Ref{Type: asserts.TestOnlyDeclType, PrimaryKey: []string{"one"}}, + Revision: asserts.RevisionNotKnown, + } + err := pool.AddUnresolved(atOne, "one") + c.Assert(err, IsNil) + + toResolve, err := pool.ToResolve() + c.Assert(err, IsNil) + c.Check(toResolve, DeepEquals, map[asserts.Grouping][]*asserts.AtRevision{ + asserts.MakePoolGrouping(0): {atOne}, + }) + + ok, err := pool.Add(s.decl1, asserts.MakePoolGrouping(0)) + c.Assert(err, IsNil) + c.Assert(ok, Equals, true) + + toResolve, err = pool.ToResolve() + c.Assert(err, IsNil) + sortToResolve(toResolve) + dev1AcctAt := s.dev1Acct.At() + dev1AcctAt.Revision = asserts.RevisionNotKnown + decl1At := s.decl1.At() + decl1At.Revision = asserts.RevisionNotKnown + storeKeyAt := s.hub.StoreAccountKey("").At() + storeKeyAt.Revision = asserts.RevisionNotKnown + c.Check(toResolve, DeepEquals, map[asserts.Grouping][]*asserts.AtRevision{ + asserts.MakePoolGrouping(0): {storeKeyAt, dev1AcctAt}, + }) + + // failed to get prereqs + c.Check(pool.AddGroupingError(errBoom, asserts.MakePoolGrouping(0)), IsNil) + + err = pool.AddUnresolved(atOne, "other") + c.Assert(err, IsNil) + + toResolve, err = pool.ToResolve() + c.Assert(err, IsNil) + c.Check(toResolve, HasLen, 0) + + c.Check(pool.Err("one"), Equals, errBoom) + c.Check(pool.Err("other"), IsNil) + + // we fail at commit though + err = pool.CommitTo(s.db) + c.Check(err, IsNil) + c.Check(pool.Err("one"), Equals, errBoom) + c.Check(pool.Err("other"), ErrorMatches, "cannot resolve prerequisite assertion.*") +} + +func (s *poolSuite) TestAddErrors(c *C) { + assertstest.AddMany(s.db, s.hub.StoreAccountKey("")) + + pool := asserts.NewPool(s.db, 64) + + storeKey := s.hub.StoreAccountKey("") + err := pool.AddToUpdate(storeKey.Ref(), "store_key") + c.Assert(err, IsNil) + + at1111 := &asserts.AtRevision{ + Ref: asserts.Ref{Type: asserts.TestOnlyRevType, PrimaryKey: []string{"1111"}}, + Revision: asserts.RevisionNotKnown, + } + err = pool.AddUnresolved(at1111, "for_one") + c.Assert(err, IsNil) + + toResolve, err := pool.ToResolve() + c.Assert(err, IsNil) + c.Check(toResolve, HasLen, 2) + + err = pool.AddError(errBoom, storeKey.Ref()) + c.Assert(err, IsNil) + + toResolve, err = pool.ToResolve() + c.Assert(err, IsNil) + c.Check(toResolve, HasLen, 0) + + c.Check(pool.Errors(), DeepEquals, map[string]error{ + "store_key": errBoom, + "for_one": asserts.ErrUnresolved, + }) +} + +func (s *poolSuite) TestPoolReuseWithClearGroupsAndUnchanged(c *C) { + assertstest.AddMany(s.db, s.hub.StoreAccountKey("")) + assertstest.AddMany(s.db, s.dev1Acct, s.decl1) + assertstest.AddMany(s.db, s.dev2Acct, s.decl2) + + pool := asserts.NewPool(s.db, 64) + + err := pool.AddToUpdate(s.decl1.Ref(), "for_one") // group num: 0 + c.Assert(err, IsNil) + + storeKeyAt := s.hub.StoreAccountKey("").At() + + toResolve, err := pool.ToResolve() + c.Assert(err, IsNil) + sortToResolve(toResolve) + c.Check(toResolve, DeepEquals, map[asserts.Grouping][]*asserts.AtRevision{ + asserts.MakePoolGrouping(0): {storeKeyAt, s.dev1Acct.At(), s.decl1.At()}, + }) + + ok, err := pool.Add(s.decl1_1, asserts.MakePoolGrouping(0)) + c.Assert(err, IsNil) + c.Assert(ok, Equals, true) + + toResolve, err = pool.ToResolve() + c.Assert(err, IsNil) + c.Check(toResolve, HasLen, 0) + + // clear the groups as we would do for real reuse when we have + // exhausted allowed groups + err = pool.ClearGroups() + c.Assert(err, IsNil) + + err = pool.AddToUpdate(s.decl2.Ref(), "for_two") // group num: 0 again + c.Assert(err, IsNil) + + // no reference to store key because it is remebered as unchanged + // across the clearing + toResolve, err = pool.ToResolve() + c.Assert(err, IsNil) + sortToResolve(toResolve) + c.Check(toResolve, DeepEquals, map[asserts.Grouping][]*asserts.AtRevision{ + asserts.MakePoolGrouping(0): {s.dev2Acct.At(), s.decl2.At()}, + }) +} diff -Nru snapd-2.45.1+20.04.2/asserts/repair.go snapd-2.48.3+20.04/asserts/repair.go --- snapd-2.45.1+20.04.2/asserts/repair.go 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/asserts/repair.go 2021-02-02 08:21:12.000000000 +0000 @@ -21,8 +21,6 @@ import ( "fmt" - "regexp" - "strconv" "strings" "time" ) @@ -55,6 +53,11 @@ return r.id } +// Sequence implements SequenceMember, it returns the same as RepairID. +func (r *Repair) Sequence() int { + return r.RepairID() +} + // Summary returns the mandatory summary description of the repair. func (r *Repair) Summary() string { return r.HeaderString("summary") @@ -97,24 +100,16 @@ // sanity var _ consistencyChecker = (*Repair)(nil) -// the repair-id can for now be a sequential number starting with 1 -var validRepairID = regexp.MustCompile("^[1-9][0-9]*$") - func assembleRepair(assert assertionBase) (Assertion, error) { err := checkAuthorityMatchesBrand(&assert) if err != nil { return nil, err } - repairID, err := checkStringMatches(assert.headers, "repair-id", validRepairID) + repairID, err := checkSequence(assert.headers, "repair-id") if err != nil { return nil, err } - id, err := strconv.Atoi(repairID) - if err != nil { - // given it matched it can likely only be too large - return nil, fmt.Errorf("repair-id too large: %s", repairID) - } summary, err := checkNotEmptyString(assert.headers, "summary") if err != nil { @@ -152,7 +147,7 @@ series: series, architectures: architectures, models: models, - id: id, + id: repairID, disabled: disabled, timestamp: timestamp, }, nil diff -Nru snapd-2.45.1+20.04.2/asserts/repair_test.go snapd-2.48.3+20.04/asserts/repair_test.go --- snapd-2.45.1+20.04.2/asserts/repair_test.go 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/asserts/repair_test.go 2021-02-02 08:21:12.000000000 +0000 @@ -95,10 +95,13 @@ a, err := asserts.Decode([]byte(s.repairStr)) c.Assert(err, IsNil) c.Check(a.Type(), Equals, asserts.RepairType) + _, ok := a.(asserts.SequenceMember) + c.Assert(ok, Equals, true) repair := a.(*asserts.Repair) c.Check(repair.Timestamp(), Equals, s.ts) c.Check(repair.BrandID(), Equals, "acme") c.Check(repair.RepairID(), Equals, 42) + c.Check(repair.Sequence(), Equals, 42) c.Check(repair.Summary(), Equals, "example repair") c.Check(repair.Series(), DeepEquals, []string{"16"}) c.Check(repair.Architectures(), DeepEquals, []string{"amd64", "arm64"}) @@ -142,10 +145,10 @@ {"architectures:\n - amd64\n - arm64\n", "architectures: foo\n", `"architectures" header must be a list of strings`}, {"models:\n - acme/frobinator\n", "models: \n", `"models" header must be a list of strings`}, {"models:\n - acme/frobinator\n", "models: something\n", `"models" header must be a list of strings`}, - {"repair-id: 42\n", "repair-id: no-number\n", `"repair-id" header contains invalid characters: "no-number"`}, - {"repair-id: 42\n", "repair-id: 0\n", `"repair-id" header contains invalid characters: "0"`}, - {"repair-id: 42\n", "repair-id: 01\n", `"repair-id" header contains invalid characters: "01"`}, - {"repair-id: 42\n", "repair-id: 99999999999999999999\n", `repair-id too large:.*`}, + {"repair-id: 42\n", "repair-id: no-number\n", `"repair-id" header is not an integer: no-number`}, + {"repair-id: 42\n", "repair-id: 0\n", `"repair-id" must be >=1: 0`}, + {"repair-id: 42\n", "repair-id: 01\n", `"repair-id" header has invalid prefix zeros: 01`}, + {"repair-id: 42\n", "repair-id: 99999999999999999999\n", `"repair-id" header is out of range: 99999999999999999999`}, {"brand-id: acme\n", "brand-id: brand-id-not-eq-authority-id\n", `authority-id and brand-id must match, repair assertions are expected to be signed by the brand: "acme" != "brand-id-not-eq-authority-id"`}, {"summary: example repair\n", "", `"summary" header is mandatory`}, {"summary: example repair\n", "summary: \n", `"summary" header should not be empty`}, diff -Nru snapd-2.45.1+20.04.2/asserts/snapasserts/snapasserts.go snapd-2.48.3+20.04/asserts/snapasserts/snapasserts.go --- snapd-2.45.1+20.04.2/asserts/snapasserts/snapasserts.go 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/asserts/snapasserts/snapasserts.go 2021-02-02 08:21:12.000000000 +0000 @@ -17,7 +17,7 @@ * */ -// Package snapasserts offers helpers to handle snap assertions and their checking for installation. +// Package snapasserts offers helpers to handle snap related assertions and their checking for installation. package snapasserts import ( diff -Nru snapd-2.45.1+20.04.2/asserts/snapasserts/validation_sets.go snapd-2.48.3+20.04/asserts/snapasserts/validation_sets.go --- snapd-2.45.1+20.04.2/asserts/snapasserts/validation_sets.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48.3+20.04/asserts/snapasserts/validation_sets.go 2021-02-02 08:21:12.000000000 +0000 @@ -0,0 +1,283 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2020 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 snapasserts + +import ( + "bytes" + "fmt" + "sort" + "strings" + + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/snap" +) + +// ValidationSetsConflictError describes an error where multiple +// validation sets are in conflict about snaps. +type ValidationSetsConflictError struct { + Sets map[string]*asserts.ValidationSet + Snaps map[string]error +} + +func (e *ValidationSetsConflictError) Error() string { + buf := bytes.NewBufferString("validation sets are in conflict:") + for _, err := range e.Snaps { + fmt.Fprintf(buf, "\n- %v", err) + } + return buf.String() +} + +// ValidationSets can hold a combination of validation-set assertions +// and can check for conflicts or help applying them. +type ValidationSets struct { + // sets maps sequence keys to validation-set in the combination + sets map[string]*asserts.ValidationSet + // snaps maps snap-ids to snap constraints + snaps map[string]*snapContraints +} + +const presConflict asserts.Presence = "conflict" + +var unspecifiedRevision = snap.R(0) +var invalidPresRevision = snap.R(-1) + +type snapContraints struct { + name string + presence asserts.Presence + // revisions maps revisions to pairing of ValidationSetSnap + // and the originating validation-set key + // * unspecifiedRevision is used for constraints without a + // revision + // * invalidPresRevision is used for constraints that mark + // presence as invalid + revisions map[snap.Revision][]*revConstraint +} + +type revConstraint struct { + validationSetKey string + asserts.ValidationSetSnap +} + +func (c *snapContraints) conflict() *snapConflictsError { + if c.presence != presConflict { + return nil + } + + const dontCare asserts.Presence = "" + whichSets := func(rcs []*revConstraint, presence asserts.Presence) []string { + which := make([]string, 0, len(rcs)) + for _, rc := range rcs { + if presence != dontCare && rc.Presence != presence { + continue + } + which = append(which, rc.validationSetKey) + } + if len(which) == 0 { + return nil + } + sort.Strings(which) + return which + } + + byRev := make(map[snap.Revision][]string, len(c.revisions)) + for r := range c.revisions { + pres := dontCare + switch r { + case invalidPresRevision: + pres = asserts.PresenceInvalid + case unspecifiedRevision: + pres = asserts.PresenceRequired + } + which := whichSets(c.revisions[r], pres) + if len(which) != 0 { + byRev[r] = which + } + } + + return &snapConflictsError{ + name: c.name, + revisions: byRev, + } +} + +type snapConflictsError struct { + name string + // revisions maps revisions to validation-set keys of the sets + // that are in conflict over the revision. + // * unspecifiedRevision is used for validation-sets conflicting + // on the snap by requiring it but without a revision + // * invalidPresRevision is used for validation-sets that mark + // presence as invalid + // see snapContraints.revisions as well + revisions map[snap.Revision][]string +} + +func (e *snapConflictsError) Error() string { + whichSets := func(which []string) string { + return fmt.Sprintf("(%s)", strings.Join(which, ",")) + } + + msg := fmt.Sprintf("cannot constrain snap %q", e.name) + invalid := false + if invalidOnes, ok := e.revisions[invalidPresRevision]; ok { + msg += fmt.Sprintf(" as both invalid %s and required", whichSets(invalidOnes)) + invalid = true + } + + var revnos []int + for r := range e.revisions { + if r.N >= 1 { + revnos = append(revnos, r.N) + } + } + if len(revnos) == 1 { + msg += fmt.Sprintf(" at revision %d %s", revnos[0], whichSets(e.revisions[snap.R(revnos[0])])) + } else if len(revnos) > 1 { + sort.Ints(revnos) + l := make([]string, 0, len(revnos)) + for _, rev := range revnos { + l = append(l, fmt.Sprintf("%d %s", rev, whichSets(e.revisions[snap.R(rev)]))) + } + msg += fmt.Sprintf(" at different revisions %s", strings.Join(l, ", ")) + } + + if unspecifiedOnes, ok := e.revisions[unspecifiedRevision]; ok { + which := whichSets(unspecifiedOnes) + if which != "" { + if len(revnos) != 0 { + msg += " or" + } + if invalid { + msg += fmt.Sprintf(" at any revision %s", which) + } else { + msg += fmt.Sprintf(" required at any revision %s", which) + } + } + } + return msg +} + +// NewValidationSets returns a new ValidationSets. +func NewValidationSets() *ValidationSets { + return &ValidationSets{ + sets: map[string]*asserts.ValidationSet{}, + snaps: map[string]*snapContraints{}, + } +} + +func valSetKey(valset *asserts.ValidationSet) string { + return fmt.Sprintf("%s/%s", valset.AccountID(), valset.Name()) +} + +// Add adds the given asserts.ValidationSet to the combination. +// It errors if a validation-set with the same sequence key has been +// added already. +func (v *ValidationSets) Add(valset *asserts.ValidationSet) error { + k := valSetKey(valset) + if _, ok := v.sets[k]; ok { + return fmt.Errorf("cannot add a second validation-set under %q", k) + } + v.sets[k] = valset + for _, sn := range valset.Snaps() { + v.addSnap(sn, k) + } + return nil +} + +func (v *ValidationSets) addSnap(sn *asserts.ValidationSetSnap, validationSetKey string) { + rev := snap.R(sn.Revision) + if sn.Presence == asserts.PresenceInvalid { + rev = invalidPresRevision + } + + rc := &revConstraint{ + validationSetKey: validationSetKey, + ValidationSetSnap: *sn, + } + + cs := v.snaps[sn.SnapID] + if cs == nil { + v.snaps[sn.SnapID] = &snapContraints{ + name: sn.Name, + presence: sn.Presence, + revisions: map[snap.Revision][]*revConstraint{ + rev: {rc}, + }, + } + return + } + + cs.revisions[rev] = append(cs.revisions[rev], rc) + if cs.presence == presConflict { + // nothing to check anymore + return + } + // this counts really different revisions or invalid + ndiff := len(cs.revisions) + if _, ok := cs.revisions[unspecifiedRevision]; ok { + ndiff -= 1 + } + switch { + case cs.presence == asserts.PresenceOptional: + cs.presence = sn.Presence + fallthrough + case cs.presence == sn.Presence || sn.Presence == asserts.PresenceOptional: + if ndiff > 1 { + if cs.presence == asserts.PresenceRequired { + // different revisions required/invalid + cs.presence = presConflict + return + } + // multiple optional at different revisions => invalid + cs.presence = asserts.PresenceInvalid + } + return + } + // we are left with a combo of required and invalid => conflict + cs.presence = presConflict + return +} + +// Conflict returns a non-nil error if the combination is in conflict, +// nil otherwise. +func (v *ValidationSets) Conflict() error { + sets := make(map[string]*asserts.ValidationSet) + snaps := make(map[string]error) + + for snapID, snConstrs := range v.snaps { + snConflictsErr := snConstrs.conflict() + if snConflictsErr != nil { + snaps[snapID] = snConflictsErr + for _, valsetKeys := range snConflictsErr.revisions { + for _, valsetKey := range valsetKeys { + sets[valsetKey] = v.sets[valsetKey] + } + } + } + } + + if len(snaps) != 0 { + return &ValidationSetsConflictError{ + Sets: sets, + Snaps: snaps, + } + } + return nil +} diff -Nru snapd-2.45.1+20.04.2/asserts/snapasserts/validation_sets_test.go snapd-2.48.3+20.04/asserts/snapasserts/validation_sets_test.go --- snapd-2.45.1+20.04.2/asserts/snapasserts/validation_sets_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48.3+20.04/asserts/snapasserts/validation_sets_test.go 2021-02-02 08:21:12.000000000 +0000 @@ -0,0 +1,265 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2020 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 snapasserts_test + +import ( + "fmt" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/asserts/assertstest" + "github.com/snapcore/snapd/asserts/snapasserts" +) + +type validationSetsSuite struct{} + +var _ = Suite(&validationSetsSuite{}) + +func (s *validationSetsSuite) TestAddFromSameSequence(c *C) { + mySnapAt7Valset := assertstest.FakeAssertion(map[string]interface{}{ + "type": "validation-set", + "authority-id": "account-id", + "series": "16", + "account-id": "account-id", + "name": "my-snap-ctl", + "sequence": "1", + "snaps": []interface{}{ + map[string]interface{}{ + "name": "my-snap", + "id": "mysnapididididididididididididid", + "presence": "required", + "revision": "7", + }, + }, + }).(*asserts.ValidationSet) + + mySnapAt8Valset := assertstest.FakeAssertion(map[string]interface{}{ + "type": "validation-set", + "authority-id": "account-id", + "series": "16", + "account-id": "account-id", + "name": "my-snap-ctl", + "sequence": "2", + "snaps": []interface{}{ + map[string]interface{}{ + "name": "my-snap", + "id": "mysnapididididididididididididid", + "presence": "required", + "revision": "8", + }, + }, + }).(*asserts.ValidationSet) + + valsets := snapasserts.NewValidationSets() + err := valsets.Add(mySnapAt7Valset) + c.Assert(err, IsNil) + err = valsets.Add(mySnapAt8Valset) + c.Check(err, ErrorMatches, `cannot add a second validation-set under "account-id/my-snap-ctl"`) +} + +func (s *validationSetsSuite) TestIntersections(c *C) { + mySnapAt7Valset := assertstest.FakeAssertion(map[string]interface{}{ + "type": "validation-set", + "authority-id": "account-id", + "series": "16", + "account-id": "account-id", + "name": "my-snap-ctl", + "sequence": "1", + "snaps": []interface{}{ + map[string]interface{}{ + "name": "my-snap", + "id": "mysnapididididididididididididid", + "presence": "required", + "revision": "7", + }, + }, + }).(*asserts.ValidationSet) + + mySnapAt7Valset2 := assertstest.FakeAssertion(map[string]interface{}{ + "type": "validation-set", + "authority-id": "account-id", + "series": "16", + "account-id": "account-id", + "name": "my-snap-ctl2", + "sequence": "2", + "snaps": []interface{}{ + map[string]interface{}{ + "name": "my-snap", + "id": "mysnapididididididididididididid", + "presence": "required", + "revision": "7", + }, + }, + }).(*asserts.ValidationSet) + + mySnapAt8Valset := assertstest.FakeAssertion(map[string]interface{}{ + "type": "validation-set", + "authority-id": "account-id", + "series": "16", + "account-id": "account-id", + "name": "my-snap-ctl-other", + "sequence": "1", + "snaps": []interface{}{ + map[string]interface{}{ + "name": "my-snap", + "id": "mysnapididididididididididididid", + "presence": "required", + "revision": "8", + }, + }, + }).(*asserts.ValidationSet) + + mySnapAt8OptValset := assertstest.FakeAssertion(map[string]interface{}{ + "type": "validation-set", + "authority-id": "account-id", + "series": "16", + "account-id": "account-id", + "name": "my-snap-ctl-opt", + "sequence": "1", + "snaps": []interface{}{ + map[string]interface{}{ + "name": "my-snap", + "id": "mysnapididididididididididididid", + "presence": "optional", + "revision": "8", + }, + }, + }).(*asserts.ValidationSet) + + mySnapInvalidValset := assertstest.FakeAssertion(map[string]interface{}{ + "type": "validation-set", + "authority-id": "account-id", + "series": "16", + "account-id": "account-id", + "name": "my-snap-ctl-inv", + "sequence": "1", + "snaps": []interface{}{ + map[string]interface{}{ + "name": "my-snap", + "id": "mysnapididididididididididididid", + "presence": "invalid", + }, + }, + }).(*asserts.ValidationSet) + + mySnapAt7OptValset := assertstest.FakeAssertion(map[string]interface{}{ + "type": "validation-set", + "authority-id": "account-id", + "series": "16", + "account-id": "account-id", + "name": "my-snap-ctl-opt2", + "sequence": "1", + "snaps": []interface{}{ + map[string]interface{}{ + "name": "my-snap", + "id": "mysnapididididididididididididid", + "presence": "optional", + "revision": "7", + }, + }, + }).(*asserts.ValidationSet) + + mySnapReqValset := assertstest.FakeAssertion(map[string]interface{}{ + "type": "validation-set", + "authority-id": "account-id", + "series": "16", + "account-id": "account-id", + "name": "my-snap-ctl-req-only", + "sequence": "1", + "snaps": []interface{}{ + map[string]interface{}{ + "name": "my-snap", + "id": "mysnapididididididididididididid", + "presence": "required", + }, + }, + }).(*asserts.ValidationSet) + + mySnapOptValset := assertstest.FakeAssertion(map[string]interface{}{ + "type": "validation-set", + "authority-id": "account-id", + "series": "16", + "account-id": "account-id", + "name": "my-snap-ctl-opt-only", + "sequence": "1", + "snaps": []interface{}{ + map[string]interface{}{ + "name": "my-snap", + "id": "mysnapididididididididididididid", + "presence": "optional", + }, + }, + }).(*asserts.ValidationSet) + + tests := []struct { + sets []*asserts.ValidationSet + conflictErr string + }{ + {[]*asserts.ValidationSet{mySnapAt7Valset}, ""}, + {[]*asserts.ValidationSet{mySnapAt7Valset, mySnapAt7Valset2}, ""}, + {[]*asserts.ValidationSet{mySnapAt7Valset, mySnapAt8Valset}, `(?ms)validation sets are in conflict:.*cannot constrain snap "my-snap" at different revisions 7 \(account-id/my-snap-ctl\), 8 \(account-id/my-snap-ctl-other\)`}, + {[]*asserts.ValidationSet{mySnapAt8Valset, mySnapAt8OptValset}, ""}, + {[]*asserts.ValidationSet{mySnapAt7Valset, mySnapAt8OptValset}, `(?ms)validation sets are in conflict:.*cannot constrain snap "my-snap" at different revisions 7 \(account-id/my-snap-ctl\), 8 \(account-id/my-snap-ctl-opt\)`}, + {[]*asserts.ValidationSet{mySnapAt7Valset, mySnapInvalidValset}, `(?ms)validation sets are in conflict:.*cannot constrain snap "my-snap" as both invalid \(account-id/my-snap-ctl-inv\) and required at revision 7 \(account-id/my-snap-ctl\)`}, + {[]*asserts.ValidationSet{mySnapInvalidValset, mySnapAt7Valset}, `(?ms)validation sets are in conflict:.*cannot constrain snap "my-snap" as both invalid \(account-id/my-snap-ctl-inv\) and required at revision 7 \(account-id/my-snap-ctl\)`}, + {[]*asserts.ValidationSet{mySnapAt8OptValset, mySnapInvalidValset}, ""}, + {[]*asserts.ValidationSet{mySnapInvalidValset, mySnapAt8OptValset}, ""}, + {[]*asserts.ValidationSet{mySnapAt7OptValset, mySnapAt8OptValset}, ""}, // no conflict but interpreted as invalid + {[]*asserts.ValidationSet{mySnapAt7OptValset, mySnapAt8OptValset, mySnapAt7Valset}, `(?ms)validation sets are in conflict:.*cannot constrain snap "my-snap" at different revisions 7 \(account-id/my-snap-ctl,account-id/my-snap-ctl-opt2\), 8 \(account-id/my-snap-ctl-opt\)`}, + {[]*asserts.ValidationSet{mySnapAt8OptValset, mySnapInvalidValset, mySnapAt7Valset}, `(?ms)validation sets are in conflict:.*cannot constrain snap "my-snap" as both invalid \(account-id/my-snap-ctl-inv\) and required at different revisions 7 \(account-id/my-snap-ctl\), 8 \(account-id/my-snap-ctl-opt\)`}, + {[]*asserts.ValidationSet{mySnapAt7Valset, mySnapReqValset}, ""}, + {[]*asserts.ValidationSet{mySnapReqValset, mySnapAt7Valset}, ""}, + {[]*asserts.ValidationSet{mySnapAt8OptValset, mySnapReqValset}, ""}, + {[]*asserts.ValidationSet{mySnapAt8OptValset, mySnapReqValset, mySnapAt7OptValset}, `(?ms)validation sets are in conflict:.*cannot constrain snap "my-snap" at different revisions 7 \(account-id/my-snap-ctl-opt2\), 8 \(account-id/my-snap-ctl-opt\) or required at any revision \(account-id/my-snap-ctl-req-only\)`}, + {[]*asserts.ValidationSet{mySnapAt8OptValset, mySnapAt7OptValset, mySnapReqValset}, `(?ms)validation sets are in conflict:.*cannot constrain snap "my-snap" at different revisions 7 \(account-id/my-snap-ctl-opt2\), 8 \(account-id/my-snap-ctl-opt\) or required at any revision \(account-id/my-snap-ctl-req-only\)`}, + {[]*asserts.ValidationSet{mySnapReqValset, mySnapInvalidValset}, `(?ms)validation sets are in conflict:.*cannot constrain snap "my-snap" as both invalid \(account-id/my-snap-ctl-inv\) and required at any revision \(account-id/my-snap-ctl-req-only\)`}, + {[]*asserts.ValidationSet{mySnapInvalidValset, mySnapReqValset}, `(?ms)validation sets are in conflict:.*cannot constrain snap "my-snap" as both invalid \(account-id/my-snap-ctl-inv\) and required at any revision \(account-id/my-snap-ctl-req-only\)`}, + {[]*asserts.ValidationSet{mySnapAt7Valset, mySnapAt8Valset, mySnapOptValset}, `(?ms)validation sets are in conflict:.*cannot constrain snap "my-snap" at different revisions 7 \(account-id/my-snap-ctl\), 8 \(account-id/my-snap-ctl-other\)`}, + {[]*asserts.ValidationSet{mySnapAt7Valset, mySnapOptValset}, ""}, + {[]*asserts.ValidationSet{mySnapOptValset, mySnapAt7Valset}, ""}, + {[]*asserts.ValidationSet{mySnapAt8OptValset, mySnapOptValset}, ""}, + {[]*asserts.ValidationSet{mySnapAt8OptValset, mySnapOptValset, mySnapAt7OptValset}, ""}, // no conflict but interpreted as invalid + {[]*asserts.ValidationSet{mySnapInvalidValset, mySnapOptValset}, ""}, + {[]*asserts.ValidationSet{mySnapOptValset, mySnapInvalidValset}, ""}, + {[]*asserts.ValidationSet{mySnapAt7Valset, mySnapAt8Valset, mySnapReqValset, mySnapInvalidValset}, `(?ms)validation sets are in conflict:.*cannot constrain snap "my-snap" as both invalid \(account-id/my-snap-ctl-inv\) and required at different revisions 7 \(account-id/my-snap-ctl\), 8 \(account-id/my-snap-ctl-other\) or at any revision \(account-id/my-snap-ctl-req-only\)`}, + } + + for _, t := range tests { + valsets := snapasserts.NewValidationSets() + cSets := make(map[string]*asserts.ValidationSet) + for _, valset := range t.sets { + err := valsets.Add(valset) + c.Assert(err, IsNil) + // mySnapOptValset never influcens an outcome + if valset != mySnapOptValset { + cSets[fmt.Sprintf("%s/%s", valset.AccountID(), valset.Name())] = valset + } + } + err := valsets.Conflict() + if t.conflictErr == "" { + c.Check(err, IsNil) + } else { + c.Check(err, ErrorMatches, t.conflictErr) + ce := err.(*snapasserts.ValidationSetsConflictError) + c.Check(ce.Sets, DeepEquals, cSets) + } + } +} diff -Nru snapd-2.45.1+20.04.2/asserts/snap_asserts.go snapd-2.48.3+20.04/asserts/snap_asserts.go --- snapd-2.45.1+20.04.2/asserts/snap_asserts.go 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/asserts/snap_asserts.go 2021-02-02 08:21:12.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2015-2016 Canonical Ltd + * Copyright (C) 2015-2020 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 @@ -481,6 +481,17 @@ } } +func checkSnapRevisionWhat(headers map[string]interface{}, name, what string) (snapRevision int, err error) { + snapRevision, err = checkIntWhat(headers, name, what) + if err != nil { + return 0, err + } + if snapRevision < 1 { + return 0, fmt.Errorf(`%q %s must be >=1: %d`, name, what, snapRevision) + } + return snapRevision, nil +} + func assembleSnapRevision(assert assertionBase) (Assertion, error) { _, err := checkDigest(assert.headers, "snap-sha3-384", crypto.SHA3_384) if err != nil { @@ -497,13 +508,10 @@ return nil, err } - snapRevision, err := checkInt(assert.headers, "snap-revision") + snapRevision, err := checkSnapRevisionWhat(assert.headers, "snap-revision", "header") if err != nil { return nil, err } - if snapRevision < 1 { - return nil, fmt.Errorf(`"snap-revision" header must be >=1: %d`, snapRevision) - } _, err = checkNotEmptyString(assert.headers, "developer-id") if err != nil { @@ -607,13 +615,10 @@ } func assembleValidation(assert assertionBase) (Assertion, error) { - approvedSnapRevision, err := checkInt(assert.headers, "approved-snap-revision") + approvedSnapRevision, err := checkSnapRevisionWhat(assert.headers, "approved-snap-revision", "header") if err != nil { return nil, err } - if approvedSnapRevision < 1 { - return nil, fmt.Errorf(`"approved-snap-revision" header must be >=1: %d`, approvedSnapRevision) - } revoked, err := checkOptionalBool(assert.headers, "revoked") if err != nil { diff -Nru snapd-2.45.1+20.04.2/asserts/snap_asserts_test.go snapd-2.48.3+20.04/asserts/snap_asserts_test.go --- snapd-2.45.1+20.04.2/asserts/snap_asserts_test.go 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/asserts/snap_asserts_test.go 2021-02-02 08:21:12.000000000 +0000 @@ -719,6 +719,8 @@ {"snap-size: 10000\n", "", `"snap-size" header is mandatory`}, {"snap-size: 10000\n", "snap-size: -1\n", `"snap-size" header is not an unsigned integer: -1`}, {"snap-size: 10000\n", "snap-size: zzz\n", `"snap-size" header is not an unsigned integer: zzz`}, + {"snap-size: 10000\n", "snap-size: 010\n", `"snap-size" header has invalid prefix zeros: 010`}, + {"snap-size: 10000\n", "snap-size: 99999999999999999999\n", `"snap-size" header is out of range: 99999999999999999999`}, {"grade: stable\n", "", `"grade" header is mandatory`}, {"grade: stable\n", "grade: \n", `"grade" header should not be empty`}, {sbs.tsLine, "", `"timestamp" header is mandatory`}, diff -Nru snapd-2.45.1+20.04.2/asserts/sysdb/sysdb.go snapd-2.48.3+20.04/asserts/sysdb/sysdb.go --- snapd-2.45.1+20.04.2/asserts/sysdb/sysdb.go 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/asserts/sysdb/sysdb.go 2021-02-02 08:21:12.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2015-2016 Canonical Ltd + * Copyright (C) 2015-2020 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,11 +39,18 @@ return asserts.OpenDatabase(cfg) } -// Open opens the system-wide assertion database with the trusted assertions set configured. -func Open() (*asserts.Database, error) { +// OpenAt opens a system assertion database at the given location with +// the trusted assertions set configured. +func OpenAt(path string) (*asserts.Database, error) { cfg := &asserts.DatabaseConfig{ Trusted: Trusted(), OtherPredefined: Generic(), } - return openDatabaseAt(dirs.SnapAssertsDBDir, cfg) + return openDatabaseAt(path, cfg) +} + +// Open opens the system-wide assertion database with the trusted assertions +// set configured. +func Open() (*asserts.Database, error) { + return OpenAt(dirs.SnapAssertsDBDir) } diff -Nru snapd-2.45.1+20.04.2/asserts/system_user.go snapd-2.48.3+20.04/asserts/system_user.go --- snapd-2.45.1+20.04.2/asserts/system_user.go 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/asserts/system_user.go 2021-02-02 08:21:12.000000000 +0000 @@ -36,6 +36,7 @@ assertionBase series []string models []string + serials []string sshKeys []string since time.Time until time.Time @@ -63,6 +64,11 @@ return su.models } +// Serials returns the serials that this assertion is valid for. +func (su *SystemUser) Serials() []string { + return su.serials +} + // Name returns the full name of the user (e.g. Random Guy). func (su *SystemUser) Name() string { return su.HeaderString("name") @@ -230,6 +236,17 @@ if err != nil { return nil, err } + serials, err := checkStringList(assert.headers, "serials") + if err != nil { + return nil, err + } + if len(serials) > 0 && assert.Format() < 1 { + return nil, fmt.Errorf(`the "serials" header is only supported for format 1 or greater`) + } + if len(serials) > 0 && len(models) != 1 { + return nil, fmt.Errorf(`in the presence of the "serials" header "models" must specify exactly one model`) + } + if _, err := checkOptionalString(assert.headers, "name"); err != nil { return nil, err } @@ -273,9 +290,24 @@ assertionBase: assert, series: series, models: models, + serials: serials, sshKeys: sshKeys, since: since, until: until, forcePasswordChange: forcePasswordChange, }, nil } + +func systemUserFormatAnalyze(headers map[string]interface{}, body []byte) (formatnum int, err error) { + formatnum = 0 + + serials, err := checkStringList(headers, "serials") + if err != nil { + return 0, err + } + if len(serials) > 0 { + formatnum = 1 + } + + return formatnum, nil +} diff -Nru snapd-2.45.1+20.04.2/asserts/system_user_test.go snapd-2.48.3+20.04/asserts/system_user_test.go --- snapd-2.45.1+20.04.2/asserts/system_user_test.go 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/asserts/system_user_test.go 2021-02-02 08:21:12.000000000 +0000 @@ -38,12 +38,14 @@ since time.Time sinceLine string + formatLine string modelsLine string systemUserStr string } const systemUserExample = "type: system-user\n" + + "FORMATLINE\n" + "authority-id: canonical\n" + "brand-id: canonical\n" + "email: foo@example.com\n" + @@ -68,9 +70,11 @@ s.until = time.Now().AddDate(0, 1, 0).Truncate(time.Second) s.untilLine = fmt.Sprintf("until: %s\n", s.until.Format(time.RFC3339)) s.modelsLine = "models:\n - frobinator\n" + s.formatLine = "format: 0\n" s.systemUserStr = strings.Replace(systemUserExample, "UNTILLINE\n", s.untilLine, 1) s.systemUserStr = strings.Replace(s.systemUserStr, "SINCELINE\n", s.sinceLine, 1) s.systemUserStr = strings.Replace(s.systemUserStr, "MODELSLINE\n", s.modelsLine, 1) + s.systemUserStr = strings.Replace(s.systemUserStr, "FORMATLINE\n", s.formatLine, 1) } func (s *systemUserSuite) TestDecodeOK(c *C) { @@ -183,6 +187,9 @@ {s.untilLine, "until: \n", `"until" header should not be empty`}, {s.untilLine, "until: 12:30\n", `"until" header is not a RFC3339 date: .*`}, {s.untilLine, "until: 1002-11-01T22:08:41+00:00\n", `'until' time cannot be before 'since' time`}, + {s.modelsLine, s.modelsLine + "serials: \n", `"serials" header must be a list of strings`}, + {s.modelsLine, s.modelsLine + "serials: something\n", `"serials" header must be a list of strings`}, + {s.modelsLine, s.modelsLine + "serials:\n - 7c7f435d-ed28-4281-bd77-e271e0846904\n", `the "serials" header is only supported for format 1 or greater`}, } for _, test := range invalidTests { @@ -212,3 +219,51 @@ _, err := asserts.Decode([]byte(su)) c.Check(err, IsNil) } + +// The following tests deal with "format: 1" which adds support for +// tying system-user assertions to device serials. + +var serialsLine = "serials:\n - 7c7f435d-ed28-4281-bd77-e271e0846904\n" + +func (s *systemUserSuite) TestDecodeInvalidFormat1Serials(c *C) { + s.systemUserStr = strings.Replace(s.systemUserStr, s.formatLine, "format: 1\n", 1) + serialWithMultipleModels := "models:\n - m1\n - m2\n" + serialsLine + + invalidTests := []struct{ original, invalid, expectedErr string }{ + {s.modelsLine, serialWithMultipleModels, `in the presence of the "serials" header "models" must specify exactly one model`}, + } + for _, test := range invalidTests { + invalid := strings.Replace(s.systemUserStr, test.original, test.invalid, 1) + _, err := asserts.Decode([]byte(invalid)) + c.Check(err, ErrorMatches, systemUserErrPrefix+test.expectedErr) + } +} + +func (s *systemUserSuite) TestDecodeOKFormat1Serials(c *C) { + s.systemUserStr = strings.Replace(s.systemUserStr, s.formatLine, "format: 1\n", 1) + + s.systemUserStr = strings.Replace(s.systemUserStr, s.modelsLine, s.modelsLine+serialsLine, 1) + a, err := asserts.Decode([]byte(s.systemUserStr)) + c.Assert(err, IsNil) + c.Check(a.Type(), Equals, asserts.SystemUserType) + systemUser := a.(*asserts.SystemUser) + // just for sanity, already covered by "format: 0" tests + c.Check(systemUser.BrandID(), Equals, "canonical") + // new in "format: 1" + c.Check(systemUser.Serials(), DeepEquals, []string{"7c7f435d-ed28-4281-bd77-e271e0846904"}) + +} + +func (s *systemUserSuite) TestSuggestedFormat(c *C) { + fmtnum, err := asserts.SuggestFormat(asserts.SystemUserType, nil, nil) + c.Assert(err, IsNil) + c.Check(fmtnum, Equals, 0) + + headers := map[string]interface{}{ + "serials": []interface{}{"serialserial"}, + } + fmtnum, err = asserts.SuggestFormat(asserts.SystemUserType, headers, nil) + c.Assert(err, IsNil) + c.Check(fmtnum, Equals, 1) + +} diff -Nru snapd-2.45.1+20.04.2/asserts/systestkeys/trusted.go snapd-2.48.3+20.04/asserts/systestkeys/trusted.go --- snapd-2.45.1+20.04.2/asserts/systestkeys/trusted.go 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/asserts/systestkeys/trusted.go 2021-02-02 08:21:12.000000000 +0000 @@ -238,7 +238,7 @@ TestRootAccountKey asserts.Assertion // here for convenience, does not need to be in the trusted set TestStoreAccountKey asserts.Assertion - // Testing-only trusted assertions for injecting in the the system trusted set. + // Testing-only trusted assertions for injecting in the system trusted set. Trusted []asserts.Assertion ) diff -Nru snapd-2.45.1+20.04.2/asserts/validation_set.go snapd-2.48.3+20.04/asserts/validation_set.go --- snapd-2.45.1+20.04.2/asserts/validation_set.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48.3+20.04/asserts/validation_set.go 2021-02-02 08:21:12.000000000 +0000 @@ -0,0 +1,262 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2020 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 asserts + +import ( + "fmt" + "regexp" + "strings" + "time" + + "github.com/snapcore/snapd/snap/naming" + "github.com/snapcore/snapd/strutil" +) + +// Presence represents a presence constraint. +type Presence string + +const ( + PresenceRequired Presence = "required" + PresenceOptional Presence = "optional" + PresenceInvalid Presence = "invalid" +) + +func presencesAsStrings(presences ...Presence) []string { + strs := make([]string, len(presences)) + for i, pres := range presences { + strs[i] = string(pres) + } + return strs +} + +var validValidationSetSnapPresences = presencesAsStrings(PresenceRequired, PresenceOptional, PresenceInvalid) + +func checkPresence(snap map[string]interface{}, which string, valid []string) (Presence, error) { + presence, err := checkOptionalStringWhat(snap, "presence", which) + if err != nil { + return Presence(""), err + } + if presence != "" && !strutil.ListContains(valid, presence) { + return Presence(""), fmt.Errorf("presence %s must be one of %s", which, strings.Join(valid, "|")) + } + return Presence(presence), nil +} + +// ValidationSetSnap holds the details about a snap constrained by a validation-set assertion. +type ValidationSetSnap struct { + Name string + SnapID string + + Presence Presence + + Revision int +} + +// SnapName implements naming.SnapRef. +func (s *ValidationSetSnap) SnapName() string { + return s.Name +} + +// ID implements naming.SnapRef. +func (s *ValidationSetSnap) ID() string { + return s.SnapID +} + +func checkValidationSetSnap(snap map[string]interface{}) (*ValidationSetSnap, error) { + name, err := checkNotEmptyStringWhat(snap, "name", "of snap") + if err != nil { + return nil, err + } + if err := naming.ValidateSnap(name); err != nil { + return nil, fmt.Errorf("invalid snap name %q", name) + } + + what := fmt.Sprintf("of snap %q", name) + + snapID, err := checkStringMatchesWhat(snap, "id", what, naming.ValidSnapID) + if err != nil { + return nil, err + } + + presence, err := checkPresence(snap, what, validValidationSetSnapPresences) + if err != nil { + return nil, err + } + + var snapRevision int + if _, ok := snap["revision"]; ok { + var err error + snapRevision, err = checkSnapRevisionWhat(snap, "revision", what) + if err != nil { + return nil, err + } + } + if snapRevision != 0 && presence == PresenceInvalid { + return nil, fmt.Errorf(`cannot specify revision %s at the same time as stating its presence is invalid`, what) + } + + return &ValidationSetSnap{ + Name: name, + SnapID: snapID, + Presence: presence, + Revision: snapRevision, + }, nil +} + +func checkValidationSetSnaps(snapList interface{}) ([]*ValidationSetSnap, error) { + const wrongHeaderType = `"snaps" header must be a list of maps` + + entries, ok := snapList.([]interface{}) + if !ok { + return nil, fmt.Errorf(wrongHeaderType) + } + + seen := make(map[string]bool, len(entries)) + seenIDs := make(map[string]string, len(entries)) + snaps := make([]*ValidationSetSnap, 0, len(entries)) + for _, entry := range entries { + snap, ok := entry.(map[string]interface{}) + if !ok { + return nil, fmt.Errorf(wrongHeaderType) + } + valSetSnap, err := checkValidationSetSnap(snap) + if err != nil { + return nil, err + } + + if seen[valSetSnap.Name] { + return nil, fmt.Errorf("cannot list the same snap %q multiple times", valSetSnap.Name) + } + seen[valSetSnap.Name] = true + snapID := valSetSnap.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, valSetSnap.Name) + } + seenIDs[snapID] = valSetSnap.Name + + if valSetSnap.Presence == "" { + valSetSnap.Presence = PresenceRequired + } + + snaps = append(snaps, valSetSnap) + } + + return snaps, nil +} + +// ValidationSet holds a validation-set assertion, which is a +// statement by an account about a set snaps and possibly revisions +// for which an extrinsic/implied property is valid (e.g. they work +// well together). validation-sets are organized in sequences under a +// name. +type ValidationSet struct { + assertionBase + + seq int + + snaps []*ValidationSetSnap + + timestamp time.Time +} + +// Series returns the series for which the snap in the set are declared. +func (vs *ValidationSet) Series() string { + return vs.HeaderString("series") +} + +// AccountID returns the identifier of the account that signed this assertion. +func (vs *ValidationSet) AccountID() string { + return vs.HeaderString("account-id") +} + +// Name returns the name under which the validation-set is organized. +func (vs *ValidationSet) Name() string { + return vs.HeaderString("name") +} + +// Sequence returns the sequential number of the validation-set in its +// named sequence. +func (vs *ValidationSet) Sequence() int { + return vs.seq +} + +// Snaps returns the constrained snaps by the validation-set. +func (vs *ValidationSet) Snaps() []*ValidationSetSnap { + return vs.snaps +} + +// Timestamp returns the time when the validation-set was issued. +func (vs *ValidationSet) Timestamp() time.Time { + return vs.timestamp +} + +func checkSequence(headers map[string]interface{}, name string) (int, error) { + seqnum, err := checkInt(headers, name) + if err != nil { + return -1, err + } + if seqnum < 1 { + return -1, fmt.Errorf("%q must be >=1: %v", name, seqnum) + } + return seqnum, nil +} + +var ( + validValidationSetName = regexp.MustCompile("^[a-z0-9](?:-?[a-z0-9])*$") +) + +func assembleValidationSet(assert assertionBase) (Assertion, error) { + authorityID := assert.AuthorityID() + accountID := assert.HeaderString("account-id") + if accountID != authorityID { + return nil, fmt.Errorf("authority-id and account-id must match, validation-set assertions are expected to be signed by the issuer account: %q != %q", authorityID, accountID) + } + + _, err := checkStringMatches(assert.headers, "name", validValidationSetName) + if err != nil { + return nil, err + } + + seq, err := checkSequence(assert.headers, "sequence") + if err != nil { + return nil, err + } + + snapList, ok := assert.headers["snaps"] + if !ok { + return nil, fmt.Errorf(`"snaps" header is mandatory`) + } + snaps, err := checkValidationSetSnaps(snapList) + if err != nil { + return nil, err + } + + timestamp, err := checkRFC3339Date(assert.headers, "timestamp") + if err != nil { + return nil, err + } + + return &ValidationSet{ + assertionBase: assert, + seq: seq, + snaps: snaps, + timestamp: timestamp, + }, nil +} diff -Nru snapd-2.45.1+20.04.2/asserts/validation_set_test.go snapd-2.48.3+20.04/asserts/validation_set_test.go --- snapd-2.45.1+20.04.2/asserts/validation_set_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48.3+20.04/asserts/validation_set_test.go 2021-02-02 08:21:12.000000000 +0000 @@ -0,0 +1,168 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2020 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 asserts_test + +import ( + "strings" + "time" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/asserts" +) + +type validationSetSuite struct { + ts time.Time + tsLine string +} + +var _ = Suite(&validationSetSuite{}) + +func (vss *validationSetSuite) SetUpSuite(c *C) { + vss.ts = time.Now().Truncate(time.Second).UTC() + vss.tsLine = "timestamp: " + vss.ts.Format(time.RFC3339) + "\n" +} + +const ( + validationSetExample = `type: validation-set +authority-id: brand-id1 +series: 16 +account-id: brand-id1 +name: baz-3000-good +sequence: 2 +snaps: + - + name: baz-linux + id: bazlinuxidididididididididididid + presence: optional + revision: 99 +OTHER` + "TSLINE" + + "body-length: 0\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==" +) + +func (vss *validationSetSuite) TestDecodeOK(c *C) { + encoded := strings.Replace(validationSetExample, "TSLINE", vss.tsLine, 1) + encoded = strings.Replace(encoded, "OTHER", "", 1) + + a, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + c.Check(a.Type(), Equals, asserts.ValidationSetType) + _, ok := a.(asserts.SequenceMember) + c.Assert(ok, Equals, true) + valset := a.(*asserts.ValidationSet) + c.Check(valset.AuthorityID(), Equals, "brand-id1") + c.Check(valset.Timestamp(), Equals, vss.ts) + c.Check(valset.Series(), Equals, "16") + c.Check(valset.AccountID(), Equals, "brand-id1") + c.Check(valset.Name(), Equals, "baz-3000-good") + c.Check(valset.Sequence(), Equals, 2) + snaps := valset.Snaps() + c.Assert(snaps, DeepEquals, []*asserts.ValidationSetSnap{ + { + Name: "baz-linux", + SnapID: "bazlinuxidididididididididididid", + Presence: asserts.PresenceOptional, + Revision: 99, + }, + }) + c.Check(snaps[0].SnapName(), Equals, "baz-linux") + c.Check(snaps[0].ID(), Equals, "bazlinuxidididididididididididid") +} + +func (vss *validationSetSuite) TestDecodeInvalid(c *C) { + const validationSetErrPrefix = "assertion validation-set: " + + encoded := strings.Replace(validationSetExample, "TSLINE", vss.tsLine, 1) + + snapsStanza := encoded[strings.Index(encoded, "snaps:"):strings.Index(encoded, "timestamp:")] + + invalidTests := []struct{ original, invalid, expectedErr string }{ + {"series: 16\n", "", `"series" header is mandatory`}, + {"series: 16\n", "series: \n", `"series" header should not be empty`}, + {"account-id: brand-id1\n", "", `"account-id" header is mandatory`}, + {"account-id: brand-id1\n", "account-id: \n", `"account-id" header should not be empty`}, + {"account-id: brand-id1\n", "account-id: random\n", `authority-id and account-id must match, validation-set assertions are expected to be signed by the issuer account: "brand-id1" != "random"`}, + {"name: baz-3000-good\n", "", `"name" header is mandatory`}, + {"name: baz-3000-good\n", "name: \n", `"name" header should not be empty`}, + {"name: baz-3000-good\n", "name: baz/3000/good\n", `"name" primary key header cannot contain '/'`}, + {"name: baz-3000-good\n", "name: baz+3000+good\n", `"name" header contains invalid characters: "baz\+3000\+good"`}, + {vss.tsLine, "timestamp: 12:30\n", `"timestamp" header is not a RFC3339 date: .*`}, + {"sequence: 2\n", "", `"sequence" header is mandatory`}, + {"sequence: 2\n", "sequence: one\n", `"sequence" header is not an integer: one`}, + {"sequence: 2\n", "sequence: 0\n", `"sequence" must be >=1: 0`}, + {"sequence: 2\n", "sequence: -1\n", `"sequence" must be >=1: -1`}, + {"sequence: 2\n", "sequence: 00\n", `"sequence" header has invalid prefix zeros: 00`}, + {"sequence: 2\n", "sequence: 01\n", `"sequence" header has invalid prefix zeros: 01`}, + {"sequence: 2\n", "sequence: 010\n", `"sequence" header has invalid prefix zeros: 010`}, + {snapsStanza, "", `"snaps" header is mandatory`}, + {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: baz-linux\n", "other: 1\n", `"name" of snap is mandatory`}, + {"name: baz-linux\n", "name: linux_2\n", `invalid snap name "linux_2"`}, + {"id: bazlinuxidididididididididididid\n", "id: 2\n", `"id" of snap "baz-linux" contains invalid characters: "2"`}, + {" id: bazlinuxidididididididididididid\n", "", `"id" of snap "baz-linux" is mandatory`}, + {"OTHER", " -\n name: baz-linux\n id: bazlinuxidididididididididididid\n", `cannot list the same snap "baz-linux" multiple times`}, + {"OTHER", " -\n name: baz-linux2\n id: bazlinuxidididididididididididid\n", `cannot specify the same snap id "bazlinuxidididididididididididid" multiple times, specified for snaps "baz-linux" and "baz-linux2"`}, + {"presence: optional\n", "presence:\n - opt\n", `"presence" of snap "baz-linux" must be a string`}, + {"presence: optional\n", "presence: no\n", `"presence" of snap "baz-linux" must be one of must be one of required|optional|invalid`}, + {"revision: 99\n", "revision: 0\n", `"revision" of snap "baz-linux" must be >=1: 0`}, + {"presence: optional\n", "presence: invalid\n", `cannot specify revision of snap "baz-linux" at the same time as stating its presence is invalid`}, + } + + for _, test := range invalidTests { + invalid := strings.Replace(encoded, test.original, test.invalid, 1) + invalid = strings.Replace(invalid, "OTHER", "", 1) + _, err := asserts.Decode([]byte(invalid)) + c.Check(err, ErrorMatches, validationSetErrPrefix+test.expectedErr) + } + +} + +func (vss *validationSetSuite) TestSnapPresenceOptionalDefaultRequired(c *C) { + encoded := strings.Replace(validationSetExample, "TSLINE", vss.tsLine, 1) + encoded = strings.Replace(encoded, "OTHER", "", 1) + encoded = strings.Replace(encoded, " presence: optional\n", "", 1) + + a, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + c.Check(a.Type(), Equals, asserts.ValidationSetType) + valset := a.(*asserts.ValidationSet) + snaps := valset.Snaps() + c.Assert(snaps, HasLen, 1) + c.Check(snaps[0].Presence, Equals, asserts.PresenceRequired) +} + +func (vss *validationSetSuite) TestSnapRevisionOptional(c *C) { + encoded := strings.Replace(validationSetExample, "TSLINE", vss.tsLine, 1) + encoded = strings.Replace(encoded, "OTHER", "", 1) + encoded = strings.Replace(encoded, " revision: 99\n", "", 1) + + a, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + c.Check(a.Type(), Equals, asserts.ValidationSetType) + valset := a.(*asserts.ValidationSet) + snaps := valset.Snaps() + c.Assert(snaps, HasLen, 1) + // 0 means unset + c.Check(snaps[0].Revision, Equals, 0) +} diff -Nru snapd-2.45.1+20.04.2/boot/assets.go snapd-2.48.3+20.04/boot/assets.go --- snapd-2.45.1+20.04.2/boot/assets.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48.3+20.04/boot/assets.go 2021-02-02 08:21:12.000000000 +0000 @@ -0,0 +1,859 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2020 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 boot + +import ( + "crypto" + "encoding/hex" + "errors" + "fmt" + "io" + "os" + "path/filepath" + + _ "golang.org/x/crypto/sha3" + + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/bootloader" + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/gadget" + "github.com/snapcore/snapd/logger" + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/secboot" + "github.com/snapcore/snapd/strutil" +) + +type trustedAssetsCache struct { + cacheDir string + hash crypto.Hash +} + +func newTrustedAssetsCache(cacheDir string) *trustedAssetsCache { + return &trustedAssetsCache{cacheDir: cacheDir, hash: crypto.SHA3_384} +} + +func (c *trustedAssetsCache) tempAssetRelPath(blName, assetName string) string { + return filepath.Join(blName, assetName+".temp") +} + +func (c *trustedAssetsCache) pathInCache(part string) string { + return filepath.Join(c.cacheDir, part) +} + +func trustedAssetCacheRelPath(blName, assetName, assetHash string) string { + return filepath.Join(blName, fmt.Sprintf("%s-%s", assetName, assetHash)) +} + +// fileHash calculates the hash of an arbitrary file using the same hash method +// as the cache. +func (c *trustedAssetsCache) fileHash(name string) (string, error) { + digest, _, err := osutil.FileDigest(name, c.hash) + if err != nil { + return "", err + } + return hex.EncodeToString(digest), nil +} + +// Add entry for a new named asset owned by a particular bootloader, with the +// binary content of the located at a given path. The cache ensures that only +// one entry for given tuple of (bootloader name, asset name, content-hash) +// exists in the cache. +func (c *trustedAssetsCache) Add(assetPath, blName, assetName string) (*trackedAsset, error) { + if err := os.MkdirAll(c.pathInCache(blName), 0755); err != nil { + return nil, fmt.Errorf("cannot create cache directory: %v", err) + } + + // input + inf, err := os.Open(assetPath) + if err != nil { + return nil, fmt.Errorf("cannot open asset file: %v", err) + } + defer inf.Close() + // temporary output + tempPath := c.pathInCache(c.tempAssetRelPath(blName, assetName)) + outf, err := osutil.NewAtomicFile(tempPath, 0644, 0, osutil.NoChown, osutil.NoChown) + if err != nil { + return nil, fmt.Errorf("cannot create temporary cache file: %v", err) + } + defer outf.Cancel() + + // copy and hash at the same time + h := c.hash.New() + tr := io.TeeReader(inf, h) + if _, err := io.Copy(outf, tr); err != nil { + return nil, fmt.Errorf("cannot copy trusted asset to cache: %v", err) + } + hashStr := hex.EncodeToString(h.Sum(nil)) + cacheKey := trustedAssetCacheRelPath(blName, assetName, hashStr) + + ta := &trackedAsset{ + blName: blName, + name: assetName, + hash: hashStr, + } + + targetName := c.pathInCache(cacheKey) + if osutil.FileExists(targetName) { + // asset is already cached + return ta, nil + } + // commit under a new name + if err := outf.CommitAs(targetName); err != nil { + return nil, fmt.Errorf("cannot commit file to assets cache: %v", err) + } + return ta, nil +} + +func (c *trustedAssetsCache) Remove(blName, assetName, hashStr string) error { + cacheKey := trustedAssetCacheRelPath(blName, assetName, hashStr) + if err := os.Remove(c.pathInCache(cacheKey)); err != nil && !os.IsNotExist(err) { + return err + } + return nil +} + +// CopyBootAssetsCacheToRoot copies the boot assets cache to a corresponding +// location under a new root directory. +func CopyBootAssetsCacheToRoot(dstRoot string) error { + if !osutil.IsDirectory(dirs.SnapBootAssetsDir) { + // nothing to copy + return nil + } + + newCacheRoot := dirs.SnapBootAssetsDirUnder(dstRoot) + if err := os.MkdirAll(newCacheRoot, 0755); err != nil { + return fmt.Errorf("cannot create cache directory under new root: %v", err) + } + err := filepath.Walk(dirs.SnapBootAssetsDir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + relPath, err := filepath.Rel(dirs.SnapBootAssetsDir, path) + if err != nil { + return err + } + if info.IsDir() { + if err := os.MkdirAll(filepath.Join(newCacheRoot, relPath), info.Mode()); err != nil { + return fmt.Errorf("cannot recreate cache directory %q: %v", relPath, err) + } + return nil + } + if !info.Mode().IsRegular() { + return fmt.Errorf("unsupported non-file entry %q mode %v", relPath, info.Mode()) + } + if err := osutil.CopyFile(path, filepath.Join(newCacheRoot, relPath), osutil.CopyFlagPreserveAll); err != nil { + return fmt.Errorf("cannot copy boot asset cache file %q: %v", relPath, err) + } + return nil + }) + return err +} + +// ErrObserverNotApplicable indicates that observer is not applicable for use +// with the model. +var ErrObserverNotApplicable = errors.New("observer not applicable") + +// TrustedAssetsInstallObserverForModel returns a new trusted assets observer +// for use during installation of the run mode system to track trusted and +// control managed assets, provided the device model indicates this might be +// needed. Otherwise, nil and ErrObserverNotApplicable is returned. +func TrustedAssetsInstallObserverForModel(model *asserts.Model, gadgetDir string, useEncryption bool) (*TrustedAssetsInstallObserver, error) { + if model.Grade() == asserts.ModelGradeUnset { + // no need to observe updates when assets are not managed + return nil, ErrObserverNotApplicable + } + if gadgetDir == "" { + return nil, fmt.Errorf("internal error: gadget dir not provided") + } + // TODO:UC20: clarify use of empty rootdir when getting the lists of + // managed and trusted assets + runBl, runTrusted, runManaged, err := gadgetMaybeTrustedBootloaderAndAssets(gadgetDir, "", + &bootloader.Options{ + Role: bootloader.RoleRunMode, + NoSlashBoot: true, + }) + if err != nil { + return nil, err + } + // and the recovery bootloader, seed is mounted during install + seedBl, seedTrusted, _, err := gadgetMaybeTrustedBootloaderAndAssets(gadgetDir, InitramfsUbuntuSeedDir, + &bootloader.Options{ + Role: bootloader.RoleRecovery, + }) + if err != nil { + return nil, err + } + if !useEncryption { + // we do not care about trusted assets when not encrypting data + // partition + runTrusted = nil + seedTrusted = nil + } + hasManaged := len(runManaged) > 0 + hasTrusted := len(runTrusted) > 0 || len(seedTrusted) > 0 + if !hasManaged && !hasTrusted && !useEncryption { + // no managed assets, and no trusted assets or we are not + // tracking them due to no encryption to data partition + return nil, ErrObserverNotApplicable + } + + return &TrustedAssetsInstallObserver{ + model: model, + cache: newTrustedAssetsCache(dirs.SnapBootAssetsDir), + gadgetDir: gadgetDir, + + blName: runBl.Name(), + managedAssets: runManaged, + trustedAssets: runTrusted, + + recoveryBlName: seedBl.Name(), + trustedRecoveryAssets: seedTrusted, + }, nil +} + +type trackedAsset struct { + blName, name, hash string +} + +func isAssetAlreadyTracked(bam bootAssetsMap, newAsset *trackedAsset) bool { + return isAssetHashTrackedInMap(bam, newAsset.name, newAsset.hash) +} + +func isAssetHashTrackedInMap(bam bootAssetsMap, assetName, assetHash string) bool { + if bam == nil { + return false + } + hashes, ok := bam[assetName] + if !ok { + return false + } + return strutil.ListContains(hashes, assetHash) +} + +// TrustedAssetsInstallObserver tracks the installation of trusted or managed +// boot assets. +type TrustedAssetsInstallObserver struct { + model *asserts.Model + gadgetDir string + cache *trustedAssetsCache + + blName string + managedAssets []string + trustedAssets []string + trackedAssets bootAssetsMap + + recoveryBlName string + trustedRecoveryAssets []string + trackedRecoveryAssets bootAssetsMap + + dataEncryptionKey secboot.EncryptionKey + saveEncryptionKey secboot.EncryptionKey +} + +// Observe observes the operation related to the content of a given gadget +// structure. In particular, the TrustedAssetsInstallObserver tracks writing of +// trusted or managed boot assets, such as the bootloader binary which is +// measured as part of the secure boot or the bootloader configuration. +// +// Implements gadget.ContentObserver. +func (o *TrustedAssetsInstallObserver) Observe(op gadget.ContentOperation, affectedStruct *gadget.LaidOutStructure, root, relativeTarget string, data *gadget.ContentChange) (gadget.ContentChangeAction, error) { + if affectedStruct.Role != gadget.SystemBoot { + // only care about system-boot + return gadget.ChangeApply, nil + } + + if len(o.managedAssets) != 0 && strutil.ListContains(o.managedAssets, relativeTarget) { + // this asset is managed by bootloader installation + return gadget.ChangeIgnore, nil + } + if len(o.trustedAssets) == 0 || !strutil.ListContains(o.trustedAssets, relativeTarget) { + // not one of the trusted assets + return gadget.ChangeApply, nil + } + ta, err := o.cache.Add(data.After, o.blName, filepath.Base(relativeTarget)) + if err != nil { + return gadget.ChangeAbort, err + } + // during installation, modeenv is written out later, at this point we + // only care that the same file may appear multiple times in gadget + // structure content, so make sure we are not tracking it yet + if !isAssetAlreadyTracked(o.trackedAssets, ta) { + if o.trackedAssets == nil { + o.trackedAssets = bootAssetsMap{} + } + if len(o.trackedAssets[ta.name]) > 0 { + return gadget.ChangeAbort, fmt.Errorf("cannot reuse asset name %q", ta.name) + } + o.trackedAssets[ta.name] = append(o.trackedAssets[ta.name], ta.hash) + } + return gadget.ChangeApply, nil +} + +// ObserveExistingTrustedRecoveryAssets observes existing trusted assets of a +// recovery bootloader located inside a given root directory. +func (o *TrustedAssetsInstallObserver) ObserveExistingTrustedRecoveryAssets(recoveryRootDir string) error { + if len(o.trustedRecoveryAssets) == 0 { + // not a trusted assets bootloader or has no trusted assets + return nil + } + for _, trustedAsset := range o.trustedRecoveryAssets { + ta, err := o.cache.Add(filepath.Join(recoveryRootDir, trustedAsset), o.recoveryBlName, filepath.Base(trustedAsset)) + if err != nil { + return err + } + if !isAssetAlreadyTracked(o.trackedRecoveryAssets, ta) { + if o.trackedRecoveryAssets == nil { + o.trackedRecoveryAssets = bootAssetsMap{} + } + if len(o.trackedRecoveryAssets[ta.name]) > 0 { + return fmt.Errorf("cannot reuse recovery asset name %q", ta.name) + } + o.trackedRecoveryAssets[ta.name] = append(o.trackedRecoveryAssets[ta.name], ta.hash) + } + } + return nil +} + +func (o *TrustedAssetsInstallObserver) currentTrustedBootAssetsMap() bootAssetsMap { + return o.trackedAssets +} + +func (o *TrustedAssetsInstallObserver) currentTrustedRecoveryBootAssetsMap() bootAssetsMap { + return o.trackedRecoveryAssets +} + +func (o *TrustedAssetsInstallObserver) ChosenEncryptionKeys(key, saveKey secboot.EncryptionKey) { + o.dataEncryptionKey = key + o.saveEncryptionKey = saveKey +} + +// TrustedAssetsUpdateObserverForModel returns a new trusted assets observer for +// tracking changes to the trusted boot assets and preserving managed assets, +// provided the device model indicates this might be needed. Otherwise, nil and +// ErrObserverNotApplicable is returned. +func TrustedAssetsUpdateObserverForModel(model *asserts.Model, gadgetDir string) (*TrustedAssetsUpdateObserver, error) { + if model.Grade() == asserts.ModelGradeUnset { + // no need to observe updates when assets are not managed + return nil, ErrObserverNotApplicable + } + // trusted assets need tracking only when the system is using encryption + // for its data partitions + trackTrustedAssets := false + _, err := sealedKeysMethod(dirs.GlobalRootDir) + switch { + case err == nil: + trackTrustedAssets = true + case err == errNoSealedKeys: + // nothing to do + case err != nil: + // all other errors + return nil, err + } + + // see what we need to observe for the run bootloader + runBl, runTrusted, runManaged, err := gadgetMaybeTrustedBootloaderAndAssets(gadgetDir, InitramfsUbuntuBootDir, + &bootloader.Options{ + Role: bootloader.RoleRunMode, + NoSlashBoot: true, + }) + if err != nil { + return nil, err + } + + // and the recovery bootloader + seedBl, seedTrusted, seedManaged, err := gadgetMaybeTrustedBootloaderAndAssets(gadgetDir, InitramfsUbuntuSeedDir, + &bootloader.Options{ + Role: bootloader.RoleRecovery, + }) + if err != nil { + return nil, err + } + + hasManaged := len(runManaged) > 0 || len(seedManaged) > 0 + hasTrusted := len(runTrusted) > 0 || len(seedTrusted) > 0 + if !hasManaged { + // no managed assets + if !hasTrusted || !trackTrustedAssets { + // no trusted assets or we are not tracking them either + return nil, ErrObserverNotApplicable + } + } + + obs := &TrustedAssetsUpdateObserver{ + cache: newTrustedAssetsCache(dirs.SnapBootAssetsDir), + model: model, + + bootBootloader: runBl, + bootManagedAssets: runManaged, + + seedBootloader: seedBl, + seedManagedAssets: seedManaged, + } + if trackTrustedAssets { + obs.seedTrustedAssets = seedTrusted + obs.bootTrustedAssets = runTrusted + } + return obs, nil +} + +// TrustedAssetsUpdateObserver tracks the updates of trusted boot assets and +// attempts to reseal when needed or preserves managed boot assets. +type TrustedAssetsUpdateObserver struct { + cache *trustedAssetsCache + model *asserts.Model + + bootBootloader bootloader.Bootloader + bootTrustedAssets []string + bootManagedAssets []string + changedAssets []*trackedAsset + + seedBootloader bootloader.Bootloader + seedTrustedAssets []string + seedManagedAssets []string + seedChangedAssets []*trackedAsset + + modeenv *Modeenv +} + +func trustedAndManagedAssetsOfBootloader(bl bootloader.Bootloader) (trustedAssets, managedAssets []string, err error) { + tbl, ok := bl.(bootloader.TrustedAssetsBootloader) + if ok { + trustedAssets, err = tbl.TrustedAssets() + if err != nil { + return nil, nil, fmt.Errorf("cannot list %q bootloader trusted assets: %v", bl.Name(), err) + } + managedAssets = tbl.ManagedAssets() + } + return trustedAssets, managedAssets, nil +} + +func findMaybeTrustedBootloaderAndAssets(rootDir string, opts *bootloader.Options) (foundBl bootloader.Bootloader, trustedAssets []string, err error) { + foundBl, err = bootloader.Find(rootDir, opts) + if err != nil { + return nil, nil, fmt.Errorf("cannot find bootloader: %v", err) + } + trustedAssets, _, err = trustedAndManagedAssetsOfBootloader(foundBl) + return foundBl, trustedAssets, err +} + +func gadgetMaybeTrustedBootloaderAndAssets(gadgetDir, rootDir string, opts *bootloader.Options) (foundBl bootloader.Bootloader, trustedAssets, managedAssets []string, err error) { + foundBl, err = bootloader.ForGadget(gadgetDir, rootDir, opts) + if err != nil { + return nil, nil, nil, fmt.Errorf("cannot find bootloader: %v", err) + } + trustedAssets, managedAssets, err = trustedAndManagedAssetsOfBootloader(foundBl) + return foundBl, trustedAssets, managedAssets, err +} + +// Observe observes the operation related to the update or rollback of the +// content of a given gadget structure. In particular, the +// TrustedAssetsUpdateObserver tracks updates of trusted boot assets such as +// bootloader binaries, or preserves managed assets such as boot configuration. +// +// Implements gadget.ContentUpdateObserver. +func (o *TrustedAssetsUpdateObserver) Observe(op gadget.ContentOperation, affectedStruct *gadget.LaidOutStructure, root, relativeTarget string, data *gadget.ContentChange) (gadget.ContentChangeAction, error) { + var whichBootloader bootloader.Bootloader + var whichTrustedAssets []string + var whichManagedAssets []string + var err error + var isRecovery bool + + switch affectedStruct.Role { + case gadget.SystemBoot: + whichBootloader = o.bootBootloader + whichTrustedAssets = o.bootTrustedAssets + whichManagedAssets = o.bootManagedAssets + case gadget.SystemSeed: + whichBootloader = o.seedBootloader + whichTrustedAssets = o.seedTrustedAssets + whichManagedAssets = o.seedManagedAssets + isRecovery = true + default: + // only system-seed and system-boot are of interest + return gadget.ChangeApply, nil + } + // maybe an asset that we manage? + if len(whichManagedAssets) != 0 && strutil.ListContains(whichManagedAssets, relativeTarget) { + // this asset is managed directly by the bootloader, preserve it + if op != gadget.ContentUpdate { + return gadget.ChangeAbort, fmt.Errorf("internal error: managed bootloader asset change for non update operation %v", op) + } + return gadget.ChangeIgnore, nil + } + + if len(whichTrustedAssets) == 0 { + // the system is not using encryption for data partitions, so + // we're done at this point + return gadget.ChangeApply, nil + } + + // maybe an asset that is trusted in the boot process? + if !strutil.ListContains(whichTrustedAssets, relativeTarget) { + // not one of the trusted assets + return gadget.ChangeApply, nil + } + if o.modeenv == nil { + // we've hit a trusted asset, so a modeenv is needed now too + o.modeenv, err = ReadModeenv("") + if err != nil { + return gadget.ChangeAbort, fmt.Errorf("cannot load modeenv: %v", err) + } + } + switch op { + case gadget.ContentUpdate: + return o.observeUpdate(whichBootloader, isRecovery, root, relativeTarget, data) + case gadget.ContentRollback: + return o.observeRollback(whichBootloader, isRecovery, root, relativeTarget, data) + default: + // we only care about update and rollback actions + return gadget.ChangeApply, nil + } +} + +func (o *TrustedAssetsUpdateObserver) observeUpdate(bl bootloader.Bootloader, recovery bool, root, relativeTarget string, change *gadget.ContentChange) (gadget.ContentChangeAction, error) { + modeenvBefore, err := o.modeenv.Copy() + if err != nil { + return gadget.ChangeAbort, fmt.Errorf("cannot copy modeenv: %v", err) + } + + // we may be running after a mid-update reboot, where a successful boot + // would have trimmed the tracked assets hash lists to contain only the + // asset we booted with + + var taBefore *trackedAsset + if change.Before != "" { + // make sure that the original copy is present in the cache if + // it existed + taBefore, err = o.cache.Add(change.Before, bl.Name(), filepath.Base(relativeTarget)) + if err != nil { + return gadget.ChangeAbort, err + } + } + + ta, err := o.cache.Add(change.After, bl.Name(), filepath.Base(relativeTarget)) + if err != nil { + return gadget.ChangeAbort, err + } + + trustedAssets := &o.modeenv.CurrentTrustedBootAssets + changedAssets := &o.changedAssets + if recovery { + trustedAssets = &o.modeenv.CurrentTrustedRecoveryBootAssets + changedAssets = &o.seedChangedAssets + } + // keep track of the change for cancellation purpose + *changedAssets = append(*changedAssets, ta) + + if *trustedAssets == nil { + *trustedAssets = bootAssetsMap{} + } + + if taBefore != nil && !isAssetAlreadyTracked(*trustedAssets, taBefore) { + // make sure that the boot asset that was was in the filesystem + // before the update, is properly tracked until either a + // successful boot or the update is canceled + // the original asset hash is listed first + (*trustedAssets)[taBefore.name] = append([]string{taBefore.hash}, (*trustedAssets)[taBefore.name]...) + } + + if !isAssetAlreadyTracked(*trustedAssets, ta) { + if len((*trustedAssets)[ta.name]) > 1 { + // we expect at most 2 different blobs for a given asset + // name, the current one and one that will be installed + // during an update; more entries indicates that the + // same asset name is used multiple times with different + // content + return gadget.ChangeAbort, fmt.Errorf("cannot reuse asset name %q", ta.name) + } + (*trustedAssets)[ta.name] = append((*trustedAssets)[ta.name], ta.hash) + } + + if o.modeenv.deepEqual(modeenvBefore) { + return gadget.ChangeApply, nil + } + if err := o.modeenv.Write(); err != nil { + return gadget.ChangeAbort, fmt.Errorf("cannot write modeeenv: %v", err) + } + return gadget.ChangeApply, nil +} + +func (o *TrustedAssetsUpdateObserver) observeRollback(bl bootloader.Bootloader, recovery bool, root, relativeTarget string, data *gadget.ContentChange) (gadget.ContentChangeAction, error) { + trustedAssets := &o.modeenv.CurrentTrustedBootAssets + otherTrustedAssets := o.modeenv.CurrentTrustedRecoveryBootAssets + if recovery { + trustedAssets = &o.modeenv.CurrentTrustedRecoveryBootAssets + otherTrustedAssets = o.modeenv.CurrentTrustedBootAssets + } + + assetName := filepath.Base(relativeTarget) + hashList, ok := (*trustedAssets)[assetName] + if !ok || len(hashList) == 0 { + // asset not tracked in modeenv + return gadget.ChangeApply, nil + } + + // new assets are appended to the list + expectedOldHash := hashList[0] + // sanity check, make sure that the current file is what we expect + newlyAdded := false + ondiskHash, err := o.cache.fileHash(filepath.Join(root, relativeTarget)) + if err != nil { + // file may not exist if it was added by the update, that's ok + if !os.IsNotExist(err) { + return gadget.ChangeAbort, fmt.Errorf("cannot calculate the digest of current asset: %v", err) + } + newlyAdded = true + if len(hashList) > 1 { + // we have more than 1 hash of the asset, so we expected + // a previous revision to be restored, but got nothing + // instead + return gadget.ChangeAbort, fmt.Errorf("tracked asset %q is unexpectedly missing from disk", + assetName) + } + } else { + if ondiskHash != expectedOldHash { + // this is unexpected, a different file exists on disk? + return gadget.ChangeAbort, fmt.Errorf("unexpected content of existing asset %q", relativeTarget) + } + } + + newHash := "" + if len(hashList) == 1 { + if newlyAdded { + newHash = hashList[0] + } + } else { + newHash = hashList[1] + } + if newHash != "" && !isAssetHashTrackedInMap(otherTrustedAssets, assetName, newHash) { + // asset revision is not used used elsewhere, we can remove it from the cache + if err := o.cache.Remove(bl.Name(), assetName, newHash); err != nil { + // XXX: should this be a log instead? + return gadget.ChangeAbort, fmt.Errorf("cannot remove unused boot asset %v:%v: %v", assetName, newHash, err) + } + } + + // update modeenv content + if !newlyAdded { + (*trustedAssets)[assetName] = hashList[:1] + } else { + delete(*trustedAssets, assetName) + } + + if err := o.modeenv.Write(); err != nil { + return gadget.ChangeAbort, fmt.Errorf("cannot write modeeenv: %v", err) + } + + return gadget.ChangeApply, nil +} + +// BeforeWrite is called when the update process has been staged for execution. +func (o *TrustedAssetsUpdateObserver) BeforeWrite() error { + if o.modeenv == nil { + // modeenv wasn't even loaded yet, meaning none of the trusted + // boot assets was updated + return nil + } + const expectReseal = true + if err := resealKeyToModeenv(dirs.GlobalRootDir, o.model, o.modeenv, expectReseal); err != nil { + return err + } + return nil +} + +func (o *TrustedAssetsUpdateObserver) canceledUpdate(recovery bool) { + trustedAssets := &o.modeenv.CurrentTrustedBootAssets + otherTrustedAssets := o.modeenv.CurrentTrustedRecoveryBootAssets + changedAssets := o.changedAssets + if recovery { + trustedAssets = &o.modeenv.CurrentTrustedRecoveryBootAssets + otherTrustedAssets = o.modeenv.CurrentTrustedBootAssets + changedAssets = o.seedChangedAssets + } + + if len(*trustedAssets) == 0 { + return + } + + for _, changed := range changedAssets { + hashList, ok := (*trustedAssets)[changed.name] + if !ok || len(hashList) == 0 { + // not tracked already, nothing to do + continue + } + if len(hashList) == 1 { + currentAssetHash := hashList[0] + if currentAssetHash != changed.hash { + // assets list has already been trimmed, nothing + // to do + continue + } else { + // asset was newly added + delete(*trustedAssets, changed.name) + } + } else { + // asset updates were appended to the list + (*trustedAssets)[changed.name] = hashList[:1] + } + if !isAssetHashTrackedInMap(otherTrustedAssets, changed.name, changed.hash) { + // asset revision is not used used elsewhere, we can remove it from the cache + if err := o.cache.Remove(changed.blName, changed.name, changed.hash); err != nil { + logger.Noticef("cannot remove unused boot asset %v:%v: %v", changed.name, changed.hash, err) + } + } + } +} + +// Canceled is called when the update has been canceled, or if changes +// were written and the update has been reverted. +func (o *TrustedAssetsUpdateObserver) Canceled() error { + if o.modeenv == nil { + // modeenv wasn't even loaded yet, meaning none of the boot + // assets was updated + return nil + } + for _, isRecovery := range []bool{false, true} { + o.canceledUpdate(isRecovery) + } + + if err := o.modeenv.Write(); err != nil { + return fmt.Errorf("cannot write modeeenv: %v", err) + } + + const expectReseal = true + if err := resealKeyToModeenv(dirs.GlobalRootDir, o.model, o.modeenv, expectReseal); err != nil { + return fmt.Errorf("while canceling gadget update: %v", err) + } + return nil +} + +func observeSuccessfulBootAssetsForBootloader(m *Modeenv, root string, opts *bootloader.Options) (drop []*trackedAsset, err error) { + trustedAssetsMap := &m.CurrentTrustedBootAssets + otherTrustedAssetsMap := m.CurrentTrustedRecoveryBootAssets + whichBootloader := "run mode" + if opts != nil && opts.Role == bootloader.RoleRecovery { + trustedAssetsMap = &m.CurrentTrustedRecoveryBootAssets + otherTrustedAssetsMap = m.CurrentTrustedBootAssets + whichBootloader = "recovery" + } + + if len(*trustedAssetsMap) == 0 { + // bootloader may have trusted assets, but we are not tracking + // any for the boot process + return nil, nil + } + + // let's find the bootloader first + bl, trustedAssets, err := findMaybeTrustedBootloaderAndAssets(root, opts) + if err != nil { + return nil, err + } + if len(trustedAssets) == 0 { + // not a trusted assets bootloader, nothing to do + return nil, nil + } + + cache := newTrustedAssetsCache(dirs.SnapBootAssetsDir) + for _, trustedAsset := range trustedAssets { + assetName := filepath.Base(trustedAsset) + + // find the hash of the file on disk + assetHash, err := cache.fileHash(filepath.Join(root, trustedAsset)) + if err != nil && !os.IsNotExist(err) { + return nil, fmt.Errorf("cannot calculate the digest of existing trusted asset: %v", err) + } + if assetHash == "" { + // no trusted asset on disk, but we booted nonetheless, + // at least log something + logger.Noticef("system booted without %v bootloader trusted asset %q", whichBootloader, trustedAsset) + // given that asset names cannot be reused, clear the + // boot assets map for the current bootloader + delete(*trustedAssetsMap, assetName) + continue + } + + // this is what we booted with + bootedWith := []string{assetHash} + // one of these was expected during boot + hashList := (*trustedAssetsMap)[assetName] + + assetFound := false + // find out if anything needs to be dropped + for _, hash := range hashList { + if hash == assetHash { + assetFound = true + continue + } + if !isAssetHashTrackedInMap(otherTrustedAssetsMap, assetName, hash) { + // asset can be dropped + drop = append(drop, &trackedAsset{ + blName: bl.Name(), + name: assetName, + hash: hash, + }) + } + } + + if !assetFound { + // unexpected, we have booted with an asset whose hash + // is not listed among the ones we expect + + // TODO:UC20: try to restore the asset from cache + return nil, fmt.Errorf("system booted with unexpected %v bootloader asset %q hash %v", whichBootloader, trustedAsset, assetHash) + } + + // update the list of what we booted with + (*trustedAssetsMap)[assetName] = bootedWith + + } + return drop, nil +} + +// observeSuccessfulBootAssets observes the state of the trusted boot assets +// after a successful boot. Returns a modified modeenv reflecting a new state, +// and a list of assets that can be dropped from the cache. +func observeSuccessfulBootAssets(m *Modeenv) (newM *Modeenv, drop []*trackedAsset, err error) { + newM, err = m.Copy() + if err != nil { + return nil, nil, err + } + + for _, bl := range []struct { + root string + opts *bootloader.Options + }{ + { + // ubuntu-boot bootloader + root: InitramfsUbuntuBootDir, + opts: &bootloader.Options{Role: bootloader.RoleRunMode, NoSlashBoot: true}, + }, { + // ubuntu-seed bootloader + root: InitramfsUbuntuSeedDir, + opts: &bootloader.Options{Role: bootloader.RoleRecovery, NoSlashBoot: true}, + }, + } { + dropForBootloader, err := observeSuccessfulBootAssetsForBootloader(newM, bl.root, bl.opts) + if err != nil { + return nil, nil, err + } + drop = append(drop, dropForBootloader...) + } + return newM, drop, nil +} diff -Nru snapd-2.45.1+20.04.2/boot/assets_test.go snapd-2.48.3+20.04/boot/assets_test.go --- snapd-2.45.1+20.04.2/boot/assets_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48.3+20.04/boot/assets_test.go 2021-02-02 08:21:12.000000000 +0000 @@ -0,0 +1,2765 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2020 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 boot_test + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + "syscall" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/boot" + "github.com/snapcore/snapd/boot/boottest" + "github.com/snapcore/snapd/bootloader" + "github.com/snapcore/snapd/bootloader/bootloadertest" + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/gadget" + "github.com/snapcore/snapd/logger" + "github.com/snapcore/snapd/secboot" + "github.com/snapcore/snapd/seed" + "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/testutil" + "github.com/snapcore/snapd/timings" +) + +type assetsSuite struct { + baseBootenvSuite +} + +var _ = Suite(&assetsSuite{}) + +func (s *assetsSuite) SetUpTest(c *C) { + s.baseBootenvSuite.SetUpTest(c) + c.Assert(os.MkdirAll(boot.InitramfsUbuntuBootDir, 0755), IsNil) + c.Assert(os.MkdirAll(boot.InitramfsUbuntuSeedDir, 0755), IsNil) + + restore := boot.MockSecbootResealKeys(func(params *secboot.ResealKeysParams) error { return nil }) + s.AddCleanup(restore) +} + +func checkContentGlob(c *C, glob string, expected []string) { + l, err := filepath.Glob(glob) + c.Assert(err, IsNil) + c.Check(l, DeepEquals, expected) +} + +func (s *assetsSuite) uc20UpdateObserverEncryptedSystemMockedBootloader(c *C) (*boot.TrustedAssetsUpdateObserver, *asserts.Model) { + // checked by TrustedAssetsUpdateObserverForModel and + // resealKeyToModeenv + s.stampSealedKeys(c, dirs.GlobalRootDir) + return s.uc20UpdateObserver(c, c.MkDir()) +} + +func (s *assetsSuite) uc20UpdateObserver(c *C, gadgetDir string) (*boot.TrustedAssetsUpdateObserver, *asserts.Model) { + uc20Model := boottest.MakeMockUC20Model() + obs, err := boot.TrustedAssetsUpdateObserverForModel(uc20Model, gadgetDir) + c.Assert(obs, NotNil) + c.Assert(err, IsNil) + return obs, uc20Model +} + +func (s *assetsSuite) bootloaderWithTrustedAssets(c *C, trustedAssets []string) *bootloadertest.MockTrustedAssetsBootloader { + tab := bootloadertest.Mock("trusted", "").WithTrustedAssets() + bootloader.Force(tab) + tab.TrustedAssetsList = trustedAssets + s.AddCleanup(func() { bootloader.Force(nil) }) + return tab +} + +func (s *assetsSuite) TestAssetsCacheAddRemove(c *C) { + cacheDir := c.MkDir() + d := c.MkDir() + + cache := boot.NewTrustedAssetsCache(cacheDir) + + data := []byte("foobar") + // SHA3-384 + hash := "0fa8abfbdaf924ad307b74dd2ed183b9a4a398891a2f6bac8fd2db7041b77f068580f9c6c66f699b496c2da1cbcc7ed8" + err := ioutil.WriteFile(filepath.Join(d, "foobar"), data, 0644) + c.Assert(err, IsNil) + + // add a new file + ta, err := cache.Add(filepath.Join(d, "foobar"), "grub", "grubx64.efi") + c.Assert(err, IsNil) + c.Check(filepath.Join(cacheDir, "grub", fmt.Sprintf("grubx64.efi-%s", hash)), testutil.FileEquals, string(data)) + c.Check(ta, NotNil) + + // try the same file again + taAgain, err := cache.Add(filepath.Join(d, "foobar"), "grub", "grubx64.efi") + c.Assert(err, IsNil) + // file already cached + c.Check(filepath.Join(cacheDir, "grub", fmt.Sprintf("grubx64.efi-%s", hash)), testutil.FileEquals, string(data)) + // and there's just one entry in the cache + checkContentGlob(c, filepath.Join(cacheDir, "grub", "*"), []string{ + filepath.Join(cacheDir, "grub", fmt.Sprintf("grubx64.efi-%s", hash)), + }) + // let go-check do the deep equals check + c.Check(taAgain, DeepEquals, ta) + + // same data but different asset name + taDifferentAsset, err := cache.Add(filepath.Join(d, "foobar"), "grub", "bootx64.efi") + c.Assert(err, IsNil) + // new entry in cache + c.Check(filepath.Join(cacheDir, "grub", fmt.Sprintf("bootx64.efi-%s", hash)), testutil.FileEquals, string(data)) + // 2 files now + checkContentGlob(c, filepath.Join(cacheDir, "grub", "*"), []string{ + filepath.Join(cacheDir, "grub", fmt.Sprintf("bootx64.efi-%s", hash)), + filepath.Join(cacheDir, "grub", fmt.Sprintf("grubx64.efi-%s", hash)), + }) + c.Check(taDifferentAsset, NotNil) + + // same source, data (new hash), existing asset name + newData := []byte("new foobar") + newHash := "5aa87615f6613a37d63c9a29746ef57457286c37148a4ae78493b0face5976c1fea940a19486e6bef65d43aec6b8f5a2" + err = ioutil.WriteFile(filepath.Join(d, "foobar"), newData, 0644) + c.Assert(err, IsNil) + + taExistingAssetName, err := cache.Add(filepath.Join(d, "foobar"), "grub", "bootx64.efi") + c.Assert(err, IsNil) + // new entry in cache + c.Check(taExistingAssetName, NotNil) + // we have both new and old asset + c.Check(filepath.Join(cacheDir, "grub", fmt.Sprintf("bootx64.efi-%s", newHash)), testutil.FileEquals, string(newData)) + c.Check(filepath.Join(cacheDir, "grub", fmt.Sprintf("bootx64.efi-%s", hash)), testutil.FileEquals, string(data)) + // 3 files in total + checkContentGlob(c, filepath.Join(cacheDir, "grub", "*"), []string{ + filepath.Join(cacheDir, "grub", fmt.Sprintf("bootx64.efi-%s", hash)), + filepath.Join(cacheDir, "grub", fmt.Sprintf("bootx64.efi-%s", newHash)), + filepath.Join(cacheDir, "grub", fmt.Sprintf("grubx64.efi-%s", hash)), + }) + + // drop + err = cache.Remove("grub", "bootx64.efi", newHash) + c.Assert(err, IsNil) + // asset bootx64.efi with given hash was dropped + c.Check(filepath.Join(cacheDir, "grub", fmt.Sprintf("bootx64.efi-%s", newHash)), testutil.FileAbsent) + // the other file still exists + c.Check(filepath.Join(cacheDir, "grub", fmt.Sprintf("bootx64.efi-%s", hash)), testutil.FileEquals, string(data)) + // remove it too + err = cache.Remove("grub", "bootx64.efi", hash) + c.Assert(err, IsNil) + c.Check(filepath.Join(cacheDir, "grub", fmt.Sprintf("bootx64.efi-%s", hash)), testutil.FileAbsent) + + // what is left is the grub assets only + checkContentGlob(c, filepath.Join(cacheDir, "grub", "*"), []string{ + filepath.Join(cacheDir, "grub", fmt.Sprintf("grubx64.efi-%s", hash)), + }) +} + +func (s *assetsSuite) TestAssetsCacheAddErr(c *C) { + cacheDir := c.MkDir() + d := c.MkDir() + cache := boot.NewTrustedAssetsCache(cacheDir) + + defer os.Chmod(cacheDir, 0755) + err := os.Chmod(cacheDir, 0000) + c.Assert(err, IsNil) + + if os.Geteuid() != 0 { + err = ioutil.WriteFile(filepath.Join(d, "foobar"), []byte("foo"), 0644) + c.Assert(err, IsNil) + // cannot create bootloader subdirectory + ta, err := cache.Add(filepath.Join(d, "foobar"), "grub", "grubx64.efi") + c.Assert(err, ErrorMatches, "cannot create cache directory: mkdir .*/grub: permission denied") + c.Check(ta, IsNil) + } + + // fix it now + err = os.Chmod(cacheDir, 0755) + c.Assert(err, IsNil) + + _, err = cache.Add(filepath.Join(d, "no-file"), "grub", "grubx64.efi") + c.Assert(err, ErrorMatches, "cannot open asset file: open .*/no-file: no such file or directory") + + if os.Geteuid() != 0 { + blDir := filepath.Join(cacheDir, "grub") + defer os.Chmod(blDir, 0755) + err = os.Chmod(blDir, 0000) + c.Assert(err, IsNil) + + _, err = cache.Add(filepath.Join(d, "foobar"), "grub", "grubx64.efi") + c.Assert(err, ErrorMatches, `cannot create temporary cache file: open .*/grub/grubx64\.efi\.temp\.[a-zA-Z0-9]+~: permission denied`) + } +} + +func (s *assetsSuite) TestAssetsCacheRemoveErr(c *C) { + cacheDir := c.MkDir() + d := c.MkDir() + cache := boot.NewTrustedAssetsCache(cacheDir) + + data := []byte("foobar") + dataHash := "0fa8abfbdaf924ad307b74dd2ed183b9a4a398891a2f6bac8fd2db7041b77f068580f9c6c66f699b496c2da1cbcc7ed8" + err := ioutil.WriteFile(filepath.Join(d, "foobar"), data, 0644) + c.Assert(err, IsNil) + // cannot create bootloader subdirectory + _, err = cache.Add(filepath.Join(d, "foobar"), "grub", "grubx64.efi") + c.Assert(err, IsNil) + // sanity + c.Check(filepath.Join(cacheDir, "grub", fmt.Sprintf("grubx64.efi-%s", dataHash)), testutil.FileEquals, string(data)) + + err = cache.Remove("grub", "no file", "some-hash") + c.Assert(err, IsNil) + + // different asset name but known hash + err = cache.Remove("grub", "different-name", dataHash) + c.Assert(err, IsNil) + c.Check(filepath.Join(cacheDir, "grub", fmt.Sprintf("grubx64.efi-%s", dataHash)), testutil.FileEquals, string(data)) +} + +func (s *assetsSuite) TestInstallObserverNew(c *C) { + d := c.MkDir() + // bootloader in gadget cannot be identified + uc20Model := boottest.MakeMockUC20Model() + for _, encryption := range []bool{true, false} { + obs, err := boot.TrustedAssetsInstallObserverForModel(uc20Model, d, encryption) + c.Assert(err, ErrorMatches, "cannot find bootloader: cannot determine bootloader") + c.Assert(obs, IsNil) + } + + // pretend grub is used + c.Assert(ioutil.WriteFile(filepath.Join(d, "grub.conf"), nil, 0755), IsNil) + + for _, encryption := range []bool{true, false} { + obs, err := boot.TrustedAssetsInstallObserverForModel(uc20Model, d, encryption) + c.Assert(err, IsNil) + c.Assert(obs, NotNil) + } + + // but nil for non UC20 + nonUC20Model := boottest.MakeMockModel() + nonUC20obs, err := boot.TrustedAssetsInstallObserverForModel(nonUC20Model, d, false) + c.Assert(err, Equals, boot.ErrObserverNotApplicable) + c.Assert(nonUC20obs, IsNil) + + // listing trusted assets fails + tab := s.bootloaderWithTrustedAssets(c, []string{ + "asset", + }) + tab.TrustedAssetsErr = fmt.Errorf("fail") + obs, err := boot.TrustedAssetsInstallObserverForModel(uc20Model, d, true) + c.Assert(err, ErrorMatches, `cannot list "trusted" bootloader trusted assets: fail`) + c.Assert(obs, IsNil) + // failed when listing run bootloader assets + c.Check(tab.TrustedAssetsCalls, Equals, 1) + + // force an error + bootloader.ForceError(fmt.Errorf("fail bootloader")) + obs, err = boot.TrustedAssetsInstallObserverForModel(uc20Model, d, true) + c.Assert(err, ErrorMatches, `cannot find bootloader: fail bootloader`) + c.Assert(obs, IsNil) +} + +var ( + mockRunBootStruct = &gadget.LaidOutStructure{ + VolumeStructure: &gadget.VolumeStructure{ + Role: gadget.SystemBoot, + }, + } + mockSeedStruct = &gadget.LaidOutStructure{ + VolumeStructure: &gadget.VolumeStructure{ + Role: gadget.SystemSeed, + }, + } +) + +func (s *assetsSuite) TestInstallObserverObserveSystemBootRealGrub(c *C) { + d := c.MkDir() + + // mock a bootloader that uses trusted assets + err := ioutil.WriteFile(filepath.Join(d, "grub.conf"), nil, 0644) + c.Assert(err, IsNil) + + // we get an observer for UC20 + uc20Model := boottest.MakeMockUC20Model() + useEncryption := true + obs, err := boot.TrustedAssetsInstallObserverForModel(uc20Model, d, useEncryption) + c.Assert(err, IsNil) + c.Assert(obs, NotNil) + + data := []byte("foobar") + // SHA3-384 + dataHash := "0fa8abfbdaf924ad307b74dd2ed183b9a4a398891a2f6bac8fd2db7041b77f068580f9c6c66f699b496c2da1cbcc7ed8" + err = ioutil.WriteFile(filepath.Join(d, "foobar"), data, 0644) + c.Assert(err, IsNil) + + otherData := []byte("other foobar") + err = ioutil.WriteFile(filepath.Join(d, "other-foobar"), otherData, 0644) + c.Assert(err, IsNil) + + writeChange := &gadget.ContentChange{ + // file that contains the data of the installed file + After: filepath.Join(d, "foobar"), + // there is no original file in place + Before: "", + } + // only grubx64.efi gets installed to system-boot + res, err := obs.Observe(gadget.ContentWrite, mockRunBootStruct, boot.InitramfsUbuntuBootDir, + "EFI/boot/grubx64.efi", writeChange) + c.Assert(err, IsNil) + c.Check(res, Equals, gadget.ChangeApply) + // Observe is called when populating content, but one can freely specify + // overlapping content entries, so a same file may be observed more than + // once + res, err = obs.Observe(gadget.ContentWrite, mockRunBootStruct, boot.InitramfsUbuntuBootDir, + "EFI/boot/grubx64.efi", writeChange) + c.Assert(err, IsNil) + c.Check(res, Equals, gadget.ChangeApply) + // try with one more file, which is not a trusted asset of a run mode, so it is ignored + res, err = obs.Observe(gadget.ContentWrite, mockRunBootStruct, boot.InitramfsUbuntuBootDir, + "EFI/boot/bootx64.efi", writeChange) + c.Assert(err, IsNil) + c.Check(res, Equals, gadget.ChangeApply) + // a managed boot asset is to be held + res, err = obs.Observe(gadget.ContentWrite, mockRunBootStruct, boot.InitramfsUbuntuBootDir, + "EFI/ubuntu/grub.cfg", writeChange) + c.Assert(err, IsNil) + c.Check(res, Equals, gadget.ChangeIgnore) + + // a single file in cache + checkContentGlob(c, filepath.Join(dirs.SnapBootAssetsDir, "grub", "*"), []string{ + filepath.Join(dirs.SnapBootAssetsDir, "grub", fmt.Sprintf("grubx64.efi-%s", dataHash)), + }) + + // and one more, a non system-boot structure, so the file is ignored + systemSeedStruct := &gadget.LaidOutStructure{ + VolumeStructure: &gadget.VolumeStructure{ + Role: gadget.SystemSeed, + }, + } + otherWriteChange := &gadget.ContentChange{ + After: filepath.Join(d, "other-foobar"), + } + res, err = obs.Observe(gadget.ContentWrite, systemSeedStruct, boot.InitramfsUbuntuBootDir, + "EFI/boot/grubx64.efi", otherWriteChange) + c.Assert(err, IsNil) + c.Check(res, Equals, gadget.ChangeApply) + // still, only one entry in the cache + checkContentGlob(c, filepath.Join(dirs.SnapBootAssetsDir, "grub", "*"), []string{ + filepath.Join(dirs.SnapBootAssetsDir, "grub", fmt.Sprintf("grubx64.efi-%s", dataHash)), + }) + + // let's see what the observer has tracked + tracked := obs.CurrentTrustedBootAssetsMap() + c.Check(tracked, DeepEquals, boot.BootAssetsMap{ + "grubx64.efi": []string{dataHash}, + }) +} + +func (s *assetsSuite) TestInstallObserverObserveSystemBootMocked(c *C) { + d := c.MkDir() + + tab := s.bootloaderWithTrustedAssets(c, []string{ + "asset", + "nested/other-asset", + }) + + // we get an observer for UC20 + uc20Model := boottest.MakeMockUC20Model() + useEncryption := true + obs, err := boot.TrustedAssetsInstallObserverForModel(uc20Model, d, useEncryption) + c.Assert(err, IsNil) + c.Assert(obs, NotNil) + // the list of trusted assets was asked for run and recovery bootloaders + c.Check(tab.TrustedAssetsCalls, Equals, 2) + + data := []byte("foobar") + // SHA3-384 + dataHash := "0fa8abfbdaf924ad307b74dd2ed183b9a4a398891a2f6bac8fd2db7041b77f068580f9c6c66f699b496c2da1cbcc7ed8" + err = ioutil.WriteFile(filepath.Join(d, "foobar"), data, 0644) + c.Assert(err, IsNil) + + writeChange := &gadget.ContentChange{ + // file that contains the data of the installed file + After: filepath.Join(d, "foobar"), + // there is no original file in place + Before: "", + } + res, err := obs.Observe(gadget.ContentWrite, mockRunBootStruct, boot.InitramfsUbuntuBootDir, + "asset", writeChange) + c.Assert(err, IsNil) + c.Check(res, Equals, gadget.ChangeApply) + // observe same asset again + res, err = obs.Observe(gadget.ContentWrite, mockRunBootStruct, boot.InitramfsUbuntuBootDir, + "asset", writeChange) + c.Assert(err, IsNil) + c.Check(res, Equals, gadget.ChangeApply) + // different one + res, err = obs.Observe(gadget.ContentWrite, mockRunBootStruct, boot.InitramfsUbuntuBootDir, + "nested/other-asset", writeChange) + c.Assert(err, IsNil) + c.Check(res, Equals, gadget.ChangeApply) + // a non trusted asset + res, err = obs.Observe(gadget.ContentWrite, mockRunBootStruct, boot.InitramfsUbuntuBootDir, + "non-trusted", writeChange) + c.Assert(err, IsNil) + c.Check(res, Equals, gadget.ChangeApply) + // a single file in cache + checkContentGlob(c, filepath.Join(dirs.SnapBootAssetsDir, "trusted", "*"), []string{ + filepath.Join(dirs.SnapBootAssetsDir, "trusted", fmt.Sprintf("asset-%s", dataHash)), + filepath.Join(dirs.SnapBootAssetsDir, "trusted", fmt.Sprintf("other-asset-%s", dataHash)), + }) + // let's see what the observer has tracked + tracked := obs.CurrentTrustedBootAssetsMap() + c.Check(tracked, DeepEquals, boot.BootAssetsMap{ + "asset": []string{dataHash}, + "other-asset": []string{dataHash}, + }) +} + +func (s *assetsSuite) TestInstallObserverObserveSystemBootMockedNoEncryption(c *C) { + d := c.MkDir() + s.bootloaderWithTrustedAssets(c, []string{"asset"}) + uc20Model := boottest.MakeMockUC20Model() + useEncryption := false + obs, err := boot.TrustedAssetsInstallObserverForModel(uc20Model, d, useEncryption) + c.Assert(err, Equals, boot.ErrObserverNotApplicable) + c.Assert(obs, IsNil) +} + +func (s *assetsSuite) TestInstallObserverObserveSystemBootMockedUnencryptedWithManaged(c *C) { + d := c.MkDir() + tab := s.bootloaderWithTrustedAssets(c, []string{"asset"}) + tab.ManagedAssetsList = []string{"managed"} + uc20Model := boottest.MakeMockUC20Model() + useEncryption := false + obs, err := boot.TrustedAssetsInstallObserverForModel(uc20Model, d, useEncryption) + c.Assert(err, IsNil) + c.Assert(obs, NotNil) + + c.Assert(ioutil.WriteFile(filepath.Join(d, "foobar"), nil, 0755), IsNil) + writeChange := &gadget.ContentChange{ + // file that contains the data of the installed file + After: filepath.Join(d, "foobar"), + // there is no original file in place + Before: "", + } + res, err := obs.Observe(gadget.ContentWrite, mockRunBootStruct, boot.InitramfsUbuntuBootDir, + "managed", writeChange) + c.Assert(err, IsNil) + c.Check(res, Equals, gadget.ChangeIgnore) +} + +func (s *assetsSuite) TestInstallObserverNonTrustedBootloader(c *C) { + // bootloader is not a trusted assets one, but we use encryption, one + // may try setting encryption key on the observer + + d := c.MkDir() + + // MockBootloader does not implement trusted assets + bootloader.Force(bootloadertest.Mock("mock", "")) + defer bootloader.Force(nil) + + // we get an observer for UC20 + uc20Model := boottest.MakeMockUC20Model() + useEncryption := true + 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}) +} + +func (s *assetsSuite) TestInstallObserverTrustedButNoAssets(c *C) { + // bootloader has no trusted assets, but encryption is enabled, and one + // may try setting a key on the observer + + d := c.MkDir() + + tab := bootloadertest.Mock("trusted-assets", "").WithTrustedAssets() + bootloader.Force(tab) + defer bootloader.Force(nil) + + // we get an observer for UC20 + uc20Model := boottest.MakeMockUC20Model() + useEncryption := true + 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}) +} + +func (s *assetsSuite) TestInstallObserverTrustedReuseNameErr(c *C) { + d := c.MkDir() + + tab := s.bootloaderWithTrustedAssets(c, []string{ + "asset", + "nested/asset", + }) + + // we get an observer for UC20 + uc20Model := boottest.MakeMockUC20Model() + useEncryption := true + obs, err := boot.TrustedAssetsInstallObserverForModel(uc20Model, d, useEncryption) + c.Assert(err, IsNil) + c.Assert(obs, NotNil) + // the list of trusted assets was asked for run and recovery bootloaders + c.Check(tab.TrustedAssetsCalls, Equals, 2) + + err = ioutil.WriteFile(filepath.Join(d, "foobar"), []byte("foobar"), 0644) + c.Assert(err, IsNil) + err = ioutil.WriteFile(filepath.Join(d, "other"), []byte("other"), 0644) + c.Assert(err, IsNil) + res, err := obs.Observe(gadget.ContentWrite, mockRunBootStruct, boot.InitramfsUbuntuBootDir, "asset", + &gadget.ContentChange{After: filepath.Join(d, "foobar")}) + c.Assert(err, IsNil) + c.Check(res, Equals, gadget.ChangeApply) + // same asset name but different content + res, err = obs.Observe(gadget.ContentWrite, mockRunBootStruct, boot.InitramfsUbuntuBootDir, "nested/asset", + &gadget.ContentChange{After: filepath.Join(d, "other")}) + c.Assert(err, ErrorMatches, `cannot reuse asset name "asset"`) + c.Check(res, Equals, gadget.ChangeAbort) +} + +func (s *assetsSuite) TestInstallObserverObserveExistingRecoveryMocked(c *C) { + d := c.MkDir() + + tab := s.bootloaderWithTrustedAssets(c, []string{ + "asset", + "nested/other-asset", + "shim", + }) + + // we get an observer for UC20 + uc20Model := boottest.MakeMockUC20Model() + useEncryption := true + obs, err := boot.TrustedAssetsInstallObserverForModel(uc20Model, d, useEncryption) + c.Assert(err, IsNil) + c.Assert(obs, NotNil) + // trusted assets for the run and recovery bootloaders were asked for + c.Check(tab.TrustedAssetsCalls, Equals, 2) + + data := []byte("foobar") + // SHA3-384 + dataHash := "0fa8abfbdaf924ad307b74dd2ed183b9a4a398891a2f6bac8fd2db7041b77f068580f9c6c66f699b496c2da1cbcc7ed8" + err = ioutil.WriteFile(filepath.Join(d, "asset"), data, 0644) + c.Assert(err, IsNil) + err = os.Mkdir(filepath.Join(d, "nested"), 0755) + c.Assert(err, IsNil) + err = ioutil.WriteFile(filepath.Join(d, "nested/other-asset"), data, 0644) + c.Assert(err, IsNil) + shim := []byte("shim") + shimHash := "dac0063e831d4b2e7a330426720512fc50fa315042f0bb30f9d1db73e4898dcb89119cac41fdfa62137c8931a50f9d7b" + err = ioutil.WriteFile(filepath.Join(d, "shim"), shim, 0644) + c.Assert(err, IsNil) + + err = obs.ObserveExistingTrustedRecoveryAssets(d) + c.Assert(err, IsNil) + checkContentGlob(c, filepath.Join(dirs.SnapBootAssetsDir, "trusted", "*"), []string{ + filepath.Join(dirs.SnapBootAssetsDir, "trusted", fmt.Sprintf("asset-%s", dataHash)), + filepath.Join(dirs.SnapBootAssetsDir, "trusted", fmt.Sprintf("other-asset-%s", dataHash)), + filepath.Join(dirs.SnapBootAssetsDir, "trusted", fmt.Sprintf("shim-%s", shimHash)), + }) + // the list of trusted assets for recovery was asked for + c.Check(tab.TrustedAssetsCalls, Equals, 2) + // let's see what the observer has tracked + tracked := obs.CurrentTrustedRecoveryBootAssetsMap() + c.Check(tracked, DeepEquals, boot.BootAssetsMap{ + "asset": []string{dataHash}, + "other-asset": []string{dataHash}, + "shim": []string{shimHash}, + }) +} + +func (s *assetsSuite) TestInstallObserverObserveExistingRecoveryReuseNameErr(c *C) { + d := c.MkDir() + + tab := s.bootloaderWithTrustedAssets(c, []string{ + "asset", + "nested/asset", + }) + // we get an observer for UC20 + uc20Model := boottest.MakeMockUC20Model() + useEncryption := true + obs, err := boot.TrustedAssetsInstallObserverForModel(uc20Model, d, useEncryption) + c.Assert(err, IsNil) + c.Assert(obs, NotNil) + // got the list of trusted assets for run and recovery bootloaders + c.Check(tab.TrustedAssetsCalls, Equals, 2) + + err = ioutil.WriteFile(filepath.Join(d, "asset"), []byte("foobar"), 0644) + c.Assert(err, IsNil) + err = os.MkdirAll(filepath.Join(d, "nested"), 0755) + c.Assert(err, IsNil) + // same asset name but different content + err = ioutil.WriteFile(filepath.Join(d, "nested/asset"), []byte("other"), 0644) + c.Assert(err, IsNil) + err = obs.ObserveExistingTrustedRecoveryAssets(d) + // same asset name but different content + c.Assert(err, ErrorMatches, `cannot reuse recovery asset name "asset"`) + // got the list of trusted assets for recovery bootloader + c.Check(tab.TrustedAssetsCalls, Equals, 2) +} + +func (s *assetsSuite) TestInstallObserverObserveExistingRecoveryButMissingErr(c *C) { + d := c.MkDir() + + tab := s.bootloaderWithTrustedAssets(c, []string{ + "asset", + }) + + uc20Model := boottest.MakeMockUC20Model() + useEncryption := true + obs, err := boot.TrustedAssetsInstallObserverForModel(uc20Model, d, useEncryption) + c.Assert(err, IsNil) + c.Assert(obs, NotNil) + c.Check(tab.TrustedAssetsCalls, Equals, 2) + + // trusted asset is missing + err = obs.ObserveExistingTrustedRecoveryAssets(d) + c.Assert(err, ErrorMatches, "cannot open asset file: .*/asset: no such file or directory") +} + +func (s *assetsSuite) TestUpdateObserverNew(c *C) { + tab := s.bootloaderWithTrustedAssets(c, nil) + + uc20Model := boottest.MakeMockUC20Model() + + gadgetDir := c.MkDir() + + // no trusted or managed assets + obs, err := boot.TrustedAssetsUpdateObserverForModel(uc20Model, gadgetDir) + c.Assert(err, Equals, boot.ErrObserverNotApplicable) + c.Check(obs, IsNil) + + // no managed, some trusted assets, but we are not tracking them + tab.TrustedAssetsList = []string{"asset"} + obs, err = boot.TrustedAssetsUpdateObserverForModel(uc20Model, gadgetDir) + c.Assert(err, Equals, boot.ErrObserverNotApplicable) + c.Check(obs, IsNil) + + // let's see some managed assets, but not trusted assets + tab.ManagedAssetsList = []string{"managed"} + tab.TrustedAssetsList = nil + obs, err = boot.TrustedAssetsUpdateObserverForModel(uc20Model, gadgetDir) + c.Assert(err, IsNil) + c.Check(obs, NotNil) + + // no managed, some trusted which we need to track + s.stampSealedKeys(c, dirs.GlobalRootDir) + tab.ManagedAssetsList = nil + tab.TrustedAssetsList = []string{"asset"} + obs, err = boot.TrustedAssetsUpdateObserverForModel(uc20Model, gadgetDir) + c.Assert(err, IsNil) + c.Assert(obs, NotNil) + + // but nil for non UC20 + nonUC20Model := boottest.MakeMockModel() + nonUC20obs, err := boot.TrustedAssetsUpdateObserverForModel(nonUC20Model, gadgetDir) + c.Assert(err, Equals, boot.ErrObserverNotApplicable) + c.Assert(nonUC20obs, IsNil) +} + +func (s *assetsSuite) TestUpdateObserverUpdateMockedWithReseal(c *C) { + // observe an update where some of the assets exist and some are new, + // followed by reseal + + d := c.MkDir() + backups := c.MkDir() + root := c.MkDir() + + // try to arrange the backups like the updater would do it + before := []byte("before") + beforeHash := "2df0976fd45ba2392dc7985cdfb7c2d096c1ea4917929dd7a0e9bffae90a443271e702663fc6a4189c1f4ab3ce7daee3" + err := ioutil.WriteFile(filepath.Join(backups, "asset.backup"), before, 0644) + c.Assert(err, IsNil) + + data := []byte("foobar") + // SHA3-384 + dataHash := "0fa8abfbdaf924ad307b74dd2ed183b9a4a398891a2f6bac8fd2db7041b77f068580f9c6c66f699b496c2da1cbcc7ed8" + err = ioutil.WriteFile(filepath.Join(d, "foobar"), data, 0644) + c.Assert(err, IsNil) + shim := []byte("shim") + shimHash := "dac0063e831d4b2e7a330426720512fc50fa315042f0bb30f9d1db73e4898dcb89119cac41fdfa62137c8931a50f9d7b" + err = ioutil.WriteFile(filepath.Join(d, "shim"), shim, 0644) + c.Assert(err, IsNil) + + m := boot.Modeenv{ + Mode: "run", + CurrentTrustedBootAssets: boot.BootAssetsMap{ + "asset": {beforeHash}, + "shim": {"shim-hash"}, + }, + CurrentTrustedRecoveryBootAssets: boot.BootAssetsMap{ + "asset": {beforeHash}, + }, + } + err = m.WriteTo("") + c.Assert(err, IsNil) + + tab := s.bootloaderWithTrustedAssets(c, []string{ + "asset", + "nested/other-asset", + "shim", + }) + tab.ManagedAssetsList = []string{ + "managed-asset", + } + + // we get an observer for UC20 + obs, _ := s.uc20UpdateObserverEncryptedSystemMockedBootloader(c) + // the list of trusted assets is obtained upfront + c.Check(tab.TrustedAssetsCalls, Equals, 2) + + res, err := obs.Observe(gadget.ContentUpdate, mockRunBootStruct, root, "asset", + &gadget.ContentChange{ + After: filepath.Join(d, "foobar"), + // original content would get backed up by the updater + Before: filepath.Join(backups, "asset.backup"), + }) + c.Assert(err, IsNil) + c.Check(res, Equals, gadget.ChangeApply) + res, err = obs.Observe(gadget.ContentUpdate, mockRunBootStruct, root, "shim", + &gadget.ContentChange{After: filepath.Join(d, "shim")}) + c.Assert(err, IsNil) + c.Check(res, Equals, gadget.ChangeApply) + // observe the recovery struct + res, err = obs.Observe(gadget.ContentUpdate, mockSeedStruct, root, "shim", + &gadget.ContentChange{After: filepath.Join(d, "shim")}) + c.Assert(err, IsNil) + c.Check(res, Equals, gadget.ChangeApply) + res, err = obs.Observe(gadget.ContentUpdate, mockSeedStruct, root, "asset", + &gadget.ContentChange{ + After: filepath.Join(d, "foobar"), + // original content + Before: filepath.Join(backups, "asset.backup"), + }) + c.Assert(err, IsNil) + c.Check(res, Equals, gadget.ChangeApply) + res, err = obs.Observe(gadget.ContentUpdate, mockSeedStruct, root, "nested/other-asset", + &gadget.ContentChange{After: filepath.Join(d, "foobar")}) + c.Assert(err, IsNil) + c.Check(res, Equals, gadget.ChangeApply) + // all files are in cache + checkContentGlob(c, filepath.Join(dirs.SnapBootAssetsDir, "trusted", "*"), []string{ + filepath.Join(dirs.SnapBootAssetsDir, "trusted", fmt.Sprintf("asset-%s", dataHash)), + filepath.Join(dirs.SnapBootAssetsDir, "trusted", fmt.Sprintf("asset-%s", beforeHash)), + filepath.Join(dirs.SnapBootAssetsDir, "trusted", fmt.Sprintf("other-asset-%s", dataHash)), + filepath.Join(dirs.SnapBootAssetsDir, "trusted", fmt.Sprintf("shim-%s", shimHash)), + }) + // check modeenv + newM, err := boot.ReadModeenv("") + c.Assert(err, IsNil) + c.Check(newM.CurrentTrustedBootAssets, DeepEquals, boot.BootAssetsMap{ + "asset": {beforeHash, dataHash}, + "shim": {"shim-hash", shimHash}, + }) + c.Check(newM.CurrentTrustedRecoveryBootAssets, DeepEquals, boot.BootAssetsMap{ + "asset": {beforeHash, dataHash}, + "shim": {shimHash}, + "other-asset": {dataHash}, + }) + + // verify that managed assets are to be preserved + res, err = obs.Observe(gadget.ContentUpdate, mockSeedStruct, root, "managed-asset", + &gadget.ContentChange{After: filepath.Join(d, "foobar")}) + c.Assert(err, IsNil) + c.Check(res, Equals, gadget.ChangeIgnore) + + // everything is set up, trigger a reseal + resealCalls := 0 + restore := boot.MockSecbootResealKeys(func(params *secboot.ResealKeysParams) error { + resealCalls++ + return nil + }) + defer restore() + + err = obs.BeforeWrite() + c.Assert(err, IsNil) + c.Check(resealCalls, Equals, 1) +} + +func (s *assetsSuite) TestUpdateObserverUpdateExistingAssetMocked(c *C) { + d := c.MkDir() + root := c.MkDir() + + tab := s.bootloaderWithTrustedAssets(c, []string{ + "asset", + "shim", + }) + tab.ManagedAssetsList = []string{ + "managed-asset", + "nested/managed-asset", + } + + data := []byte("foobar") + // SHA3-384 + dataHash := "0fa8abfbdaf924ad307b74dd2ed183b9a4a398891a2f6bac8fd2db7041b77f068580f9c6c66f699b496c2da1cbcc7ed8" + err := ioutil.WriteFile(filepath.Join(d, "foobar"), data, 0644) + c.Assert(err, IsNil) + shim := []byte("shim") + shimHash := "dac0063e831d4b2e7a330426720512fc50fa315042f0bb30f9d1db73e4898dcb89119cac41fdfa62137c8931a50f9d7b" + err = ioutil.WriteFile(filepath.Join(d, "shim"), shim, 0644) + c.Assert(err, IsNil) + + // add one file to the cache, as if the system got rebooted before + // modeenv got updated + cache := boot.NewTrustedAssetsCache(dirs.SnapBootAssetsDir) + _, err = cache.Add(filepath.Join(d, "foobar"), "trusted", "asset") + c.Assert(err, IsNil) + // file is in the cache + checkContentGlob(c, filepath.Join(dirs.SnapBootAssetsDir, "trusted", "*"), []string{ + filepath.Join(dirs.SnapBootAssetsDir, "trusted", fmt.Sprintf("asset-%s", dataHash)), + }) + + m := boot.Modeenv{ + Mode: "run", + CurrentTrustedBootAssets: boot.BootAssetsMap{ + "asset": {"asset-hash"}, + }, + CurrentTrustedRecoveryBootAssets: boot.BootAssetsMap{ + // shim with same hash is listed as trusted, but missing + // from cache + "shim": {shimHash}, + }, + } + err = m.WriteTo("") + c.Assert(err, IsNil) + + // we get an observer for UC20 + obs, _ := s.uc20UpdateObserverEncryptedSystemMockedBootloader(c) + + // observe the updates + res, err := obs.Observe(gadget.ContentUpdate, mockRunBootStruct, root, "asset", + &gadget.ContentChange{After: filepath.Join(d, "foobar")}) + c.Assert(err, IsNil) + c.Check(res, Equals, gadget.ChangeApply) + res, err = obs.Observe(gadget.ContentUpdate, mockSeedStruct, root, "asset", + &gadget.ContentChange{After: filepath.Join(d, "foobar")}) + c.Assert(err, IsNil) + c.Check(res, Equals, gadget.ChangeApply) + res, err = obs.Observe(gadget.ContentUpdate, mockSeedStruct, root, "shim", + &gadget.ContentChange{After: filepath.Join(d, "shim")}) + c.Assert(err, IsNil) + c.Check(res, Equals, gadget.ChangeApply) + // trusted assets were asked for + c.Check(tab.TrustedAssetsCalls, Equals, 2) + // file is in the cache + checkContentGlob(c, filepath.Join(dirs.SnapBootAssetsDir, "trusted", "*"), []string{ + filepath.Join(dirs.SnapBootAssetsDir, "trusted", fmt.Sprintf("asset-%s", dataHash)), + // shim was added to cache + filepath.Join(dirs.SnapBootAssetsDir, "trusted", fmt.Sprintf("shim-%s", shimHash)), + }) + // check modeenv + newM, err := boot.ReadModeenv("") + c.Assert(err, IsNil) + c.Check(newM.CurrentTrustedBootAssets, DeepEquals, boot.BootAssetsMap{ + "asset": {"asset-hash", dataHash}, + }) + c.Check(newM.CurrentTrustedRecoveryBootAssets, DeepEquals, boot.BootAssetsMap{ + "asset": {dataHash}, + "shim": {shimHash}, + }) + + // verify that managed assets are to be preserved + res, err = obs.Observe(gadget.ContentUpdate, mockSeedStruct, root, "managed-asset", + &gadget.ContentChange{After: filepath.Join(d, "foobar")}) + c.Assert(err, IsNil) + c.Check(res, Equals, gadget.ChangeIgnore) + res, err = obs.Observe(gadget.ContentUpdate, mockSeedStruct, root, "nested/managed-asset", + &gadget.ContentChange{After: filepath.Join(d, "foobar")}) + c.Assert(err, IsNil) + c.Check(res, Equals, gadget.ChangeIgnore) + + // everything is set up, trigger reseal + resealCalls := 0 + restore := boot.MockSecbootResealKeys(func(params *secboot.ResealKeysParams) error { + resealCalls++ + return nil + }) + defer restore() + + // execute before-write action + err = obs.BeforeWrite() + c.Assert(err, IsNil) + c.Check(resealCalls, Equals, 1) +} + +func (s *assetsSuite) TestUpdateObserverUpdateNothingTrackedMocked(c *C) { + d := c.MkDir() + root := c.MkDir() + + tab := s.bootloaderWithTrustedAssets(c, []string{ + "asset", + }) + + data := []byte("foobar") + // SHA3-384 + dataHash := "0fa8abfbdaf924ad307b74dd2ed183b9a4a398891a2f6bac8fd2db7041b77f068580f9c6c66f699b496c2da1cbcc7ed8" + err := ioutil.WriteFile(filepath.Join(d, "foobar"), data, 0644) + c.Assert(err, IsNil) + + m := boot.Modeenv{ + Mode: "run", + // nothing is tracked in modeenv yet + } + err = m.WriteTo("") + c.Assert(err, IsNil) + + // we get an observer for UC20 + obs, _ := s.uc20UpdateObserverEncryptedSystemMockedBootloader(c) + + // observe the updates + res, err := obs.Observe(gadget.ContentUpdate, mockRunBootStruct, root, "asset", + &gadget.ContentChange{After: filepath.Join(d, "foobar")}) + c.Assert(err, IsNil) + c.Check(res, Equals, gadget.ChangeApply) + res, err = obs.Observe(gadget.ContentUpdate, mockSeedStruct, root, "asset", + &gadget.ContentChange{After: filepath.Join(d, "foobar")}) + c.Assert(err, IsNil) + c.Check(res, Equals, gadget.ChangeApply) + // trusted assets were asked for + c.Check(tab.TrustedAssetsCalls, Equals, 2) + // file is in the cache + checkContentGlob(c, filepath.Join(dirs.SnapBootAssetsDir, "trusted", "*"), []string{ + filepath.Join(dirs.SnapBootAssetsDir, "trusted", fmt.Sprintf("asset-%s", dataHash)), + }) + // check modeenv + newM, err := boot.ReadModeenv("") + c.Assert(err, IsNil) + c.Check(newM.CurrentTrustedBootAssets, DeepEquals, boot.BootAssetsMap{ + "asset": {dataHash}, + }) + c.Check(newM.CurrentTrustedRecoveryBootAssets, DeepEquals, boot.BootAssetsMap{ + "asset": {dataHash}, + }) + + // reseal does nothing + err = obs.BeforeWrite() + c.Assert(err, IsNil) + c.Check(tab.RecoveryBootChainCalls, HasLen, 0) + c.Check(tab.BootChainKernelPath, HasLen, 0) +} + +func (s *assetsSuite) TestUpdateObserverUpdateOtherRoleStructMocked(c *C) { + d := c.MkDir() + root := c.MkDir() + + tab := s.bootloaderWithTrustedAssets(c, []string{ + "asset", + }) + + // modeenv is not set up, but the observer should not care + + // we get an observer for UC20 + obs, _ := s.uc20UpdateObserverEncryptedSystemMockedBootloader(c) + // and once again for the recovery bootloader + c.Check(tab.TrustedAssetsCalls, Equals, 2) + + // non system-boot or system-seed structure gets ignored + mockVolumeStruct := &gadget.LaidOutStructure{ + VolumeStructure: &gadget.VolumeStructure{ + Role: gadget.SystemData, + }, + } + + // observe the updates + res, err := obs.Observe(gadget.ContentUpdate, mockVolumeStruct, root, "asset", + &gadget.ContentChange{After: filepath.Join(d, "foobar")}) + c.Assert(err, IsNil) + c.Check(res, Equals, gadget.ChangeApply) +} + +func (s *assetsSuite) TestUpdateObserverUpdateTrivialErr(c *C) { + // test trivial error scenarios of the update observer + + s.stampSealedKeys(c, dirs.GlobalRootDir) + + d := c.MkDir() + root := c.MkDir() + gadgetDir := c.MkDir() + + uc20Model := boottest.MakeMockUC20Model() + + // first no bootloader + bootloader.ForceError(fmt.Errorf("bootloader fail")) + + obs, err := boot.TrustedAssetsUpdateObserverForModel(uc20Model, gadgetDir) + c.Assert(obs, IsNil) + c.Assert(err, ErrorMatches, "cannot find bootloader: bootloader fail") + + bootloader.ForceError(nil) + bl := bootloadertest.Mock("trusted", "").WithTrustedAssets() + bootloader.Force(bl) + defer bootloader.Force(nil) + + bl.TrustedAssetsErr = fmt.Errorf("fail") + obs, err = boot.TrustedAssetsUpdateObserverForModel(uc20Model, gadgetDir) + c.Assert(obs, IsNil) + c.Assert(err, ErrorMatches, `cannot list "trusted" bootloader trusted assets: fail`) + // failed listing trusted assets + c.Check(bl.TrustedAssetsCalls, Equals, 1) + + // grab a new bootloader mock + bl = bootloadertest.Mock("trusted", "").WithTrustedAssets() + bootloader.Force(bl) + bl.TrustedAssetsList = []string{"asset"} + + obs, err = boot.TrustedAssetsUpdateObserverForModel(uc20Model, gadgetDir) + c.Assert(err, IsNil) + c.Assert(obs, NotNil) + c.Check(bl.TrustedAssetsCalls, Equals, 2) + + // no modeenv + res, err := obs.Observe(gadget.ContentUpdate, mockRunBootStruct, root, "asset", + &gadget.ContentChange{After: filepath.Join(d, "foobar")}) + c.Assert(err, ErrorMatches, `cannot load modeenv: .* no such file or directory`) + c.Check(res, Equals, gadget.ChangeAbort) + res, err = obs.Observe(gadget.ContentUpdate, mockSeedStruct, root, "asset", + &gadget.ContentChange{After: filepath.Join(d, "foobar")}) + c.Assert(err, ErrorMatches, `cannot load modeenv: .* no such file or directory`) + c.Check(res, Equals, gadget.ChangeAbort) + + m := boot.Modeenv{ + Mode: "run", + } + err = m.WriteTo("") + c.Assert(err, IsNil) + + // no source file, hash will fail + res, err = obs.Observe(gadget.ContentUpdate, mockRunBootStruct, root, "asset", + &gadget.ContentChange{After: filepath.Join(d, "foobar")}) + c.Assert(err, ErrorMatches, `cannot open asset file: .*/foobar: no such file or directory`) + c.Check(res, Equals, gadget.ChangeAbort) + res, err = obs.Observe(gadget.ContentUpdate, mockRunBootStruct, root, "asset", + &gadget.ContentChange{Before: filepath.Join(d, "before"), After: filepath.Join(d, "foobar")}) + c.Assert(err, ErrorMatches, `cannot open asset file: .*/before: no such file or directory`) + c.Check(res, Equals, gadget.ChangeAbort) + res, err = obs.Observe(gadget.ContentUpdate, mockSeedStruct, root, "asset", + &gadget.ContentChange{After: filepath.Join(d, "foobar")}) + c.Assert(err, ErrorMatches, `cannot open asset file: .*/foobar: no such file or directory`) + c.Check(res, Equals, gadget.ChangeAbort) +} + +func (s *assetsSuite) TestUpdateObserverUpdateRepeatedAssetErr(c *C) { + d := c.MkDir() + root := c.MkDir() + + bl := bootloadertest.Mock("trusted", "").WithTrustedAssets() + bootloader.Force(bl) + defer bootloader.Force(nil) + bl.TrustedAssetsList = []string{"asset"} + + obs, _ := s.uc20UpdateObserverEncryptedSystemMockedBootloader(c) + + // we are already tracking 2 assets, this is an unexpected state for observing content updates + m := boot.Modeenv{ + Mode: "run", + CurrentTrustedBootAssets: boot.BootAssetsMap{ + "asset": {"one", "two"}, + }, + CurrentTrustedRecoveryBootAssets: boot.BootAssetsMap{ + "asset": {"one", "two"}, + }, + } + err := m.WriteTo("") + c.Assert(err, IsNil) + + // and the source file + err = ioutil.WriteFile(filepath.Join(d, "foobar"), nil, 0644) + c.Assert(err, IsNil) + + res, err := obs.Observe(gadget.ContentUpdate, mockRunBootStruct, root, "asset", + &gadget.ContentChange{After: filepath.Join(d, "foobar")}) + c.Assert(err, ErrorMatches, `cannot reuse asset name "asset"`) + c.Check(res, Equals, gadget.ChangeAbort) + res, err = obs.Observe(gadget.ContentUpdate, mockSeedStruct, root, "asset", + &gadget.ContentChange{After: filepath.Join(d, "foobar")}) + c.Assert(err, ErrorMatches, `cannot reuse asset name "asset"`) + c.Check(res, Equals, gadget.ChangeAbort) +} + +func (s *assetsSuite) TestUpdateObserverUpdateAfterSuccessfulBootMocked(c *C) { + //observe an update in a scenario when a mid-gadget-update reboot + //happened and we have successfully booted with new assets only, but the + //update is incomplete and gets started again + + d := c.MkDir() + backups := c.MkDir() + root := c.MkDir() + + // try to arrange the backups like the updater would do it + before := []byte("before") + beforeHash := "2df0976fd45ba2392dc7985cdfb7c2d096c1ea4917929dd7a0e9bffae90a443271e702663fc6a4189c1f4ab3ce7daee3" + err := ioutil.WriteFile(filepath.Join(backups, "asset.backup"), before, 0644) + c.Assert(err, IsNil) + + data := []byte("foobar") + // SHA3-384 + dataHash := "0fa8abfbdaf924ad307b74dd2ed183b9a4a398891a2f6bac8fd2db7041b77f068580f9c6c66f699b496c2da1cbcc7ed8" + err = ioutil.WriteFile(filepath.Join(d, "foobar"), data, 0644) + c.Assert(err, IsNil) + + // pretend we rebooted mid update and have successfully booted with the + // new assets already, the old asset may have been dropped from the cache already + cache := boot.NewTrustedAssetsCache(dirs.SnapBootAssetsDir) + _, err = cache.Add(filepath.Join(d, "foobar"), "trusted", "asset") + c.Assert(err, IsNil) + // file is in the cache + checkContentGlob(c, filepath.Join(dirs.SnapBootAssetsDir, "trusted", "*"), []string{ + filepath.Join(dirs.SnapBootAssetsDir, "trusted", fmt.Sprintf("asset-%s", dataHash)), + }) + // and similarly, only the new asset in modeenv + m := boot.Modeenv{ + Mode: "run", + CurrentTrustedBootAssets: boot.BootAssetsMap{ + "asset": {dataHash}, + }, + CurrentTrustedRecoveryBootAssets: boot.BootAssetsMap{ + "asset": {dataHash}, + }, + } + err = m.WriteTo("") + c.Assert(err, IsNil) + + s.bootloaderWithTrustedAssets(c, []string{ + "asset", + }) + + // we get an observer for UC20 + obs, _ := s.uc20UpdateObserverEncryptedSystemMockedBootloader(c) + + res, err := obs.Observe(gadget.ContentUpdate, mockRunBootStruct, root, "asset", + &gadget.ContentChange{ + After: filepath.Join(d, "foobar"), + // original content would get backed up by the updater + Before: filepath.Join(backups, "asset.backup"), + }) + c.Assert(err, IsNil) + c.Check(res, Equals, gadget.ChangeApply) + res, err = obs.Observe(gadget.ContentUpdate, mockSeedStruct, root, "asset", + &gadget.ContentChange{ + After: filepath.Join(d, "foobar"), + // original content + Before: filepath.Join(backups, "asset.backup"), + }) + c.Assert(err, IsNil) + c.Check(res, Equals, gadget.ChangeApply) + + // all files are in cache + checkContentGlob(c, filepath.Join(dirs.SnapBootAssetsDir, "trusted", "*"), []string{ + filepath.Join(dirs.SnapBootAssetsDir, "trusted", fmt.Sprintf("asset-%s", dataHash)), + filepath.Join(dirs.SnapBootAssetsDir, "trusted", fmt.Sprintf("asset-%s", beforeHash)), + }) + // check modeenv + newM, err := boot.ReadModeenv("") + c.Assert(err, IsNil) + c.Check(newM.CurrentTrustedBootAssets, DeepEquals, boot.BootAssetsMap{ + // original asset is restored, listed first + "asset": {beforeHash, dataHash}, + }) + c.Check(newM.CurrentTrustedRecoveryBootAssets, DeepEquals, boot.BootAssetsMap{ + // same here + "asset": {beforeHash, dataHash}, + }) +} + +func (s *assetsSuite) TestUpdateObserverRollbackModeenvManipulationMocked(c *C) { + root := c.MkDir() + rootSeed := c.MkDir() + d := c.MkDir() + backups := c.MkDir() + + tab := s.bootloaderWithTrustedAssets(c, []string{ + "asset", + "nested/other-asset", + "shim", + }) + + data := []byte("foobar") + // SHA3-384 + dataHash := "0fa8abfbdaf924ad307b74dd2ed183b9a4a398891a2f6bac8fd2db7041b77f068580f9c6c66f699b496c2da1cbcc7ed8" + // file exists in both run and seed bootloader rootdirs + c.Assert(ioutil.WriteFile(filepath.Join(root, "asset"), data, 0644), IsNil) + c.Assert(ioutil.WriteFile(filepath.Join(rootSeed, "asset"), data, 0644), IsNil) + // and in the gadget + c.Assert(ioutil.WriteFile(filepath.Join(d, "asset"), data, 0644), IsNil) + // would be listed as Before + c.Assert(ioutil.WriteFile(filepath.Join(backups, "asset.backup"), data, 0644), IsNil) + + shim := []byte("shim") + shimHash := "dac0063e831d4b2e7a330426720512fc50fa315042f0bb30f9d1db73e4898dcb89119cac41fdfa62137c8931a50f9d7b" + // only exists in seed bootloader rootdir + c.Assert(ioutil.WriteFile(filepath.Join(rootSeed, "shim"), shim, 0644), IsNil) + // and in the gadget + c.Assert(ioutil.WriteFile(filepath.Join(d, "shim"), shim, 0644), IsNil) + // would be listed as Before + c.Assert(ioutil.WriteFile(filepath.Join(backups, "shim.backup"), data, 0644), IsNil) + + c.Assert(os.MkdirAll(filepath.Join(dirs.SnapBootAssetsDir, "trusted"), 0755), IsNil) + // mock some files in cache + for _, name := range []string{ + fmt.Sprintf("asset-%s", dataHash), + fmt.Sprintf("shim-%s", shimHash), + "shim-newshimhash", + "asset-newhash", + "other-asset-newotherhash", + } { + err := ioutil.WriteFile(filepath.Join(dirs.SnapBootAssetsDir, "trusted", name), nil, 0644) + c.Assert(err, IsNil) + } + + // we get an observer for UC20 + obs, _ := s.uc20UpdateObserverEncryptedSystemMockedBootloader(c) + // the list of trusted assets is obtained upfront + c.Check(tab.TrustedAssetsCalls, Equals, 2) + + m := boot.Modeenv{ + Mode: "run", + CurrentTrustedBootAssets: boot.BootAssetsMap{ + // new version added during update + "asset": {dataHash, "newhash"}, + }, + CurrentTrustedRecoveryBootAssets: boot.BootAssetsMap{ + // no new version added during update + "asset": {dataHash}, + // new version added during update + "shim": {shimHash, "newshimhash"}, + // completely new file + "other-asset": {"newotherhash"}, + }, + } + err := m.WriteTo("") + c.Assert(err, IsNil) + + res, err := obs.Observe(gadget.ContentRollback, mockRunBootStruct, root, "asset", + &gadget.ContentChange{ + After: filepath.Join(d, "asset"), + Before: filepath.Join(backups, "asset.backup"), + }) + c.Assert(err, IsNil) + c.Check(res, Equals, gadget.ChangeApply) + res, err = obs.Observe(gadget.ContentRollback, mockRunBootStruct, root, "shim", + &gadget.ContentChange{ + After: filepath.Join(d, "shim"), + // no before content, new file + }) + c.Assert(err, IsNil) + c.Check(res, Equals, gadget.ChangeApply) + // observe the recovery struct + res, err = obs.Observe(gadget.ContentRollback, mockSeedStruct, rootSeed, "shim", + &gadget.ContentChange{ + After: filepath.Join(d, "shim"), + Before: filepath.Join(backups, "shim.backup"), + }) + c.Assert(err, IsNil) + c.Check(res, Equals, gadget.ChangeApply) + res, err = obs.Observe(gadget.ContentRollback, mockSeedStruct, rootSeed, "asset", + &gadget.ContentChange{ + After: filepath.Join(d, "asset"), + Before: filepath.Join(backups, "asset.backup"), + }) + c.Assert(err, IsNil) + c.Check(res, Equals, gadget.ChangeApply) + res, err = obs.Observe(gadget.ContentRollback, mockSeedStruct, rootSeed, "nested/other-asset", + &gadget.ContentChange{ + After: filepath.Join(d, "asset"), + }) + c.Assert(err, IsNil) + c.Check(res, Equals, gadget.ChangeApply) + // all files are in cache + checkContentGlob(c, filepath.Join(dirs.SnapBootAssetsDir, "trusted", "*"), []string{ + filepath.Join(dirs.SnapBootAssetsDir, "trusted", fmt.Sprintf("asset-%s", dataHash)), + filepath.Join(dirs.SnapBootAssetsDir, "trusted", fmt.Sprintf("shim-%s", shimHash)), + }) + // check modeenv + newM, err := boot.ReadModeenv("") + c.Assert(err, IsNil) + c.Check(newM.CurrentTrustedBootAssets, DeepEquals, boot.BootAssetsMap{ + "asset": {dataHash}, + }) + c.Check(newM.CurrentTrustedRecoveryBootAssets, DeepEquals, boot.BootAssetsMap{ + "asset": {dataHash}, + "shim": {shimHash}, + }) +} + +func (s *assetsSuite) TestUpdateObserverRollbackFileSanity(c *C) { + root := c.MkDir() + + tab := s.bootloaderWithTrustedAssets(c, []string{"asset"}) + + // we get an observer for UC20 + obs, _ := s.uc20UpdateObserverEncryptedSystemMockedBootloader(c) + // list of trusted assets is obtained upfront + c.Check(tab.TrustedAssetsCalls, Equals, 2) + + // sane state of modeenv before rollback + m := boot.Modeenv{ + Mode: "run", + CurrentTrustedBootAssets: boot.BootAssetsMap{ + // only one hash is listed, indicating it's a new file + "asset": {"newhash"}, + }, + CurrentTrustedRecoveryBootAssets: boot.BootAssetsMap{ + // same thing + "asset": {"newhash"}, + }, + } + err := m.WriteTo("") + c.Assert(err, IsNil) + // file does not exist on disk + res, err := obs.Observe(gadget.ContentRollback, mockRunBootStruct, root, "asset", + &gadget.ContentChange{}) + c.Assert(err, IsNil) + c.Check(res, Equals, gadget.ChangeApply) + // observe the recovery struct + res, err = obs.Observe(gadget.ContentRollback, mockSeedStruct, root, "asset", + &gadget.ContentChange{}) + c.Assert(err, IsNil) + c.Check(res, Equals, gadget.ChangeApply) + // check modeenv + newM, err := boot.ReadModeenv("") + c.Assert(err, IsNil) + c.Check(newM.CurrentTrustedBootAssets, HasLen, 0) + c.Check(newM.CurrentTrustedRecoveryBootAssets, HasLen, 0) + + // new observer + obs, _ = s.uc20UpdateObserverEncryptedSystemMockedBootloader(c) + m = boot.Modeenv{ + Mode: "run", + CurrentTrustedBootAssets: boot.BootAssetsMap{ + // only one hash is listed, indicating it's a new file + "asset": {"newhash", "bogushash"}, + }, + CurrentTrustedRecoveryBootAssets: boot.BootAssetsMap{ + // same thing + "asset": {"newhash", "bogushash"}, + }, + } + err = m.WriteTo("") + c.Assert(err, IsNil) + // again, file does not exist on disk, but we expected it to be there + res, err = obs.Observe(gadget.ContentRollback, mockRunBootStruct, root, "asset", + &gadget.ContentChange{}) + c.Assert(err, ErrorMatches, `tracked asset "asset" is unexpectedly missing from disk`) + c.Check(res, Equals, gadget.ChangeAbort) + res, err = obs.Observe(gadget.ContentRollback, mockSeedStruct, root, "asset", + &gadget.ContentChange{}) + c.Assert(err, ErrorMatches, `tracked asset "asset" is unexpectedly missing from disk`) + c.Check(res, Equals, gadget.ChangeAbort) + + // create the file which will fail checksum check + err = ioutil.WriteFile(filepath.Join(root, "asset"), nil, 0644) + c.Assert(err, IsNil) + // once more, the file exists on disk, but has unexpected checksum + res, err = obs.Observe(gadget.ContentRollback, mockRunBootStruct, root, "asset", + &gadget.ContentChange{}) + c.Assert(err, ErrorMatches, `unexpected content of existing asset "asset"`) + c.Check(res, Equals, gadget.ChangeAbort) + res, err = obs.Observe(gadget.ContentRollback, mockSeedStruct, root, "asset", + &gadget.ContentChange{}) + c.Assert(err, ErrorMatches, `unexpected content of existing asset "asset"`) + c.Check(res, Equals, gadget.ChangeAbort) +} + +func (s *assetsSuite) TestUpdateObserverUpdateRollbackGrub(c *C) { + // exercise a full update/rollback cycle with grub + + gadgetDir := c.MkDir() + bootDir := c.MkDir() + seedDir := c.MkDir() + + // prepare a marker for grub bootloader + c.Assert(ioutil.WriteFile(filepath.Join(gadgetDir, "grub.conf"), nil, 0644), IsNil) + + // we get an observer for UC20 + s.stampSealedKeys(c, dirs.GlobalRootDir) + obs, _ := s.uc20UpdateObserver(c, gadgetDir) + + cache := boot.NewTrustedAssetsCache(dirs.SnapBootAssetsDir) + + for _, dir := range []struct { + root string + fileWithContent [][]string + addContentToCache bool + }{ + { + // data of boot bootloader + root: bootDir, + // SHA3-384: 0d0c6522fcc813770f2bb9ca68ad3b4f0ccc6b4bfbd2e8497030079e6146f92177ad8f6f83d96ab61d7d42f5228a4389 + fileWithContent: [][]string{ + {"EFI/boot/grubx64.efi", "grub efi"}, + }, + addContentToCache: true, + }, { + // data of seed bootloader + root: seedDir, + fileWithContent: [][]string{ + // SHA3-384: 6c3e6fc78ade5aadc5f9f0603a127346cc174436eb5e0188e108a376c3ba4d8951c460a8f51674e797c06951f74cb10d + {"EFI/boot/grubx64.efi", "recovery grub efi"}, + // SHA3-384: c0437507ac094a7e9c699725cc0a4726cd10799af9eb79bbeaa136c2773163c80432295c2a04d3aa2ddd535ce8f1a12b + {"EFI/boot/bootx64.efi", "recovery shim efi"}, + }, + addContentToCache: true, + }, { + // gadget content + root: gadgetDir, + fileWithContent: [][]string{ + // SHA3-384: f9554844308e89b565c1cdbcbdb9b09b8210dd2f1a11cb3b361de0a59f780ae3d4bd6941729a60e0f8ce15b2edef605d + {"grubx64.efi", "new grub efi"}, + // SHA3-384: cc0663cc7e6c7ada990261c3ff1d72da001dc02451558716422d3d2443b8789463363c9ff0cd1b853c6ced3e8e7dc39d + {"bootx64.efi", "new recovery shim efi"}, + {"grub.conf", "grub from gadget"}, + }, + }, + // just the markers + { + root: bootDir, + fileWithContent: [][]string{ + {"EFI/ubuntu/grub.cfg", "grub marker"}, + }, + }, { + root: seedDir, + fileWithContent: [][]string{ + {"EFI/ubuntu/grub.cfg", "grub marker"}, + }, + }, + } { + for _, f := range dir.fileWithContent { + p := filepath.Join(dir.root, f[0]) + err := os.MkdirAll(filepath.Dir(p), 0755) + c.Assert(err, IsNil) + err = ioutil.WriteFile(p, []byte(f[1]), 0644) + c.Assert(err, IsNil) + if dir.addContentToCache { + _, err = cache.Add(p, "grub", filepath.Base(p)) + c.Assert(err, IsNil) + } + } + } + cacheContentBefore := []string{ + // recovery shim + filepath.Join(dirs.SnapBootAssetsDir, "grub", "bootx64.efi-c0437507ac094a7e9c699725cc0a4726cd10799af9eb79bbeaa136c2773163c80432295c2a04d3aa2ddd535ce8f1a12b"), + // boot bootloader + filepath.Join(dirs.SnapBootAssetsDir, "grub", "grubx64.efi-0d0c6522fcc813770f2bb9ca68ad3b4f0ccc6b4bfbd2e8497030079e6146f92177ad8f6f83d96ab61d7d42f5228a4389"), + // recovery bootloader + filepath.Join(dirs.SnapBootAssetsDir, "grub", "grubx64.efi-6c3e6fc78ade5aadc5f9f0603a127346cc174436eb5e0188e108a376c3ba4d8951c460a8f51674e797c06951f74cb10d"), + } + checkContentGlob(c, filepath.Join(dirs.SnapBootAssetsDir, "grub", "*"), cacheContentBefore) + // current files are tracked + m := boot.Modeenv{ + Mode: "run", + CurrentTrustedBootAssets: boot.BootAssetsMap{ + "grubx64.efi": {"0d0c6522fcc813770f2bb9ca68ad3b4f0ccc6b4bfbd2e8497030079e6146f92177ad8f6f83d96ab61d7d42f5228a4389"}, + }, + CurrentTrustedRecoveryBootAssets: boot.BootAssetsMap{ + "grubx64.efi": {"6c3e6fc78ade5aadc5f9f0603a127346cc174436eb5e0188e108a376c3ba4d8951c460a8f51674e797c06951f74cb10d"}, + "bootx64.efi": {"c0437507ac094a7e9c699725cc0a4726cd10799af9eb79bbeaa136c2773163c80432295c2a04d3aa2ddd535ce8f1a12b"}, + }, + } + err := m.WriteTo("") + c.Assert(err, IsNil) + + // updates first + res, err := obs.Observe(gadget.ContentUpdate, mockRunBootStruct, bootDir, "EFI/boot/grubx64.efi", + &gadget.ContentChange{After: filepath.Join(gadgetDir, "grubx64.efi")}) + c.Assert(err, IsNil) + c.Check(res, Equals, gadget.ChangeApply) + res, err = obs.Observe(gadget.ContentUpdate, mockSeedStruct, seedDir, "EFI/boot/grubx64.efi", + &gadget.ContentChange{After: filepath.Join(gadgetDir, "grubx64.efi")}) + c.Assert(err, IsNil) + c.Check(res, Equals, gadget.ChangeApply) + res, err = obs.Observe(gadget.ContentUpdate, mockSeedStruct, seedDir, "EFI/boot/bootx64.efi", + &gadget.ContentChange{After: filepath.Join(gadgetDir, "bootx64.efi")}) + c.Assert(err, IsNil) + c.Check(res, Equals, gadget.ChangeApply) + // grub.cfg on ubuntu-seed and ubuntu-boot is managed by snapd + res, err = obs.Observe(gadget.ContentUpdate, mockRunBootStruct, seedDir, "EFI/ubuntu/grub.cfg", + &gadget.ContentChange{After: filepath.Join(gadgetDir, "grub.conf")}) + c.Assert(err, IsNil) + c.Check(res, Equals, gadget.ChangeIgnore) + res, err = obs.Observe(gadget.ContentUpdate, mockSeedStruct, seedDir, "EFI/ubuntu/grub.cfg", + &gadget.ContentChange{After: filepath.Join(gadgetDir, "grub.conf")}) + c.Assert(err, IsNil) + c.Check(res, Equals, gadget.ChangeIgnore) + + // verify cache contents + checkContentGlob(c, filepath.Join(dirs.SnapBootAssetsDir, "grub", "*"), []string{ + // recovery shim + filepath.Join(dirs.SnapBootAssetsDir, "grub", "bootx64.efi-c0437507ac094a7e9c699725cc0a4726cd10799af9eb79bbeaa136c2773163c80432295c2a04d3aa2ddd535ce8f1a12b"), + // new recovery shim + filepath.Join(dirs.SnapBootAssetsDir, "grub", "bootx64.efi-cc0663cc7e6c7ada990261c3ff1d72da001dc02451558716422d3d2443b8789463363c9ff0cd1b853c6ced3e8e7dc39d"), + // boot bootloader + filepath.Join(dirs.SnapBootAssetsDir, "grub", "grubx64.efi-0d0c6522fcc813770f2bb9ca68ad3b4f0ccc6b4bfbd2e8497030079e6146f92177ad8f6f83d96ab61d7d42f5228a4389"), + // recovery bootloader + filepath.Join(dirs.SnapBootAssetsDir, "grub", "grubx64.efi-6c3e6fc78ade5aadc5f9f0603a127346cc174436eb5e0188e108a376c3ba4d8951c460a8f51674e797c06951f74cb10d"), + // new recovery and boot bootloader + filepath.Join(dirs.SnapBootAssetsDir, "grub", "grubx64.efi-f9554844308e89b565c1cdbcbdb9b09b8210dd2f1a11cb3b361de0a59f780ae3d4bd6941729a60e0f8ce15b2edef605d"), + }) + + // and modeenv contents + newM, err := boot.ReadModeenv("") + c.Assert(err, IsNil) + c.Check(newM.CurrentTrustedBootAssets, DeepEquals, boot.BootAssetsMap{ + "grubx64.efi": { + // old hash + "0d0c6522fcc813770f2bb9ca68ad3b4f0ccc6b4bfbd2e8497030079e6146f92177ad8f6f83d96ab61d7d42f5228a4389", + // update + "f9554844308e89b565c1cdbcbdb9b09b8210dd2f1a11cb3b361de0a59f780ae3d4bd6941729a60e0f8ce15b2edef605d", + }, + }) + c.Check(newM.CurrentTrustedRecoveryBootAssets, DeepEquals, boot.BootAssetsMap{ + "grubx64.efi": { + // old hash + "6c3e6fc78ade5aadc5f9f0603a127346cc174436eb5e0188e108a376c3ba4d8951c460a8f51674e797c06951f74cb10d", + // update + "f9554844308e89b565c1cdbcbdb9b09b8210dd2f1a11cb3b361de0a59f780ae3d4bd6941729a60e0f8ce15b2edef605d", + }, + "bootx64.efi": { + // old hash + "c0437507ac094a7e9c699725cc0a4726cd10799af9eb79bbeaa136c2773163c80432295c2a04d3aa2ddd535ce8f1a12b", + // update + "cc0663cc7e6c7ada990261c3ff1d72da001dc02451558716422d3d2443b8789463363c9ff0cd1b853c6ced3e8e7dc39d", + }, + }) + + // hiya, update failed, pretend we do a rollback, files on disk are as + // if they were restored + + res, err = obs.Observe(gadget.ContentRollback, mockRunBootStruct, bootDir, "EFI/boot/grubx64.efi", + &gadget.ContentChange{}) + c.Assert(err, IsNil) + c.Check(res, Equals, gadget.ChangeApply) + res, err = obs.Observe(gadget.ContentRollback, mockSeedStruct, seedDir, "EFI/boot/grubx64.efi", + &gadget.ContentChange{}) + c.Assert(err, IsNil) + c.Check(res, Equals, gadget.ChangeApply) + res, err = obs.Observe(gadget.ContentRollback, mockSeedStruct, seedDir, "EFI/boot/bootx64.efi", + &gadget.ContentChange{}) + c.Assert(err, IsNil) + c.Check(res, Equals, gadget.ChangeApply) + + // modeenv is back to the initial state + afterRollbackM, err := boot.ReadModeenv("") + c.Assert(err, IsNil) + c.Check(afterRollbackM.CurrentTrustedBootAssets, DeepEquals, m.CurrentTrustedBootAssets) + c.Check(afterRollbackM.CurrentTrustedRecoveryBootAssets, DeepEquals, m.CurrentTrustedRecoveryBootAssets) + // and cache is back to the same state as before + checkContentGlob(c, filepath.Join(dirs.SnapBootAssetsDir, "grub", "*"), cacheContentBefore) +} + +func (s *assetsSuite) TestUpdateObserverCanceledSimpleAfterBackupMocked(c *C) { + d := c.MkDir() + root := c.MkDir() + + m := boot.Modeenv{ + Mode: "run", + CurrentTrustedBootAssets: boot.BootAssetsMap{ + "asset": {"assethash"}, + "shim": {"shimhash"}, + }, + CurrentTrustedRecoveryBootAssets: boot.BootAssetsMap{ + "asset": {"recoveryhash"}, + }, + } + err := m.WriteTo("") + c.Assert(err, IsNil) + + // mock some files in cache + c.Assert(os.MkdirAll(filepath.Join(dirs.SnapBootAssetsDir, "trusted"), 0755), IsNil) + for _, name := range []string{ + "shim-shimhash", + "asset-assethash", + "asset-recoveryhash", + } { + err = ioutil.WriteFile(filepath.Join(dirs.SnapBootAssetsDir, "trusted", name), nil, 0644) + c.Assert(err, IsNil) + } + + s.bootloaderWithTrustedAssets(c, []string{"asset", "shim"}) + + // we get an observer for UC20 + obs, _ := s.uc20UpdateObserverEncryptedSystemMockedBootloader(c) + + data := []byte("foobar") + // SHA3-384 + dataHash := "0fa8abfbdaf924ad307b74dd2ed183b9a4a398891a2f6bac8fd2db7041b77f068580f9c6c66f699b496c2da1cbcc7ed8" + err = ioutil.WriteFile(filepath.Join(d, "foobar"), data, 0644) + c.Assert(err, IsNil) + shim := []byte("shim") + shimHash := "dac0063e831d4b2e7a330426720512fc50fa315042f0bb30f9d1db73e4898dcb89119cac41fdfa62137c8931a50f9d7b" + err = ioutil.WriteFile(filepath.Join(d, "shim"), shim, 0644) + c.Assert(err, IsNil) + + res, err := obs.Observe(gadget.ContentUpdate, mockRunBootStruct, root, "asset", + &gadget.ContentChange{After: filepath.Join(d, "foobar")}) + c.Assert(err, IsNil) + c.Check(res, Equals, gadget.ChangeApply) + res, err = obs.Observe(gadget.ContentUpdate, mockRunBootStruct, root, "shim", + &gadget.ContentChange{After: filepath.Join(d, "shim")}) + c.Assert(err, IsNil) + c.Check(res, Equals, gadget.ChangeApply) + // observe the recovery struct + res, err = obs.Observe(gadget.ContentUpdate, mockSeedStruct, root, "shim", + &gadget.ContentChange{After: filepath.Join(d, "shim")}) + c.Assert(err, IsNil) + c.Check(res, Equals, gadget.ChangeApply) + res, err = obs.Observe(gadget.ContentUpdate, mockSeedStruct, root, "asset", + &gadget.ContentChange{After: filepath.Join(d, "foobar")}) + c.Assert(err, IsNil) + c.Check(res, Equals, gadget.ChangeApply) + // files are in cache + checkContentGlob(c, filepath.Join(dirs.SnapBootAssetsDir, "trusted", "*"), []string{ + filepath.Join(dirs.SnapBootAssetsDir, "trusted", fmt.Sprintf("asset-%s", dataHash)), + filepath.Join(dirs.SnapBootAssetsDir, "trusted", "asset-assethash"), + filepath.Join(dirs.SnapBootAssetsDir, "trusted", "asset-recoveryhash"), + filepath.Join(dirs.SnapBootAssetsDir, "trusted", fmt.Sprintf("shim-%s", shimHash)), + filepath.Join(dirs.SnapBootAssetsDir, "trusted", "shim-shimhash"), + }) + // check modeenv + newM, err := boot.ReadModeenv("") + c.Assert(err, IsNil) + c.Check(newM.CurrentTrustedBootAssets, DeepEquals, boot.BootAssetsMap{ + "asset": {"assethash", dataHash}, + "shim": {"shimhash", shimHash}, + }) + c.Check(newM.CurrentTrustedRecoveryBootAssets, DeepEquals, boot.BootAssetsMap{ + "asset": {"recoveryhash", dataHash}, + "shim": {shimHash}, + }) + resealCalls := 0 + restore := boot.MockSecbootResealKeys(func(params *secboot.ResealKeysParams) error { + resealCalls++ + return nil + }) + defer restore() + + // update is canceled + err = obs.Canceled() + c.Assert(err, IsNil) + // modeenv is back to initial state + afterCancelM, err := boot.ReadModeenv("") + c.Assert(err, IsNil) + c.Check(afterCancelM.CurrentTrustedBootAssets, DeepEquals, m.CurrentTrustedBootAssets) + c.Check(afterCancelM.CurrentTrustedRecoveryBootAssets, DeepEquals, m.CurrentTrustedRecoveryBootAssets) + // unused assets were dropped + checkContentGlob(c, filepath.Join(dirs.SnapBootAssetsDir, "trusted", "*"), []string{ + filepath.Join(dirs.SnapBootAssetsDir, "trusted", "asset-assethash"), + filepath.Join(dirs.SnapBootAssetsDir, "trusted", "asset-recoveryhash"), + filepath.Join(dirs.SnapBootAssetsDir, "trusted", "shim-shimhash"), + }) + + c.Check(resealCalls, Equals, 1) +} + +func (s *assetsSuite) TestUpdateObserverCanceledPartiallyUsedMocked(c *C) { + // cancel an update where one of the assets is already used and canceling does not remove it from the cache + + d := c.MkDir() + root := c.MkDir() + + s.bootloaderWithTrustedAssets(c, []string{"asset", "shim"}) + + data := []byte("foobar") + // SHA3-384 + dataHash := "0fa8abfbdaf924ad307b74dd2ed183b9a4a398891a2f6bac8fd2db7041b77f068580f9c6c66f699b496c2da1cbcc7ed8" + err := ioutil.WriteFile(filepath.Join(d, "foobar"), data, 0644) + c.Assert(err, IsNil) + shim := []byte("shim") + shimHash := "dac0063e831d4b2e7a330426720512fc50fa315042f0bb30f9d1db73e4898dcb89119cac41fdfa62137c8931a50f9d7b" + err = ioutil.WriteFile(filepath.Join(d, "shim"), shim, 0644) + c.Assert(err, IsNil) + + // mock some files in cache + c.Assert(os.MkdirAll(filepath.Join(dirs.SnapBootAssetsDir, "trusted"), 0755), IsNil) + for _, name := range []string{ + "shim-shimhash", + "asset-assethash", + fmt.Sprintf("shim-%s", shimHash), + } { + err = ioutil.WriteFile(filepath.Join(dirs.SnapBootAssetsDir, "trusted", name), nil, 0644) + c.Assert(err, IsNil) + } + + // we get an observer for UC20 + obs, _ := s.uc20UpdateObserverEncryptedSystemMockedBootloader(c) + + m := boot.Modeenv{ + Mode: "run", + CurrentTrustedBootAssets: boot.BootAssetsMap{ + "asset": {"assethash"}, + "shim": {"shimhash"}, + }, + CurrentTrustedRecoveryBootAssets: boot.BootAssetsMap{ + "shim": {shimHash}, + }, + } + err = m.WriteTo("") + c.Assert(err, IsNil) + + res, err := obs.Observe(gadget.ContentUpdate, mockRunBootStruct, root, "asset", + &gadget.ContentChange{After: filepath.Join(d, "foobar")}) + c.Assert(err, IsNil) + c.Check(res, Equals, gadget.ChangeApply) + res, err = obs.Observe(gadget.ContentUpdate, mockRunBootStruct, root, "shim", + &gadget.ContentChange{After: filepath.Join(d, "shim")}) + c.Assert(err, IsNil) + c.Check(res, Equals, gadget.ChangeApply) + // observe the recovery struct + // XXX: shim is not updated + res, err = obs.Observe(gadget.ContentUpdate, mockSeedStruct, root, "asset", + &gadget.ContentChange{After: filepath.Join(d, "foobar")}) + c.Assert(err, IsNil) + c.Check(res, Equals, gadget.ChangeApply) + // files are in cache + checkContentGlob(c, filepath.Join(dirs.SnapBootAssetsDir, "trusted", "*"), []string{ + filepath.Join(dirs.SnapBootAssetsDir, "trusted", fmt.Sprintf("asset-%s", dataHash)), + filepath.Join(dirs.SnapBootAssetsDir, "trusted", "asset-assethash"), + filepath.Join(dirs.SnapBootAssetsDir, "trusted", fmt.Sprintf("shim-%s", shimHash)), + filepath.Join(dirs.SnapBootAssetsDir, "trusted", "shim-shimhash"), + }) + // check modeenv + newM, err := boot.ReadModeenv("") + c.Assert(err, IsNil) + c.Check(newM.CurrentTrustedBootAssets, DeepEquals, boot.BootAssetsMap{ + "asset": {"assethash", dataHash}, + "shim": {"shimhash", shimHash}, + }) + c.Check(newM.CurrentTrustedRecoveryBootAssets, DeepEquals, boot.BootAssetsMap{ + "asset": {dataHash}, + "shim": {shimHash}, + }) + // update is canceled + err = obs.Canceled() + c.Assert(err, IsNil) + // modeenv is back to initial state + afterCancelM, err := boot.ReadModeenv("") + c.Assert(err, IsNil) + c.Check(afterCancelM.CurrentTrustedBootAssets, DeepEquals, m.CurrentTrustedBootAssets) + c.Check(afterCancelM.CurrentTrustedRecoveryBootAssets, DeepEquals, m.CurrentTrustedRecoveryBootAssets) + // unused assets were dropped + checkContentGlob(c, filepath.Join(dirs.SnapBootAssetsDir, "trusted", "*"), []string{ + filepath.Join(dirs.SnapBootAssetsDir, "trusted", "asset-assethash"), + filepath.Join(dirs.SnapBootAssetsDir, "trusted", fmt.Sprintf("shim-%s", shimHash)), + filepath.Join(dirs.SnapBootAssetsDir, "trusted", "shim-shimhash"), + }) +} + +func (s *assetsSuite) TestUpdateObserverCanceledNoActionsMocked(c *C) { + // make sure that when no ContentUpdate actions were registered, or some + // were registered for one bootloader, but not the other, is not + // triggering unwanted behavior on cancel + + d := c.MkDir() + root := c.MkDir() + + m := boot.Modeenv{ + Mode: "run", + CurrentTrustedBootAssets: boot.BootAssetsMap{ + "asset": {"assethash"}, + "shim": {"shimhash"}, + }, + CurrentTrustedRecoveryBootAssets: boot.BootAssetsMap{ + "asset": {"recoveryhash"}, + }, + } + err := m.WriteTo("") + c.Assert(err, IsNil) + + // mock the files in cache + c.Assert(os.MkdirAll(filepath.Join(dirs.SnapBootAssetsDir, "trusted"), 0755), IsNil) + for _, name := range []string{ + "shim-shimhash", + "asset-assethash", + "asset-recoveryhash", + } { + err = ioutil.WriteFile(filepath.Join(dirs.SnapBootAssetsDir, "trusted", name), nil, 0644) + c.Assert(err, IsNil) + } + + s.bootloaderWithTrustedAssets(c, []string{"asset", "shim"}) + // we get an observer for UC20 + obs, _ := s.uc20UpdateObserverEncryptedSystemMockedBootloader(c) + + resealCalls := 0 + restore := boot.MockSecbootResealKeys(func(params *secboot.ResealKeysParams) error { + resealCalls++ + return nil + }) + defer restore() + + // cancel the update + err = obs.Canceled() + c.Assert(err, IsNil) + // modeenv is unchanged + afterCancelM, err := boot.ReadModeenv("") + c.Assert(err, IsNil) + c.Check(afterCancelM.CurrentTrustedBootAssets, DeepEquals, m.CurrentTrustedBootAssets) + c.Check(afterCancelM.CurrentTrustedRecoveryBootAssets, DeepEquals, m.CurrentTrustedRecoveryBootAssets) + // unused assets were dropped + checkContentGlob(c, filepath.Join(dirs.SnapBootAssetsDir, "trusted", "*"), []string{ + filepath.Join(dirs.SnapBootAssetsDir, "trusted", "asset-assethash"), + filepath.Join(dirs.SnapBootAssetsDir, "trusted", "asset-recoveryhash"), + filepath.Join(dirs.SnapBootAssetsDir, "trusted", "shim-shimhash"), + }) + + c.Check(resealCalls, Equals, 0) + + err = ioutil.WriteFile(filepath.Join(d, "shim"), []byte("shim"), 0644) + c.Assert(err, IsNil) + // observe only recovery bootloader update, no action for run bootloader + res, err := obs.Observe(gadget.ContentUpdate, mockSeedStruct, root, "shim", + &gadget.ContentChange{After: filepath.Join(d, "shim")}) + c.Assert(err, IsNil) + c.Check(res, Equals, gadget.ChangeApply) + // cancel again + err = obs.Canceled() + c.Assert(err, IsNil) + afterCancelM, err = boot.ReadModeenv("") + c.Assert(err, IsNil) + c.Check(afterCancelM.CurrentTrustedBootAssets, DeepEquals, m.CurrentTrustedBootAssets) + c.Check(afterCancelM.CurrentTrustedRecoveryBootAssets, DeepEquals, m.CurrentTrustedRecoveryBootAssets) + checkContentGlob(c, filepath.Join(dirs.SnapBootAssetsDir, "trusted", "*"), []string{ + filepath.Join(dirs.SnapBootAssetsDir, "trusted", "asset-assethash"), + filepath.Join(dirs.SnapBootAssetsDir, "trusted", "asset-recoveryhash"), + filepath.Join(dirs.SnapBootAssetsDir, "trusted", "shim-shimhash"), + }) +} + +func (s *assetsSuite) TestUpdateObserverCanceledEmptyModeenvAssets(c *C) { + // cancel an update where the maps of trusted assets are nil/empty + d := c.MkDir() + root := c.MkDir() + m := boot.Modeenv{ + Mode: "run", + } + err := m.WriteTo("") + c.Assert(err, IsNil) + + s.bootloaderWithTrustedAssets(c, []string{"asset", "shim"}) + // we get an observer for UC20 + obs, _ := s.uc20UpdateObserverEncryptedSystemMockedBootloader(c) + + // trigger loading modeenv and bootloader information + err = ioutil.WriteFile(filepath.Join(d, "shim"), []byte("shim"), 0644) + c.Assert(err, IsNil) + // observe an update only for the recovery bootloader, the run bootloader trusted assets remain empty + res, err := obs.Observe(gadget.ContentUpdate, mockSeedStruct, root, "shim", + &gadget.ContentChange{After: filepath.Join(d, "shim")}) + c.Assert(err, IsNil) + c.Check(res, Equals, gadget.ChangeApply) + + // cancel the update + err = obs.Canceled() + c.Assert(err, IsNil) + afterCancelM, err := boot.ReadModeenv("") + c.Assert(err, IsNil) + c.Check(afterCancelM.CurrentTrustedBootAssets, HasLen, 0) + c.Check(afterCancelM.CurrentTrustedRecoveryBootAssets, HasLen, 0) + + // get a new observer, and observe an update for run bootloader asset only + obs, _ = s.uc20UpdateObserverEncryptedSystemMockedBootloader(c) + res, err = obs.Observe(gadget.ContentUpdate, mockRunBootStruct, root, "shim", + &gadget.ContentChange{After: filepath.Join(d, "shim")}) + c.Assert(err, IsNil) + c.Check(res, Equals, gadget.ChangeApply) + // cancel once more + err = obs.Canceled() + c.Assert(err, IsNil) + afterCancelM, err = boot.ReadModeenv("") + c.Assert(err, IsNil) + c.Check(afterCancelM.CurrentTrustedBootAssets, HasLen, 0) + c.Check(afterCancelM.CurrentTrustedRecoveryBootAssets, HasLen, 0) +} + +func (s *assetsSuite) TestUpdateObserverCanceledAfterRollback(c *C) { + // pretend there are changed assets with hashes that are not listed in + // modeenv + d := c.MkDir() + root := c.MkDir() + + m := boot.Modeenv{ + Mode: "run", + CurrentTrustedBootAssets: boot.BootAssetsMap{ + "asset": {"assethash"}, + }, + CurrentTrustedRecoveryBootAssets: boot.BootAssetsMap{ + "asset": {"assethash"}, + }, + } + err := m.WriteTo("") + c.Assert(err, IsNil) + + s.bootloaderWithTrustedAssets(c, []string{"asset", "shim"}) + // we get an observer for UC20 + obs, _ := s.uc20UpdateObserverEncryptedSystemMockedBootloader(c) + + // trigger loading modeenv and bootloader information + err = ioutil.WriteFile(filepath.Join(d, "shim"), []byte("shim"), 0644) + c.Assert(err, IsNil) + res, err := obs.Observe(gadget.ContentUpdate, mockSeedStruct, root, "shim", + &gadget.ContentChange{After: filepath.Join(d, "shim")}) + c.Assert(err, IsNil) + c.Check(res, Equals, gadget.ChangeApply) + res, err = obs.Observe(gadget.ContentUpdate, mockRunBootStruct, root, "shim", + &gadget.ContentChange{After: filepath.Join(d, "shim")}) + c.Assert(err, IsNil) + c.Check(res, Equals, gadget.ChangeApply) + + // procure the desired state by: + // injecting a changed asset for run bootloader + recoveryAsset := true + obs.InjectChangedAsset("trusted", "asset", "changehash", !recoveryAsset) + // and a changed asset for recovery bootloader + obs.InjectChangedAsset("trusted", "asset", "changehash", recoveryAsset) + // completely unknown + obs.InjectChangedAsset("trusted", "unknown", "somehash", !recoveryAsset) + + // cancel the update + err = obs.Canceled() + c.Assert(err, IsNil) + afterCancelM, err := boot.ReadModeenv("") + c.Assert(err, IsNil) + c.Check(afterCancelM.CurrentTrustedBootAssets, DeepEquals, m.CurrentTrustedBootAssets) + c.Check(afterCancelM.CurrentTrustedRecoveryBootAssets, DeepEquals, m.CurrentTrustedRecoveryBootAssets) +} + +func (s *assetsSuite) TestUpdateObserverCanceledUnhappyCacheStillProceeds(c *C) { + // make sure that trying to remove the file from cache will not break + // the cancellation + + if os.Geteuid() == 0 { + c.Skip("the test cannot be executed by the root user") + } + + logBuf, restore := logger.MockLogger() + defer restore() + + d := c.MkDir() + root := c.MkDir() + + m := boot.Modeenv{ + Mode: "run", + CurrentTrustedBootAssets: boot.BootAssetsMap{ + "asset": {"assethash"}, + }, + CurrentTrustedRecoveryBootAssets: boot.BootAssetsMap{ + "asset": {"recoveryhash"}, + }, + } + err := m.WriteTo("") + c.Assert(err, IsNil) + + // mock the files in cache + c.Assert(os.MkdirAll(filepath.Join(dirs.SnapBootAssetsDir, "trusted"), 0755), IsNil) + for _, name := range []string{ + "asset-assethash", + "asset-recoveryhash", + } { + err = ioutil.WriteFile(filepath.Join(dirs.SnapBootAssetsDir, "trusted", name), nil, 0644) + c.Assert(err, IsNil) + } + + s.bootloaderWithTrustedAssets(c, []string{"asset", "shim"}) + // we get an observer for UC20 + obs, _ := s.uc20UpdateObserverEncryptedSystemMockedBootloader(c) + + shim := []byte("shim") + shimHash := "dac0063e831d4b2e7a330426720512fc50fa315042f0bb30f9d1db73e4898dcb89119cac41fdfa62137c8931a50f9d7b" + err = ioutil.WriteFile(filepath.Join(d, "shim"), shim, 0644) + c.Assert(err, IsNil) + res, err := obs.Observe(gadget.ContentUpdate, mockSeedStruct, root, "shim", + &gadget.ContentChange{After: filepath.Join(d, "shim")}) + c.Assert(err, IsNil) + c.Check(res, Equals, gadget.ChangeApply) + res, err = obs.Observe(gadget.ContentUpdate, mockRunBootStruct, root, "shim", + &gadget.ContentChange{After: filepath.Join(d, "shim")}) + c.Assert(err, IsNil) + c.Check(res, Equals, gadget.ChangeApply) + // make sure that the cache directory state is as expected + checkContentGlob(c, filepath.Join(dirs.SnapBootAssetsDir, "trusted", "*"), []string{ + filepath.Join(dirs.SnapBootAssetsDir, "trusted", "asset-assethash"), + filepath.Join(dirs.SnapBootAssetsDir, "trusted", "asset-recoveryhash"), + filepath.Join(dirs.SnapBootAssetsDir, "trusted", fmt.Sprintf("shim-%s", shimHash)), + }) + // and the file is added to the assets map + newM, err := boot.ReadModeenv("") + c.Assert(err, IsNil) + c.Check(newM.CurrentTrustedBootAssets, DeepEquals, boot.BootAssetsMap{ + "asset": {"assethash"}, + "shim": {shimHash}, + }) + c.Check(newM.CurrentTrustedRecoveryBootAssets, DeepEquals, boot.BootAssetsMap{ + "asset": {"recoveryhash"}, + "shim": {shimHash}, + }) + + // make cache directory read only and thus cache.Remove() fail + c.Assert(os.Chmod(filepath.Join(dirs.SnapBootAssetsDir, "trusted"), 0444), IsNil) + defer os.Chmod(filepath.Join(dirs.SnapBootAssetsDir, "trusted"), 0755) + + // cancel should not fail, even though files cannot be removed from cache + err = obs.Canceled() + c.Assert(err, IsNil) + afterCancelM, err := boot.ReadModeenv("") + c.Assert(err, IsNil) + c.Check(afterCancelM.CurrentTrustedBootAssets, DeepEquals, m.CurrentTrustedBootAssets) + c.Check(afterCancelM.CurrentTrustedRecoveryBootAssets, DeepEquals, m.CurrentTrustedRecoveryBootAssets) + checkContentGlob(c, filepath.Join(dirs.SnapBootAssetsDir, "trusted", "*"), []string{ + filepath.Join(dirs.SnapBootAssetsDir, "trusted", "asset-assethash"), + filepath.Join(dirs.SnapBootAssetsDir, "trusted", "asset-recoveryhash"), + filepath.Join(dirs.SnapBootAssetsDir, "trusted", fmt.Sprintf("shim-%s", shimHash)), + }) + c.Check(logBuf.String(), Matches, fmt.Sprintf(`.* cannot remove unused boot asset shim:%s: .* permission denied\n`, shimHash)) +} + +func (s *assetsSuite) TestObserveSuccessfulBootNoTrusted(c *C) { + // call to observe successful boot without any trusted assets + + m := &boot.Modeenv{ + Mode: "run", + // no trusted assets + } + newM, drop, err := boot.ObserveSuccessfulBootWithAssets(m) + c.Assert(err, IsNil) + c.Check(drop, IsNil) + c.Check(newM, DeepEquals, m) +} + +func (s *assetsSuite) TestObserveSuccessfulBootNoAssetsOnDisk(c *C) { + // call to observe successful boot, but assets do not exist on disk + + s.bootloaderWithTrustedAssets(c, []string{"asset"}) + + m := &boot.Modeenv{ + Mode: "run", + CurrentTrustedBootAssets: boot.BootAssetsMap{ + "asset": {"assethash"}, + }, + CurrentTrustedRecoveryBootAssets: boot.BootAssetsMap{ + "asset": {"assethash"}, + }, + } + + newM, drop, err := boot.ObserveSuccessfulBootWithAssets(m) + c.Assert(err, IsNil) + c.Check(drop, IsNil) + // we booted without assets on disk nonetheless + c.Check(newM.CurrentTrustedBootAssets, HasLen, 0) + c.Check(newM.CurrentTrustedRecoveryBootAssets, HasLen, 0) +} + +func (s *assetsSuite) TestObserveSuccessfulBootAfterUpdate(c *C) { + // call to observe successful boot + + s.bootloaderWithTrustedAssets(c, []string{"asset", "shim"}) + + data := []byte("foobar") + // SHA3-384 + dataHash := "0fa8abfbdaf924ad307b74dd2ed183b9a4a398891a2f6bac8fd2db7041b77f068580f9c6c66f699b496c2da1cbcc7ed8" + shim := []byte("shim") + shimHash := "dac0063e831d4b2e7a330426720512fc50fa315042f0bb30f9d1db73e4898dcb89119cac41fdfa62137c8931a50f9d7b" + + // only asset for ubuntu-boot + c.Assert(ioutil.WriteFile(filepath.Join(boot.InitramfsUbuntuBootDir, "asset"), data, 0644), IsNil) + // shim and asset for ubuntu-seed + c.Assert(ioutil.WriteFile(filepath.Join(boot.InitramfsUbuntuSeedDir, "asset"), data, 0644), IsNil) + c.Assert(ioutil.WriteFile(filepath.Join(boot.InitramfsUbuntuSeedDir, "shim"), shim, 0644), IsNil) + + m := &boot.Modeenv{ + Mode: "run", + CurrentTrustedBootAssets: boot.BootAssetsMap{ + "asset": {"assethash", dataHash}, + }, + CurrentTrustedRecoveryBootAssets: boot.BootAssetsMap{ + "asset": {"recoveryassethash", dataHash}, + "shim": {"recoveryshimhash", shimHash}, + }, + } + + newM, drop, err := boot.ObserveSuccessfulBootWithAssets(m) + c.Assert(err, IsNil) + c.Assert(newM, NotNil) + c.Check(newM.CurrentTrustedBootAssets, DeepEquals, boot.BootAssetsMap{ + "asset": {dataHash}, + }) + c.Check(newM.CurrentTrustedRecoveryBootAssets, DeepEquals, boot.BootAssetsMap{ + "asset": {dataHash}, + "shim": {shimHash}, + }) + c.Check(drop, HasLen, 3) + for i, en := range []struct { + assetName, hash string + }{ + {"asset", "assethash"}, + {"asset", "recoveryassethash"}, + {"shim", "recoveryshimhash"}, + } { + c.Check(drop[i].Equals("trusted", en.assetName, en.hash), IsNil) + } +} + +func (s *assetsSuite) TestObserveSuccessfulBootWithUnexpected(c *C) { + // call to observe successful boot, but the asset we booted with is unexpected + + s.bootloaderWithTrustedAssets(c, []string{"asset"}) + + data := []byte("foobar") + dataHash := "0fa8abfbdaf924ad307b74dd2ed183b9a4a398891a2f6bac8fd2db7041b77f068580f9c6c66f699b496c2da1cbcc7ed8" + unexpected := []byte("unexpected") + unexpectedHash := "2c823b62c52e614e48faac7e8b1fbb8ff3aee4d06b6f7fe5bd7d64953162b6e9879ead4827fa19c8c9a514585ddac94c" + + // asset for ubuntu-boot + c.Assert(ioutil.WriteFile(filepath.Join(boot.InitramfsUbuntuBootDir, "asset"), unexpected, 0644), IsNil) + // and for ubuntu-seed + c.Assert(ioutil.WriteFile(filepath.Join(boot.InitramfsUbuntuSeedDir, "asset"), unexpected, 0644), IsNil) + + m := &boot.Modeenv{ + Mode: "run", + CurrentTrustedBootAssets: boot.BootAssetsMap{ + "asset": {"assethash", dataHash}, + }, + CurrentTrustedRecoveryBootAssets: boot.BootAssetsMap{ + "asset": {"recoveryassethash", dataHash}, + }, + } + + newM, drop, err := boot.ObserveSuccessfulBootWithAssets(m) + c.Assert(err, ErrorMatches, fmt.Sprintf(`system booted with unexpected run mode bootloader asset "asset" hash %v`, unexpectedHash)) + c.Assert(newM, IsNil) + c.Check(drop, HasLen, 0) + + // make the run bootloader asset an expected one, we should still fail + // on the recovery bootloader asset + c.Assert(ioutil.WriteFile(filepath.Join(boot.InitramfsUbuntuBootDir, "asset"), data, 0644), IsNil) + + newM, drop, err = boot.ObserveSuccessfulBootWithAssets(m) + c.Assert(err, ErrorMatches, fmt.Sprintf(`system booted with unexpected recovery bootloader asset "asset" hash %v`, unexpectedHash)) + c.Assert(newM, IsNil) + c.Check(drop, HasLen, 0) +} + +func (s *assetsSuite) TestObserveSuccessfulBootSingleEntries(c *C) { + // call to observe successful boot + + s.bootloaderWithTrustedAssets(c, []string{"asset", "shim"}) + + data := []byte("foobar") + // SHA3-384 + dataHash := "0fa8abfbdaf924ad307b74dd2ed183b9a4a398891a2f6bac8fd2db7041b77f068580f9c6c66f699b496c2da1cbcc7ed8" + shim := []byte("shim") + shimHash := "dac0063e831d4b2e7a330426720512fc50fa315042f0bb30f9d1db73e4898dcb89119cac41fdfa62137c8931a50f9d7b" + + // only asset for ubuntu-boot + c.Assert(ioutil.WriteFile(filepath.Join(boot.InitramfsUbuntuBootDir, "asset"), data, 0644), IsNil) + // shim and asset for ubuntu-seed + c.Assert(ioutil.WriteFile(filepath.Join(boot.InitramfsUbuntuSeedDir, "asset"), data, 0644), IsNil) + c.Assert(ioutil.WriteFile(filepath.Join(boot.InitramfsUbuntuSeedDir, "shim"), shim, 0644), IsNil) + + m := &boot.Modeenv{ + Mode: "run", + CurrentTrustedBootAssets: boot.BootAssetsMap{ + "asset": {dataHash}, + }, + CurrentTrustedRecoveryBootAssets: boot.BootAssetsMap{ + "asset": {dataHash}, + "shim": {shimHash}, + }, + } + + // nothing is changed + newM, drop, err := boot.ObserveSuccessfulBootWithAssets(m) + c.Assert(err, IsNil) + c.Assert(newM, NotNil) + c.Check(newM, DeepEquals, m) + c.Check(drop, HasLen, 0) +} + +func (s *assetsSuite) TestObserveSuccessfulBootDropCandidateUsedByOtherBootloader(c *C) { + // observe successful boot, an unused recovery asset of a recovery + // bootloader is used by the ubuntu-boot bootloader, so it cannot be + // dropped from cache + + s.bootloaderWithTrustedAssets(c, []string{"asset"}) + + maybeDrop := []byte("maybe-drop") + maybeDropHash := "08a99ce3af529ebbfb9a82df690007ac650635b165c3d1b416d471907fa3843270dce9cc001ea26f4afb4e0c5af05209" + data := []byte("foobar") + dataHash := "0fa8abfbdaf924ad307b74dd2ed183b9a4a398891a2f6bac8fd2db7041b77f068580f9c6c66f699b496c2da1cbcc7ed8" + + // ubuntu-boot booted with maybe-drop asset + c.Assert(ioutil.WriteFile(filepath.Join(boot.InitramfsUbuntuBootDir, "asset"), maybeDrop, 0644), IsNil) + + c.Assert(ioutil.WriteFile(filepath.Join(boot.InitramfsUbuntuSeedDir, "asset"), data, 0644), IsNil) + + m := &boot.Modeenv{ + Mode: "run", + CurrentTrustedBootAssets: boot.BootAssetsMap{ + "asset": {maybeDropHash}, + }, + CurrentTrustedRecoveryBootAssets: boot.BootAssetsMap{ + "asset": {maybeDropHash, dataHash}, + }, + } + + // nothing is changed + newM, drop, err := boot.ObserveSuccessfulBootWithAssets(m) + c.Assert(err, IsNil) + c.Assert(newM, NotNil) + c.Check(newM.CurrentTrustedBootAssets, DeepEquals, boot.BootAssetsMap{ + "asset": {maybeDropHash}, + }) + c.Check(newM.CurrentTrustedRecoveryBootAssets, DeepEquals, boot.BootAssetsMap{ + "asset": {dataHash}, + }) + // nothing get dropped, maybe-drop asset is still used by the + // ubuntu-boot bootloader + c.Check(drop, HasLen, 0) +} + +func (s *assetsSuite) TestObserveSuccessfulBootParallelUpdate(c *C) { + // call to observe successful boot + + s.bootloaderWithTrustedAssets(c, []string{"asset", "shim"}) + + data := []byte("foobar") + // SHA3-384 + dataHash := "0fa8abfbdaf924ad307b74dd2ed183b9a4a398891a2f6bac8fd2db7041b77f068580f9c6c66f699b496c2da1cbcc7ed8" + shim := []byte("shim") + shimHash := "dac0063e831d4b2e7a330426720512fc50fa315042f0bb30f9d1db73e4898dcb89119cac41fdfa62137c8931a50f9d7b" + + // only asset for ubuntu-boot + c.Assert(ioutil.WriteFile(filepath.Join(boot.InitramfsUbuntuBootDir, "asset"), data, 0644), IsNil) + // shim and asset for ubuntu-seed + c.Assert(ioutil.WriteFile(filepath.Join(boot.InitramfsUbuntuSeedDir, "asset"), data, 0644), IsNil) + c.Assert(ioutil.WriteFile(filepath.Join(boot.InitramfsUbuntuSeedDir, "shim"), shim, 0644), IsNil) + + m := &boot.Modeenv{ + Mode: "run", + CurrentTrustedBootAssets: boot.BootAssetsMap{ + "asset": {"oldhash", dataHash}, + }, + CurrentTrustedRecoveryBootAssets: boot.BootAssetsMap{ + "asset": {"oldhash", dataHash}, + "shim": {shimHash}, + }, + } + + newM, drop, err := boot.ObserveSuccessfulBootWithAssets(m) + c.Assert(err, IsNil) + c.Assert(newM, NotNil) + c.Check(newM.CurrentTrustedBootAssets, DeepEquals, boot.BootAssetsMap{ + "asset": {dataHash}, + }) + c.Check(newM.CurrentTrustedRecoveryBootAssets, DeepEquals, boot.BootAssetsMap{ + "asset": {dataHash}, + "shim": {shimHash}, + }) + // asset was updated in parallel on both partition from the same + // oldhash that should be dropped now + c.Check(drop, HasLen, 1) + c.Check(drop[0].Equals("trusted", "asset", "oldhash"), IsNil) +} + +func (s *assetsSuite) TestObserveSuccessfulBootHashErr(c *C) { + // call to observe successful boot + + if os.Geteuid() == 0 { + c.Skip("the test cannot be executed by the root user") + } + + s.bootloaderWithTrustedAssets(c, []string{"asset"}) + + data := []byte("foobar") + dataHash := "0fa8abfbdaf924ad307b74dd2ed183b9a4a398891a2f6bac8fd2db7041b77f068580f9c6c66f699b496c2da1cbcc7ed8" + + c.Assert(ioutil.WriteFile(filepath.Join(boot.InitramfsUbuntuBootDir, "asset"), data, 0000), IsNil) + c.Assert(ioutil.WriteFile(filepath.Join(boot.InitramfsUbuntuSeedDir, "asset"), data, 0000), IsNil) + + m := &boot.Modeenv{ + Mode: "run", + CurrentTrustedBootAssets: boot.BootAssetsMap{ + "asset": {dataHash}, + }, + CurrentTrustedRecoveryBootAssets: boot.BootAssetsMap{ + "asset": {dataHash}, + }, + } + + // nothing is changed + _, _, err := boot.ObserveSuccessfulBootWithAssets(m) + c.Assert(err, ErrorMatches, "cannot calculate the digest of existing trusted asset: .*/asset: permission denied") +} + +func (s *assetsSuite) TestCopyBootAssetsCacheHappy(c *C) { + newRoot := c.MkDir() + // does not fail when dir does not exist + err := boot.CopyBootAssetsCacheToRoot(newRoot) + c.Assert(err, IsNil) + + // temporarily overide umask + oldUmask := syscall.Umask(0000) + defer syscall.Umask(oldUmask) + + entries := []struct { + name, content string + mode uint + }{ + {"foo/bar", "1234", 0644}, + {"grub/grubx64.efi-1234", "grub content", 0622}, + {"top-level", "top level content", 0666}, + {"deeply/nested/content", "deeply nested content", 0611}, + } + + for _, entry := range entries { + p := filepath.Join(dirs.SnapBootAssetsDir, entry.name) + err = os.MkdirAll(filepath.Dir(p), 0755) + c.Assert(err, IsNil) + err = ioutil.WriteFile(p, []byte(entry.content), os.FileMode(entry.mode)) + c.Assert(err, IsNil) + } + + err = boot.CopyBootAssetsCacheToRoot(newRoot) + c.Assert(err, IsNil) + for _, entry := range entries { + p := filepath.Join(dirs.SnapBootAssetsDirUnder(newRoot), entry.name) + c.Check(p, testutil.FileEquals, entry.content) + fi, err := os.Stat(p) + c.Assert(err, IsNil) + c.Check(fi.Mode().Perm(), Equals, os.FileMode(entry.mode), + Commentf("unexpected mode of copied file %q: %v", entry.name, fi.Mode().Perm())) + } +} + +func (s *assetsSuite) TestCopyBootAssetsCacheUnhappy(c *C) { + // non-file + newRoot := c.MkDir() + dirs.SnapBootAssetsDir = c.MkDir() + p := filepath.Join(dirs.SnapBootAssetsDir, "fifo") + syscall.Mkfifo(p, 0644) + err := boot.CopyBootAssetsCacheToRoot(newRoot) + c.Assert(err, ErrorMatches, `unsupported non-file entry "fifo" mode prw-.*`) + + if os.Geteuid() == 0 { + // the rest of the test cannot be executed by root user + return + } + + // non-writable root + newRoot = c.MkDir() + nonWritableRoot := filepath.Join(newRoot, "non-writable") + err = os.MkdirAll(nonWritableRoot, 0000) + c.Assert(err, IsNil) + dirs.SnapBootAssetsDir = c.MkDir() + err = ioutil.WriteFile(filepath.Join(dirs.SnapBootAssetsDir, "file"), nil, 0644) + c.Assert(err, IsNil) + err = boot.CopyBootAssetsCacheToRoot(nonWritableRoot) + c.Assert(err, ErrorMatches, `cannot create cache directory under new root: mkdir .*: permission denied`) + + // file cannot be read + newRoot = c.MkDir() + dirs.SnapBootAssetsDir = c.MkDir() + err = ioutil.WriteFile(filepath.Join(dirs.SnapBootAssetsDir, "file"), nil, 0000) + c.Assert(err, IsNil) + err = boot.CopyBootAssetsCacheToRoot(newRoot) + c.Assert(err, ErrorMatches, `cannot copy boot asset cache file "file": failed to copy all: .*`) + + // directory at destination cannot be recreated + newRoot = c.MkDir() + dirs.SnapBootAssetsDir = c.MkDir() + // make a directory at destination non writable + err = os.MkdirAll(dirs.SnapBootAssetsDirUnder(newRoot), 0755) + c.Assert(err, IsNil) + err = os.Chmod(dirs.SnapBootAssetsDirUnder(newRoot), 0000) + c.Assert(err, IsNil) + err = os.MkdirAll(filepath.Join(dirs.SnapBootAssetsDir, "dir"), 0755) + c.Assert(err, IsNil) + err = ioutil.WriteFile(filepath.Join(dirs.SnapBootAssetsDir, "dir", "file"), nil, 0000) + c.Assert(err, IsNil) + err = boot.CopyBootAssetsCacheToRoot(newRoot) + c.Assert(err, ErrorMatches, `cannot recreate cache directory "dir": .*: permission denied`) + +} + +func (s *assetsSuite) TestUpdateObserverReseal(c *C) { + // observe an update followed by reseal + + d := c.MkDir() + backups := c.MkDir() + root := c.MkDir() + + // try to arrange the backups like the updater would do it + before := []byte("before") + beforeHash := "2df0976fd45ba2392dc7985cdfb7c2d096c1ea4917929dd7a0e9bffae90a443271e702663fc6a4189c1f4ab3ce7daee3" + err := ioutil.WriteFile(filepath.Join(backups, "asset.backup"), before, 0644) + c.Assert(err, IsNil) + + data := []byte("foobar") + // SHA3-384 + dataHash := "0fa8abfbdaf924ad307b74dd2ed183b9a4a398891a2f6bac8fd2db7041b77f068580f9c6c66f699b496c2da1cbcc7ed8" + err = ioutil.WriteFile(filepath.Join(d, "foobar"), data, 0644) + c.Assert(err, IsNil) + shim := []byte("shim") + shimHash := "dac0063e831d4b2e7a330426720512fc50fa315042f0bb30f9d1db73e4898dcb89119cac41fdfa62137c8931a50f9d7b" + err = ioutil.WriteFile(filepath.Join(d, "shim"), shim, 0644) + c.Assert(err, IsNil) + + m := boot.Modeenv{ + Mode: "run", + CurrentTrustedBootAssets: boot.BootAssetsMap{ + "asset": {beforeHash}, + }, + CurrentTrustedRecoveryBootAssets: boot.BootAssetsMap{ + "asset": {beforeHash}, + }, + CurrentRecoverySystems: []string{"recovery-system-label"}, + CurrentKernels: []string{"pc-kernel_500.snap"}, + } + err = m.WriteTo("") + c.Assert(err, IsNil) + + tab := s.bootloaderWithTrustedAssets(c, []string{ + "asset", + "shim", + }) + + // we get an observer for UC20 + obs, uc20model := s.uc20UpdateObserverEncryptedSystemMockedBootloader(c) + + res, err := obs.Observe(gadget.ContentUpdate, mockRunBootStruct, root, "asset", + &gadget.ContentChange{ + After: filepath.Join(d, "foobar"), + // original content would get backed up by the updater + Before: filepath.Join(backups, "asset.backup"), + }) + c.Assert(err, IsNil) + c.Check(res, Equals, gadget.ChangeApply) + res, err = obs.Observe(gadget.ContentUpdate, mockRunBootStruct, root, "shim", + &gadget.ContentChange{After: filepath.Join(d, "shim")}) + c.Assert(err, IsNil) + c.Check(res, Equals, gadget.ChangeApply) + // observe the recovery struct + res, err = obs.Observe(gadget.ContentUpdate, mockSeedStruct, root, "shim", + &gadget.ContentChange{After: filepath.Join(d, "shim")}) + c.Assert(err, IsNil) + c.Check(res, Equals, gadget.ChangeApply) + res, err = obs.Observe(gadget.ContentUpdate, mockSeedStruct, root, "asset", + &gadget.ContentChange{ + After: filepath.Join(d, "foobar"), + // original content + Before: filepath.Join(backups, "asset.backup"), + }) + c.Assert(err, IsNil) + c.Check(res, Equals, gadget.ChangeApply) + checkContentGlob(c, filepath.Join(dirs.SnapBootAssetsDir, "trusted", "*"), []string{ + filepath.Join(dirs.SnapBootAssetsDir, "trusted", fmt.Sprintf("asset-%s", dataHash)), + filepath.Join(dirs.SnapBootAssetsDir, "trusted", fmt.Sprintf("asset-%s", beforeHash)), + filepath.Join(dirs.SnapBootAssetsDir, "trusted", fmt.Sprintf("shim-%s", shimHash)), + }) + + restore := boot.MockSeedReadSystemEssential(func(seedDir, label string, essentialTypes []snap.Type, tm timings.Measurer) (*asserts.Model, []*seed.Snap, error) { + kernelSnap := &seed.Snap{ + Path: "/var/lib/snapd/seed/snaps/pc-kernel_1.snap", + SideInfo: &snap.SideInfo{ + Revision: snap.Revision{N: 1}, + RealName: "pc-kernel", + }, + } + return uc20model, []*seed.Snap{kernelSnap}, nil + }) + defer restore() + + // everything is set up, trigger a reseal + + resealCalls := 0 + shimBf := bootloader.NewBootFile("", filepath.Join(dirs.SnapBootAssetsDir, "trusted", fmt.Sprintf("shim-%s", shimHash)), bootloader.RoleRecovery) + assetBf := bootloader.NewBootFile("", filepath.Join(dirs.SnapBootAssetsDir, "trusted", fmt.Sprintf("asset-%s", dataHash)), bootloader.RoleRecovery) + beforeAssetBf := bootloader.NewBootFile("", filepath.Join(dirs.SnapBootAssetsDir, "trusted", fmt.Sprintf("asset-%s", beforeHash)), bootloader.RoleRecovery) + recoveryKernelBf := bootloader.NewBootFile("/var/lib/snapd/seed/snaps/pc-kernel_1.snap", "kernel.efi", bootloader.RoleRecovery) + runKernelBf := bootloader.NewBootFile(filepath.Join(s.rootdir, "var/lib/snapd/snaps/pc-kernel_500.snap"), "kernel.efi", bootloader.RoleRunMode) + + tab.RecoveryBootChainList = []bootloader.BootFile{ + bootloader.NewBootFile("", "shim", bootloader.RoleRecovery), + bootloader.NewBootFile("", "asset", bootloader.RoleRecovery), + recoveryKernelBf, + } + tab.BootChainList = []bootloader.BootFile{ + bootloader.NewBootFile("", "shim", bootloader.RoleRecovery), + bootloader.NewBootFile("", "asset", bootloader.RoleRecovery), + runKernelBf, + } + + restore = boot.MockSecbootResealKeys(func(params *secboot.ResealKeysParams) error { + resealCalls++ + + c.Assert(params.ModelParams, HasLen, 1) + mp := params.ModelParams[0] + c.Check(mp.Model, DeepEquals, uc20model) + for _, ch := range mp.EFILoadChains { + printChain(c, ch, "-") + } + switch resealCalls { + case 1: + c.Check(mp.EFILoadChains, DeepEquals, []*secboot.LoadChain{ + secboot.NewLoadChain(shimBf, + secboot.NewLoadChain(assetBf, + secboot.NewLoadChain(recoveryKernelBf)), + secboot.NewLoadChain(beforeAssetBf, + secboot.NewLoadChain(recoveryKernelBf))), + secboot.NewLoadChain(shimBf, + secboot.NewLoadChain(assetBf, + secboot.NewLoadChain(runKernelBf)), + secboot.NewLoadChain(beforeAssetBf, + secboot.NewLoadChain(runKernelBf))), + }) + case 2: + c.Check(mp.EFILoadChains, DeepEquals, []*secboot.LoadChain{ + secboot.NewLoadChain(shimBf, + secboot.NewLoadChain(assetBf, + secboot.NewLoadChain(recoveryKernelBf)), + secboot.NewLoadChain(beforeAssetBf, + secboot.NewLoadChain(recoveryKernelBf))), + }) + default: + c.Errorf("unexpected additional call to secboot.ResealKey (call # %d)", resealCalls) + } + return nil + }) + defer restore() + + err = obs.BeforeWrite() + c.Assert(err, IsNil) + c.Check(resealCalls, Equals, 2) +} + +func (s *assetsSuite) TestUpdateObserverCanceledReseal(c *C) { + // check that Canceled calls reseal when there were changes to the + // trusted boot assets + d := c.MkDir() + root := c.MkDir() + + m := boot.Modeenv{ + Mode: "run", + CurrentTrustedBootAssets: boot.BootAssetsMap{ + "asset": {"assethash"}, + "shim": {"shimhash"}, + }, + CurrentTrustedRecoveryBootAssets: boot.BootAssetsMap{ + "asset": {"assethash"}, + "shim": {"shimhash"}, + }, + CurrentRecoverySystems: []string{"system"}, + CurrentKernels: []string{"pc-kernel_1.snap"}, + } + err := m.WriteTo("") + c.Assert(err, IsNil) + + // mock some files in cache + c.Assert(os.MkdirAll(filepath.Join(dirs.SnapBootAssetsDir, "trusted"), 0755), IsNil) + for _, name := range []string{ + "shim-shimhash", + "asset-assethash", + "asset-recoveryhash", + } { + err = ioutil.WriteFile(filepath.Join(dirs.SnapBootAssetsDir, "trusted", name), nil, 0644) + c.Assert(err, IsNil) + } + + tab := s.bootloaderWithTrustedAssets(c, []string{"asset", "shim"}) + + // we get an observer for UC20 + obs, uc20model := s.uc20UpdateObserverEncryptedSystemMockedBootloader(c) + + data := []byte("foobar") + err = ioutil.WriteFile(filepath.Join(d, "foobar"), data, 0644) + c.Assert(err, IsNil) + shim := []byte("shim") + err = ioutil.WriteFile(filepath.Join(d, "shim"), shim, 0644) + c.Assert(err, IsNil) + + // trigger a bunch of updates, so that we have things to cancel + res, err := obs.Observe(gadget.ContentUpdate, mockRunBootStruct, root, "asset", + &gadget.ContentChange{After: filepath.Join(d, "foobar")}) + c.Assert(err, IsNil) + c.Check(res, Equals, gadget.ChangeApply) + res, err = obs.Observe(gadget.ContentUpdate, mockRunBootStruct, root, "shim", + &gadget.ContentChange{After: filepath.Join(d, "shim")}) + c.Assert(err, IsNil) + c.Check(res, Equals, gadget.ChangeApply) + // observe the recovery struct + res, err = obs.Observe(gadget.ContentUpdate, mockSeedStruct, root, "shim", + &gadget.ContentChange{After: filepath.Join(d, "shim")}) + c.Assert(err, IsNil) + c.Check(res, Equals, gadget.ChangeApply) + res, err = obs.Observe(gadget.ContentUpdate, mockSeedStruct, root, "asset", + &gadget.ContentChange{After: filepath.Join(d, "foobar")}) + c.Assert(err, IsNil) + c.Check(res, Equals, gadget.ChangeApply) + + restore := boot.MockSeedReadSystemEssential(func(seedDir, label string, essentialTypes []snap.Type, tm timings.Measurer) (*asserts.Model, []*seed.Snap, error) { + kernelSnap := &seed.Snap{ + Path: "/var/lib/snapd/seed/snaps/pc-kernel_1.snap", + SideInfo: &snap.SideInfo{ + Revision: snap.Revision{N: 1}, + RealName: "pc-kernel", + }, + } + return uc20model, []*seed.Snap{kernelSnap}, nil + }) + defer restore() + + shimBf := bootloader.NewBootFile("", filepath.Join(dirs.SnapBootAssetsDir, "trusted/shim-shimhash"), bootloader.RoleRecovery) + assetBf := bootloader.NewBootFile("", filepath.Join(dirs.SnapBootAssetsDir, "trusted/asset-assethash"), bootloader.RoleRecovery) + recoveryKernelBf := bootloader.NewBootFile("/var/lib/snapd/seed/snaps/pc-kernel_1.snap", "kernel.efi", bootloader.RoleRecovery) + runKernelBf := bootloader.NewBootFile(filepath.Join(s.rootdir, "var/lib/snapd/snaps/pc-kernel_500.snap"), "kernel.efi", bootloader.RoleRunMode) + tab.RecoveryBootChainList = []bootloader.BootFile{ + bootloader.NewBootFile("", "shim", bootloader.RoleRecovery), + bootloader.NewBootFile("", "asset", bootloader.RoleRecovery), + recoveryKernelBf, + } + tab.BootChainList = []bootloader.BootFile{ + bootloader.NewBootFile("", "shim", bootloader.RoleRecovery), + bootloader.NewBootFile("", "asset", bootloader.RoleRecovery), + runKernelBf, + } + + 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, DeepEquals, uc20model) + for _, ch := range mp.EFILoadChains { + printChain(c, ch, "-") + } + switch resealCalls { + case 1: + c.Check(mp.EFILoadChains, DeepEquals, []*secboot.LoadChain{ + secboot.NewLoadChain(shimBf, + secboot.NewLoadChain(assetBf, + secboot.NewLoadChain(recoveryKernelBf))), + secboot.NewLoadChain(shimBf, + secboot.NewLoadChain(assetBf, + secboot.NewLoadChain(runKernelBf))), + }) + case 2: + c.Check(mp.EFILoadChains, DeepEquals, []*secboot.LoadChain{ + secboot.NewLoadChain(shimBf, + secboot.NewLoadChain(assetBf, + secboot.NewLoadChain(recoveryKernelBf))), + }) + default: + c.Errorf("unexpected additional call to secboot.ResealKey (call # %d)", resealCalls) + } + return nil + }) + defer restore() + + // update is canceled + err = obs.Canceled() + c.Assert(err, IsNil) + // modeenv is back to initial state + afterCancelM, err := boot.ReadModeenv("") + c.Assert(err, IsNil) + c.Check(afterCancelM.CurrentTrustedBootAssets, DeepEquals, m.CurrentTrustedBootAssets) + c.Check(afterCancelM.CurrentTrustedRecoveryBootAssets, DeepEquals, m.CurrentTrustedRecoveryBootAssets) + // unused assets were dropped + checkContentGlob(c, filepath.Join(dirs.SnapBootAssetsDir, "trusted", "*"), []string{ + filepath.Join(dirs.SnapBootAssetsDir, "trusted", "asset-assethash"), + filepath.Join(dirs.SnapBootAssetsDir, "trusted", "asset-recoveryhash"), + filepath.Join(dirs.SnapBootAssetsDir, "trusted", "shim-shimhash"), + }) + + c.Check(resealCalls, Equals, 2) +} + +func (s *assetsSuite) TestUpdateObserverUpdateMockedNonEncryption(c *C) { + // observe an update on a system where encryption is not used + + d := c.MkDir() + backups := c.MkDir() + root := c.MkDir() + + // try to arrange the backups like the updater would do it + data := []byte("foobar") + err := ioutil.WriteFile(filepath.Join(d, "foobar"), data, 0644) + c.Assert(err, IsNil) + + m := boot.Modeenv{ + Mode: "run", + } + err = m.WriteTo("") + c.Assert(err, IsNil) + + tab := s.bootloaderWithTrustedAssets(c, []string{ + "asset", + }) + tab.ManagedAssetsList = []string{ + "managed-asset", + } + + // we get an observer for UC20, bootloader is mocked + obs, _ := s.uc20UpdateObserver(c, c.MkDir()) + + // asset is ignored, and the change is applied + change := &gadget.ContentChange{ + After: filepath.Join(d, "foobar"), + // original content would get backed up by the updater + Before: filepath.Join(backups, "asset.backup"), + } + res, err := obs.Observe(gadget.ContentUpdate, mockRunBootStruct, root, "asset", change) + c.Assert(err, IsNil) + c.Check(res, Equals, gadget.ChangeApply) + res, err = obs.Observe(gadget.ContentUpdate, mockSeedStruct, root, "asset", change) + c.Assert(err, IsNil) + c.Check(res, Equals, gadget.ChangeApply) + // trusted assets were asked for when setting up bootloader context + c.Check(tab.TrustedAssetsCalls, Equals, 2) + // but nothing is really tracked + checkContentGlob(c, filepath.Join(dirs.SnapBootAssetsDir, "trusted", "*"), nil) + // check modeenv + newM, err := boot.ReadModeenv("") + c.Assert(err, IsNil) + c.Check(newM.CurrentTrustedBootAssets, HasLen, 0) + c.Check(newM.CurrentTrustedRecoveryBootAssets, HasLen, 0) + + // verify that managed assets are to be preserved + res, err = obs.Observe(gadget.ContentUpdate, mockRunBootStruct, root, "managed-asset", + &gadget.ContentChange{After: filepath.Join(d, "foobar")}) + c.Assert(err, IsNil) + c.Check(res, Equals, gadget.ChangeIgnore) + res, err = obs.Observe(gadget.ContentUpdate, mockSeedStruct, root, "managed-asset", + &gadget.ContentChange{After: filepath.Join(d, "foobar")}) + c.Assert(err, IsNil) + c.Check(res, Equals, gadget.ChangeIgnore) + + // make sure that no reseal is triggered + resealCalls := 0 + restore := boot.MockSecbootResealKeys(func(params *secboot.ResealKeysParams) error { + resealCalls++ + return nil + }) + defer restore() + + err = obs.BeforeWrite() + c.Assert(err, IsNil) + c.Check(resealCalls, Equals, 0) + + err = obs.Canceled() + c.Assert(err, IsNil) + c.Check(resealCalls, Equals, 0) +} diff -Nru snapd-2.45.1+20.04.2/boot/bootchain.go snapd-2.48.3+20.04/boot/bootchain.go --- snapd-2.45.1+20.04.2/boot/bootchain.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48.3+20.04/boot/bootchain.go 2021-02-02 08:21:12.000000000 +0000 @@ -0,0 +1,331 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2020 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 boot + +import ( + "bytes" + "encoding/json" + "fmt" + "os" + "path/filepath" + "sort" + + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/bootloader" + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/secboot" +) + +// TODO:UC20 add a doc comment when this is stabilized +type bootChain struct { + BrandID string `json:"brand-id"` + Model string `json:"model"` + Grade asserts.ModelGrade `json:"grade"` + ModelSignKeyID string `json:"model-sign-key-id"` + AssetChain []bootAsset `json:"asset-chain"` + Kernel string `json:"kernel"` + // KernelRevision is the revision of the kernel snap. It is empty if + // kernel is unasserted, in which case always reseal. + KernelRevision string `json:"kernel-revision"` + KernelCmdlines []string `json:"kernel-cmdlines"` + + model *asserts.Model + kernelBootFile bootloader.BootFile +} + +// TODO:UC20 add a doc comment when this is stabilized +type bootAsset struct { + Role bootloader.Role `json:"role"` + Name string `json:"name"` + Hashes []string `json:"hashes"` +} + +func bootAssetLess(b, other *bootAsset) bool { + byRole := b.Role < other.Role + byName := b.Name < other.Name + // sort order: role -> name -> hash list (len -> lexical) + if b.Role != other.Role { + return byRole + } + if b.Name != other.Name { + return byName + } + return stringListsLess(b.Hashes, other.Hashes) +} + +func stringListsEqual(sl1, sl2 []string) bool { + if len(sl1) != len(sl2) { + return false + } + for i := range sl1 { + if sl1[i] != sl2[i] { + return false + } + } + return true +} + +func stringListsLess(sl1, sl2 []string) bool { + if len(sl1) != len(sl2) { + return len(sl1) < len(sl2) + } + for idx := range sl1 { + if sl1[idx] < sl2[idx] { + return true + } + } + return false +} + +func toPredictableBootAsset(b *bootAsset) *bootAsset { + if b == nil { + return nil + } + newB := *b + if b.Hashes != nil { + newB.Hashes = make([]string, len(b.Hashes)) + copy(newB.Hashes, b.Hashes) + sort.Strings(newB.Hashes) + } + return &newB +} + +func toPredictableBootChain(b *bootChain) *bootChain { + if b == nil { + return nil + } + newB := *b + if b.AssetChain != nil { + newB.AssetChain = make([]bootAsset, len(b.AssetChain)) + for i := range b.AssetChain { + newB.AssetChain[i] = *toPredictableBootAsset(&b.AssetChain[i]) + } + } + if b.KernelCmdlines != nil { + newB.KernelCmdlines = make([]string, len(b.KernelCmdlines)) + copy(newB.KernelCmdlines, b.KernelCmdlines) + sort.Strings(newB.KernelCmdlines) + } + return &newB +} + +func predictableBootAssetsEqual(b1, b2 []bootAsset) bool { + b1JSON, err := json.Marshal(b1) + if err != nil { + return false + } + b2JSON, err := json.Marshal(b2) + if err != nil { + return false + } + return bytes.Equal(b1JSON, b2JSON) +} + +func predictableBootAssetsLess(b1, b2 []bootAsset) bool { + if len(b1) != len(b2) { + return len(b1) < len(b2) + } + for i := range b1 { + if bootAssetLess(&b1[i], &b2[i]) { + return true + } + } + return false +} + +type byBootChainOrder []bootChain + +func (b byBootChainOrder) Len() int { return len(b) } +func (b byBootChainOrder) Swap(i, j int) { b[i], b[j] = b[j], b[i] } +func (b byBootChainOrder) Less(i, j int) bool { + // sort by model info + if b[i].BrandID != b[j].BrandID { + return b[i].BrandID < b[j].BrandID + } + if b[i].Model != b[j].Model { + return b[i].Model < b[j].Model + } + if b[i].Grade != b[j].Grade { + return b[i].Grade < b[j].Grade + } + if b[i].ModelSignKeyID != b[j].ModelSignKeyID { + return b[i].ModelSignKeyID < b[j].ModelSignKeyID + } + // then boot assets + if !predictableBootAssetsEqual(b[i].AssetChain, b[j].AssetChain) { + return predictableBootAssetsLess(b[i].AssetChain, b[j].AssetChain) + } + // then kernel + if b[i].Kernel != b[j].Kernel { + return b[i].Kernel < b[j].Kernel + } + if b[i].KernelRevision != b[j].KernelRevision { + return b[i].KernelRevision < b[j].KernelRevision + } + // and last kernel command lines + if !stringListsEqual(b[i].KernelCmdlines, b[j].KernelCmdlines) { + return stringListsLess(b[i].KernelCmdlines, b[j].KernelCmdlines) + } + return false +} + +type predictableBootChains []bootChain + +// hasUnrevisionedKernels returns true if any of the chains have an +// unrevisioned kernel. Revisions will not be set for unasserted +// kernels. +func (pbc predictableBootChains) hasUnrevisionedKernels() bool { + for i := range pbc { + if pbc[i].KernelRevision == "" { + return true + } + } + return false +} + +func toPredictableBootChains(chains []bootChain) predictableBootChains { + if chains == nil { + return nil + } + predictableChains := make([]bootChain, len(chains)) + for i := range chains { + predictableChains[i] = *toPredictableBootChain(&chains[i]) + } + sort.Sort(byBootChainOrder(predictableChains)) + return predictableChains +} + +type bootChainEquivalence int + +const ( + bootChainEquivalent bootChainEquivalence = 0 + bootChainDifferent bootChainEquivalence = 1 + bootChainUnrevisioned bootChainEquivalence = -1 +) + +// predictableBootChainsEqualForReseal returns bootChainEquivalent +// when boot chains are equivalent for reseal. If the boot chains +// are clearly different it returns bootChainDifferent. +// If it would return bootChainEquivalent but the chains contain +// unrevisioned kernels it will return bootChainUnrevisioned. +func predictableBootChainsEqualForReseal(pb1, pb2 predictableBootChains) bootChainEquivalence { + pb1JSON, err := json.Marshal(pb1) + if err != nil { + return bootChainDifferent + } + pb2JSON, err := json.Marshal(pb2) + if err != nil { + return bootChainDifferent + } + if bytes.Equal(pb1JSON, pb2JSON) { + if pb1.hasUnrevisionedKernels() { + return bootChainUnrevisioned + } + return bootChainEquivalent + } + return bootChainDifferent +} + +// bootAssetsToLoadChains generates a list of load chains covering given boot +// assets sequence. At the end of each chain, adds an entry for the kernel boot +// file. +func bootAssetsToLoadChains(assets []bootAsset, kernelBootFile bootloader.BootFile, roleToBlName map[bootloader.Role]string) ([]*secboot.LoadChain, error) { + // kernel is added after all the assets + addKernelBootFile := len(assets) == 0 + if addKernelBootFile { + return []*secboot.LoadChain{secboot.NewLoadChain(kernelBootFile)}, nil + } + + thisAsset := assets[0] + blName := roleToBlName[thisAsset.Role] + if blName == "" { + return nil, fmt.Errorf("internal error: no bootloader name for boot asset role %q", thisAsset.Role) + } + var chains []*secboot.LoadChain + for _, hash := range thisAsset.Hashes { + var bf bootloader.BootFile + var next []*secboot.LoadChain + var err error + + p := filepath.Join( + dirs.SnapBootAssetsDir, + trustedAssetCacheRelPath(blName, thisAsset.Name, hash)) + if !osutil.FileExists(p) { + return nil, fmt.Errorf("file %s not found in boot assets cache", p) + } + bf = bootloader.NewBootFile( + "", // asset comes from the filesystem, not a snap + p, + thisAsset.Role, + ) + next, err = bootAssetsToLoadChains(assets[1:], kernelBootFile, roleToBlName) + if err != nil { + return nil, err + } + chains = append(chains, secboot.NewLoadChain(bf, next...)) + } + return chains, nil +} + +// predictableBootChainsWrapperForStorage wraps the boot chains so +// that we do not store the arrays directly as JSON and we can add +// other information +type predictableBootChainsWrapperForStorage struct { + ResealCount int `json:"reseal-count"` + BootChains predictableBootChains `json:"boot-chains"` +} + +func readBootChains(path string) (pbc predictableBootChains, resealCount int, err error) { + inf, err := os.Open(path) + if err != nil { + if os.IsNotExist(err) { + return nil, 0, nil + } + return nil, 0, fmt.Errorf("cannot open existing boot chains data file: %v", err) + } + defer inf.Close() + var wrapped predictableBootChainsWrapperForStorage + if err := json.NewDecoder(inf).Decode(&wrapped); err != nil { + return nil, 0, fmt.Errorf("cannot read boot chains data: %v", err) + } + return wrapped.BootChains, wrapped.ResealCount, nil +} + +func writeBootChains(pbc predictableBootChains, path string, resealCount int) error { + if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { + return fmt.Errorf("cannot create device fde state directory: %v", err) + } + outf, err := osutil.NewAtomicFile(path, 0600, 0, osutil.NoChown, osutil.NoChown) + if err != nil { + return fmt.Errorf("cannot create a temporary boot chains file: %v", err) + } + // becomes noop when the file is committed + defer outf.Cancel() + + wrapped := predictableBootChainsWrapperForStorage{ + ResealCount: resealCount, + BootChains: pbc, + } + if err := json.NewEncoder(outf).Encode(wrapped); err != nil { + return fmt.Errorf("cannot write boot chains data: %v", err) + } + return outf.Commit() +} diff -Nru snapd-2.45.1+20.04.2/boot/bootchain_test.go snapd-2.48.3+20.04/boot/bootchain_test.go --- snapd-2.45.1+20.04.2/boot/bootchain_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48.3+20.04/boot/bootchain_test.go 2021-02-02 08:21:12.000000000 +0000 @@ -0,0 +1,1236 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2020 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 boot_test + +import ( + "encoding/json" + "io/ioutil" + "os" + "path/filepath" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/boot" + "github.com/snapcore/snapd/boot/boottest" + "github.com/snapcore/snapd/bootloader" + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/secboot" + "github.com/snapcore/snapd/testutil" +) + +type bootchainSuite struct { + testutil.BaseTest + + rootDir string +} + +var _ = Suite(&bootchainSuite{}) + +func (s *bootchainSuite) SetUpTest(c *C) { + s.BaseTest.SetUpTest(c) + s.rootDir = c.MkDir() + s.AddCleanup(func() { dirs.SetRootDir("/") }) + dirs.SetRootDir(s.rootDir) + + c.Assert(os.MkdirAll(filepath.Join(dirs.SnapBootAssetsDir), 0755), IsNil) +} + +func (s *bootchainSuite) TestBootAssetLess(c *C) { + for _, tc := range []struct { + l, r *boot.BootAsset + exp bool + }{ + {&boot.BootAsset{Role: "recovery"}, &boot.BootAsset{Role: "run"}, true}, + {&boot.BootAsset{Role: "run"}, &boot.BootAsset{Role: "recovery"}, false}, + {&boot.BootAsset{Name: "1"}, &boot.BootAsset{Name: "11"}, true}, + {&boot.BootAsset{Name: "11"}, &boot.BootAsset{Name: "1"}, false}, + {&boot.BootAsset{Hashes: []string{"11"}}, &boot.BootAsset{Hashes: []string{"11", "11"}}, true}, + {&boot.BootAsset{Hashes: []string{"11"}}, &boot.BootAsset{Hashes: []string{"12"}}, true}, + } { + less := boot.BootAssetLess(tc.l, tc.r) + c.Check(less, Equals, tc.exp, Commentf("expected %v got %v for:\nl:%v\nr:%v", tc.exp, less, tc.l, tc.r)) + } +} + +func (s *bootchainSuite) TestBootAssetsPredictable(c *C) { + // by role + ba := boot.BootAsset{ + Role: bootloader.RoleRunMode, Name: "list", Hashes: []string{"b", "a"}, + } + pred := boot.ToPredictableBootAsset(&ba) + c.Check(pred, DeepEquals, &boot.BootAsset{ + Role: bootloader.RoleRunMode, Name: "list", Hashes: []string{"a", "b"}, + }) + // original structure is not changed + c.Check(ba, DeepEquals, boot.BootAsset{ + Role: bootloader.RoleRunMode, Name: "list", Hashes: []string{"b", "a"}, + }) + + // try to make a predictable struct predictable once more + predAgain := boot.ToPredictableBootAsset(pred) + c.Check(predAgain, DeepEquals, pred) + + baNil := boot.ToPredictableBootAsset(nil) + c.Check(baNil, IsNil) +} + +func (s *bootchainSuite) TestBootChainMarshalOnlyAssets(c *C) { + pbNil := boot.ToPredictableBootChain(nil) + c.Check(pbNil, IsNil) + + bc := &boot.BootChain{ + AssetChain: []boot.BootAsset{ + {Role: bootloader.RoleRecovery, Name: "shim", Hashes: []string{"b"}}, + {Role: bootloader.RoleRecovery, Name: "loader", Hashes: []string{"e", "d"}}, + {Role: bootloader.RoleRunMode, Name: "loader", Hashes: []string{"d", "c"}}, + {Role: bootloader.RoleRunMode, Name: "1oader", Hashes: []string{"e", "d"}}, + {Role: bootloader.RoleRunMode, Name: "0oader", Hashes: []string{"z", "x"}}, + }, + } + + predictableBc := boot.ToPredictableBootChain(bc) + + c.Check(predictableBc, DeepEquals, &boot.BootChain{ + // assets not reordered + AssetChain: []boot.BootAsset{ + // hash lists are sorted + {Role: bootloader.RoleRecovery, Name: "shim", Hashes: []string{"b"}}, + {Role: bootloader.RoleRecovery, Name: "loader", Hashes: []string{"d", "e"}}, + {Role: bootloader.RoleRunMode, Name: "loader", Hashes: []string{"c", "d"}}, + {Role: bootloader.RoleRunMode, Name: "1oader", Hashes: []string{"d", "e"}}, + {Role: bootloader.RoleRunMode, Name: "0oader", Hashes: []string{"x", "z"}}, + }, + }) + + // already predictable, but try again + alreadySortedBc := boot.ToPredictableBootChain(predictableBc) + c.Check(alreadySortedBc, DeepEquals, predictableBc) + + // boot chain with 2 identical assets + bcIdenticalAssets := &boot.BootChain{ + AssetChain: []boot.BootAsset{ + {Role: bootloader.RoleRunMode, Name: "loader", Hashes: []string{"z"}}, + {Role: bootloader.RoleRunMode, Name: "loader", Hashes: []string{"z"}}, + }, + } + sortedBcIdentical := boot.ToPredictableBootChain(bcIdenticalAssets) + c.Check(sortedBcIdentical, DeepEquals, bcIdenticalAssets) +} + +func (s *bootchainSuite) TestBootChainMarshalFull(c *C) { + bc := &boot.BootChain{ + BrandID: "mybrand", + Model: "foo", + Grade: "dangerous", + ModelSignKeyID: "my-key-id", + // asset chain does not get sorted when marshaling + AssetChain: []boot.BootAsset{ + // hash list will get sorted + {Role: bootloader.RoleRecovery, Name: "shim", Hashes: []string{"b", "a"}}, + {Role: bootloader.RoleRecovery, Name: "loader", Hashes: []string{"d"}}, + {Role: bootloader.RoleRunMode, Name: "loader", Hashes: []string{"c", "d"}}, + }, + Kernel: "pc-kernel", + KernelRevision: "1234", + KernelCmdlines: []string{`foo=bar baz=0x123`, `a=1`}, + } + + uc20model := boottest.MakeMockUC20Model() + bc.SetModelAssertion(uc20model) + kernelBootFile := bootloader.NewBootFile("pc-kernel", "/foo", bootloader.RoleRecovery) + bc.SetKernelBootFile(kernelBootFile) + + expectedPredictableBc := &boot.BootChain{ + BrandID: "mybrand", + Model: "foo", + Grade: "dangerous", + ModelSignKeyID: "my-key-id", + // assets are not reordered + AssetChain: []boot.BootAsset{ + // hash lists are sorted + {Role: bootloader.RoleRecovery, Name: "shim", Hashes: []string{"a", "b"}}, + {Role: bootloader.RoleRecovery, Name: "loader", Hashes: []string{"d"}}, + {Role: bootloader.RoleRunMode, Name: "loader", Hashes: []string{"c", "d"}}, + }, + Kernel: "pc-kernel", + KernelRevision: "1234", + KernelCmdlines: []string{`a=1`, `foo=bar baz=0x123`}, + } + // those can't be set directly, but are copied as well + expectedPredictableBc.SetModelAssertion(uc20model) + expectedPredictableBc.SetKernelBootFile(kernelBootFile) + + predictableBc := boot.ToPredictableBootChain(bc) + c.Check(predictableBc, DeepEquals, expectedPredictableBc) + + d, err := json.Marshal(predictableBc) + c.Assert(err, IsNil) + c.Check(string(d), Equals, `{"brand-id":"mybrand","model":"foo","grade":"dangerous","model-sign-key-id":"my-key-id","asset-chain":[{"role":"recovery","name":"shim","hashes":["a","b"]},{"role":"recovery","name":"loader","hashes":["d"]},{"role":"run-mode","name":"loader","hashes":["c","d"]}],"kernel":"pc-kernel","kernel-revision":"1234","kernel-cmdlines":["a=1","foo=bar baz=0x123"]}`) + expectedOriginal := &boot.BootChain{ + BrandID: "mybrand", + Model: "foo", + Grade: "dangerous", + ModelSignKeyID: "my-key-id", + AssetChain: []boot.BootAsset{ + {Role: bootloader.RoleRecovery, Name: "shim", Hashes: []string{"b", "a"}}, + {Role: bootloader.RoleRecovery, Name: "loader", Hashes: []string{"d"}}, + {Role: bootloader.RoleRunMode, Name: "loader", Hashes: []string{"c", "d"}}, + }, + Kernel: "pc-kernel", + KernelRevision: "1234", + KernelCmdlines: []string{`foo=bar baz=0x123`, `a=1`}, + } + expectedOriginal.SetModelAssertion(uc20model) + expectedOriginal.SetKernelBootFile(kernelBootFile) + // original structure has not been modified + c.Check(bc, DeepEquals, expectedOriginal) +} + +func (s *bootchainSuite) TestPredictableBootChainsEqualForReseal(c *C) { + var pbNil boot.PredictableBootChains + + c.Check(boot.PredictableBootChainsEqualForReseal(pbNil, pbNil), Equals, boot.BootChainEquivalent) + + bcJustOne := []boot.BootChain{ + { + BrandID: "mybrand", + Model: "foo", + Grade: "dangerous", + ModelSignKeyID: "my-key-id", + AssetChain: []boot.BootAsset{ + {Role: bootloader.RoleRecovery, Name: "shim", Hashes: []string{"b", "a"}}, + {Role: bootloader.RoleRecovery, Name: "loader", Hashes: []string{"d"}}, + {Role: bootloader.RoleRunMode, Name: "loader", Hashes: []string{"c", "d"}}, + }, + Kernel: "pc-kernel-other", + KernelRevision: "1234", + KernelCmdlines: []string{`foo`}, + }, + } + pbJustOne := boot.ToPredictableBootChains(bcJustOne) + // equal with self + c.Check(boot.PredictableBootChainsEqualForReseal(pbJustOne, pbJustOne), Equals, boot.BootChainEquivalent) + + // equal with nil? + c.Check(boot.PredictableBootChainsEqualForReseal(pbJustOne, pbNil), Equals, boot.BootChainDifferent) + + bcMoreAssets := []boot.BootChain{ + { + BrandID: "mybrand", + Model: "foo", + Grade: "dangerous", + ModelSignKeyID: "my-key-id", + AssetChain: []boot.BootAsset{ + {Role: bootloader.RoleRecovery, Name: "shim", Hashes: []string{"a", "b"}}, + {Role: bootloader.RoleRecovery, Name: "loader", Hashes: []string{"d"}}, + }, + Kernel: "pc-kernel-recovery", + KernelRevision: "1234", + KernelCmdlines: []string{`foo`}, + }, { + BrandID: "mybrand", + Model: "foo", + Grade: "dangerous", + ModelSignKeyID: "my-key-id", + AssetChain: []boot.BootAsset{ + {Role: bootloader.RoleRecovery, Name: "shim", Hashes: []string{"a", "b"}}, + {Role: bootloader.RoleRecovery, Name: "loader", Hashes: []string{"d"}}, + {Role: bootloader.RoleRunMode, Name: "loader", Hashes: []string{"c", "d"}}, + }, + Kernel: "pc-kernel-other", + KernelRevision: "1234", + KernelCmdlines: []string{`foo`}, + }, + } + + pbMoreAssets := boot.ToPredictableBootChains(bcMoreAssets) + + c.Check(boot.PredictableBootChainsEqualForReseal(pbMoreAssets, pbJustOne), Equals, boot.BootChainDifferent) + // with self + c.Check(boot.PredictableBootChainsEqualForReseal(pbMoreAssets, pbMoreAssets), Equals, boot.BootChainEquivalent) + // chains composed of respective elements are not equal + c.Check(boot.PredictableBootChainsEqualForReseal( + []boot.BootChain{pbMoreAssets[0]}, + []boot.BootChain{pbMoreAssets[1]}), + Equals, boot.BootChainDifferent) + + // unrevisioned/unasserted kernels + bcUnrevOne := []boot.BootChain{pbJustOne[0]} + bcUnrevOne[0].KernelRevision = "" + pbUnrevOne := boot.ToPredictableBootChains(bcUnrevOne) + // soundness + c.Check(boot.PredictableBootChainsEqualForReseal(pbJustOne, pbJustOne), Equals, boot.BootChainEquivalent) + // never equal even with self because of unrevisioned + c.Check(boot.PredictableBootChainsEqualForReseal(pbJustOne, pbUnrevOne), Equals, boot.BootChainDifferent) + c.Check(boot.PredictableBootChainsEqualForReseal(pbUnrevOne, pbUnrevOne), Equals, boot.BootChainUnrevisioned) + + bcUnrevMoreAssets := []boot.BootChain{pbMoreAssets[0], pbMoreAssets[1]} + bcUnrevMoreAssets[1].KernelRevision = "" + pbUnrevMoreAssets := boot.ToPredictableBootChains(bcUnrevMoreAssets) + // never equal even with self because of unrevisioned + c.Check(boot.PredictableBootChainsEqualForReseal(pbUnrevMoreAssets, pbMoreAssets), Equals, boot.BootChainDifferent) + c.Check(boot.PredictableBootChainsEqualForReseal(pbUnrevMoreAssets, pbUnrevMoreAssets), Equals, boot.BootChainUnrevisioned) +} + +func (s *bootchainSuite) TestPredictableBootChainsFullMarshal(c *C) { + // chains will be sorted + chains := []boot.BootChain{ + { + BrandID: "mybrand", + Model: "foo", + Grade: "signed", + ModelSignKeyID: "my-key-id", + AssetChain: []boot.BootAsset{ + // hashes will be sorted + {Role: bootloader.RoleRecovery, Name: "shim", Hashes: []string{"x", "y"}}, + {Role: bootloader.RoleRecovery, Name: "loader", Hashes: []string{"c", "d"}}, + {Role: bootloader.RoleRunMode, Name: "loader", Hashes: []string{"z", "x"}}, + }, + Kernel: "pc-kernel-other", + KernelRevision: "2345", + KernelCmdlines: []string{`snapd_recovery_mode=run foo`}, + }, { + BrandID: "mybrand", + Model: "foo", + Grade: "dangerous", + ModelSignKeyID: "my-key-id", + AssetChain: []boot.BootAsset{ + // hashes will be sorted + {Role: bootloader.RoleRecovery, Name: "shim", Hashes: []string{"y", "x"}}, + {Role: bootloader.RoleRecovery, Name: "loader", Hashes: []string{"c", "d"}}, + {Role: bootloader.RoleRunMode, Name: "loader", Hashes: []string{"b", "a"}}, + }, + Kernel: "pc-kernel-other", + KernelRevision: "1234", + KernelCmdlines: []string{`snapd_recovery_mode=run foo`}, + }, { + // recovery system + BrandID: "mybrand", + Model: "foo", + Grade: "dangerous", + ModelSignKeyID: "my-key-id", + AssetChain: []boot.BootAsset{ + // hashes will be sorted + {Role: bootloader.RoleRecovery, Name: "shim", Hashes: []string{"y", "x"}}, + {Role: bootloader.RoleRecovery, Name: "loader", Hashes: []string{"c", "d"}}, + }, + Kernel: "pc-kernel-other", + KernelRevision: "12", + KernelCmdlines: []string{ + // will be sorted + `snapd_recovery_mode=recover snapd_recovery_system=23 foo`, + `snapd_recovery_mode=recover snapd_recovery_system=12 foo`, + }, + }, + } + + predictableChains := boot.ToPredictableBootChains(chains) + d, err := json.Marshal(predictableChains) + c.Assert(err, IsNil) + + var data []map[string]interface{} + err = json.Unmarshal(d, &data) + c.Assert(err, IsNil) + c.Check(data, DeepEquals, []map[string]interface{}{ + { + "model": "foo", + "brand-id": "mybrand", + "grade": "dangerous", + "model-sign-key-id": "my-key-id", + "kernel": "pc-kernel-other", + "kernel-revision": "12", + "kernel-cmdlines": []interface{}{ + `snapd_recovery_mode=recover snapd_recovery_system=12 foo`, + `snapd_recovery_mode=recover snapd_recovery_system=23 foo`, + }, + "asset-chain": []interface{}{ + map[string]interface{}{"role": "recovery", "name": "shim", "hashes": []interface{}{"x", "y"}}, + map[string]interface{}{"role": "recovery", "name": "loader", "hashes": []interface{}{"c", "d"}}, + }, + }, { + "model": "foo", + "brand-id": "mybrand", + "grade": "dangerous", + "model-sign-key-id": "my-key-id", + "kernel": "pc-kernel-other", + "kernel-revision": "1234", + "kernel-cmdlines": []interface{}{"snapd_recovery_mode=run foo"}, + "asset-chain": []interface{}{ + map[string]interface{}{"role": "recovery", "name": "shim", "hashes": []interface{}{"x", "y"}}, + map[string]interface{}{"role": "recovery", "name": "loader", "hashes": []interface{}{"c", "d"}}, + map[string]interface{}{"role": "run-mode", "name": "loader", "hashes": []interface{}{"a", "b"}}, + }, + }, { + "model": "foo", + "brand-id": "mybrand", + "grade": "signed", + "model-sign-key-id": "my-key-id", + "kernel": "pc-kernel-other", + "kernel-revision": "2345", + "kernel-cmdlines": []interface{}{"snapd_recovery_mode=run foo"}, + "asset-chain": []interface{}{ + map[string]interface{}{"role": "recovery", "name": "shim", "hashes": []interface{}{"x", "y"}}, + map[string]interface{}{"role": "recovery", "name": "loader", "hashes": []interface{}{"c", "d"}}, + map[string]interface{}{"role": "run-mode", "name": "loader", "hashes": []interface{}{"x", "z"}}, + }, + }, + }) +} + +func (s *bootchainSuite) TestPredictableBootChainsFields(c *C) { + chainsNil := boot.ToPredictableBootChains(nil) + c.Check(chainsNil, IsNil) + + justOne := []boot.BootChain{ + { + BrandID: "mybrand", + Model: "foo", + Grade: "signed", + ModelSignKeyID: "my-key-id", + Kernel: "pc-kernel-other", + KernelRevision: "2345", + KernelCmdlines: []string{`foo`}, + }, + } + predictableJustOne := boot.ToPredictableBootChains(justOne) + c.Check(predictableJustOne, DeepEquals, boot.PredictableBootChains(justOne)) + + chainsGrade := []boot.BootChain{ + { + Grade: "signed", + }, { + Grade: "dangerous", + }, + } + c.Check(boot.ToPredictableBootChains(chainsGrade), DeepEquals, boot.PredictableBootChains{ + { + Grade: "dangerous", + }, { + Grade: "signed", + }, + }) + + chainsKernel := []boot.BootChain{ + { + Grade: "dangerous", + Kernel: "foo", + }, { + Grade: "dangerous", + Kernel: "bar", + }, + } + c.Check(boot.ToPredictableBootChains(chainsKernel), DeepEquals, boot.PredictableBootChains{ + { + Grade: "dangerous", + Kernel: "bar", + }, { + Grade: "dangerous", + Kernel: "foo", + }, + }) + + chainsKernelRevision := []boot.BootChain{ + { + Kernel: "foo", + KernelRevision: "9", + }, { + Kernel: "foo", + KernelRevision: "21", + }, + } + c.Check(boot.ToPredictableBootChains(chainsKernelRevision), DeepEquals, boot.PredictableBootChains{ + { + Kernel: "foo", + KernelRevision: "21", + }, { + Kernel: "foo", + KernelRevision: "9", + }, + }) + + chainsCmdline := []boot.BootChain{ + { + Grade: "dangerous", + Kernel: "foo", + KernelCmdlines: []string{`panic=1`}, + }, { + Grade: "dangerous", + Kernel: "foo", + KernelCmdlines: []string{`a`}, + }, + } + c.Check(boot.ToPredictableBootChains(chainsCmdline), DeepEquals, boot.PredictableBootChains{ + { + Grade: "dangerous", + Kernel: "foo", + KernelCmdlines: []string{`a`}, + }, { + Grade: "dangerous", + Kernel: "foo", + KernelCmdlines: []string{`panic=1`}, + }, + }) + + chainsModel := []boot.BootChain{ + { + Model: "fridge", + Grade: "dangerous", + Kernel: "foo", + KernelCmdlines: []string{`panic=1`}, + }, { + Model: "box", + Grade: "dangerous", + Kernel: "foo", + KernelCmdlines: []string{`panic=1`}, + }, + } + c.Check(boot.ToPredictableBootChains(chainsModel), DeepEquals, boot.PredictableBootChains{ + { + Model: "box", + Grade: "dangerous", + Kernel: "foo", + KernelCmdlines: []string{`panic=1`}, + }, { + Model: "fridge", + Grade: "dangerous", + Kernel: "foo", + KernelCmdlines: []string{`panic=1`}, + }, + }) + + chainsBrand := []boot.BootChain{ + { + BrandID: "foo", + Model: "box", + Grade: "dangerous", + Kernel: "foo", + KernelCmdlines: []string{`panic=1`}, + }, { + BrandID: "acme", + Model: "box", + Grade: "dangerous", + Kernel: "foo", + KernelCmdlines: []string{`panic=1`}, + }, + } + c.Check(boot.ToPredictableBootChains(chainsBrand), DeepEquals, boot.PredictableBootChains{ + { + BrandID: "acme", + Model: "box", + Grade: "dangerous", + Kernel: "foo", + KernelCmdlines: []string{`panic=1`}, + }, { + BrandID: "foo", + Model: "box", + Grade: "dangerous", + Kernel: "foo", + KernelCmdlines: []string{`panic=1`}, + }, + }) + + chainsKeyID := []boot.BootChain{ + { + BrandID: "foo", + Model: "box", + Grade: "dangerous", + Kernel: "foo", + KernelCmdlines: []string{`panic=1`}, + ModelSignKeyID: "key-2", + }, { + BrandID: "foo", + Model: "box", + Grade: "dangerous", + Kernel: "foo", + KernelCmdlines: []string{`panic=1`}, + ModelSignKeyID: "key-1", + }, + } + c.Check(boot.ToPredictableBootChains(chainsKeyID), DeepEquals, boot.PredictableBootChains{ + { + BrandID: "foo", + Model: "box", + Grade: "dangerous", + Kernel: "foo", + KernelCmdlines: []string{`panic=1`}, + ModelSignKeyID: "key-1", + }, { + BrandID: "foo", + Model: "box", + Grade: "dangerous", + Kernel: "foo", + KernelCmdlines: []string{`panic=1`}, + ModelSignKeyID: "key-2", + }, + }) + + chainsAssets := []boot.BootChain{ + { + BrandID: "foo", + Model: "box", + Grade: "dangerous", + ModelSignKeyID: "key-1", + AssetChain: []boot.BootAsset{ + // will be sorted + {Hashes: []string{"b", "a"}}, + }, + Kernel: "foo", + KernelCmdlines: []string{`panic=1`}, + }, { + BrandID: "foo", + Model: "box", + Grade: "dangerous", + ModelSignKeyID: "key-1", + AssetChain: []boot.BootAsset{ + {Hashes: []string{"b"}}, + }, + Kernel: "foo", + KernelCmdlines: []string{`panic=1`}, + }, + } + c.Check(boot.ToPredictableBootChains(chainsAssets), DeepEquals, boot.PredictableBootChains{ + { + BrandID: "foo", + Model: "box", + Grade: "dangerous", + ModelSignKeyID: "key-1", + AssetChain: []boot.BootAsset{ + {Hashes: []string{"b"}}, + }, + Kernel: "foo", + KernelCmdlines: []string{`panic=1`}, + }, { + BrandID: "foo", + Model: "box", + Grade: "dangerous", + ModelSignKeyID: "key-1", + AssetChain: []boot.BootAsset{ + {Hashes: []string{"a", "b"}}, + }, + Kernel: "foo", + KernelCmdlines: []string{`panic=1`}, + }, + }) + + chainsFewerAssets := []boot.BootChain{ + { + AssetChain: []boot.BootAsset{ + {Hashes: []string{"b", "a"}}, + {Hashes: []string{"c", "d"}}, + }, + }, { + AssetChain: []boot.BootAsset{ + {Hashes: []string{"b"}}, + }, + }, + } + c.Check(boot.ToPredictableBootChains(chainsFewerAssets), DeepEquals, boot.PredictableBootChains{ + { + AssetChain: []boot.BootAsset{ + {Hashes: []string{"b"}}, + }, + }, { + AssetChain: []boot.BootAsset{ + {Hashes: []string{"a", "b"}}, + {Hashes: []string{"c", "d"}}, + }, + }, + }) + + // not confused if 2 chains are identical + chainsIdenticalAssets := []boot.BootChain{ + { + BrandID: "foo", + Model: "box", + ModelSignKeyID: "key-1", + AssetChain: []boot.BootAsset{ + {Name: "asset", Hashes: []string{"a", "b"}}, + {Name: "asset", Hashes: []string{"a", "b"}}, + }, + Grade: "dangerous", + Kernel: "foo", + KernelCmdlines: []string{`panic=1`}, + }, { + BrandID: "foo", + Model: "box", + Grade: "dangerous", + ModelSignKeyID: "key-1", + AssetChain: []boot.BootAsset{ + {Name: "asset", Hashes: []string{"a", "b"}}, + {Name: "asset", Hashes: []string{"a", "b"}}, + }, + Kernel: "foo", + KernelCmdlines: []string{`panic=1`}, + }, + } + c.Check(boot.ToPredictableBootChains(chainsIdenticalAssets), DeepEquals, boot.PredictableBootChains(chainsIdenticalAssets)) +} + +func (s *bootchainSuite) TestPredictableBootChainsSortOrder(c *C) { + // check that sort order is model info, assets, kernel, kernel cmdline + + chains := []boot.BootChain{ + { + Model: "b", + AssetChain: []boot.BootAsset{ + {Name: "asset", Hashes: []string{"y"}}, + }, + Kernel: "k1", + KernelCmdlines: []string{"cm=1"}, + }, + { + Model: "b", + AssetChain: []boot.BootAsset{ + {Name: "asset", Hashes: []string{"y"}}, + }, + Kernel: "k2", + KernelCmdlines: []string{"cm=1"}, + }, + { + Model: "a", + AssetChain: []boot.BootAsset{ + {Name: "asset", Hashes: []string{"y"}}, + }, + Kernel: "k1", + KernelCmdlines: []string{"cm=1"}, + }, + { + Model: "a", + AssetChain: []boot.BootAsset{ + {Name: "asset", Hashes: []string{"y"}}, + }, + Kernel: "k2", + KernelCmdlines: []string{"cm=1"}, + }, + { + Model: "b", + AssetChain: []boot.BootAsset{ + {Name: "asset", Hashes: []string{"y"}}, + }, + Kernel: "k1", + KernelCmdlines: []string{"cm=2"}, + }, + { + Model: "b", + AssetChain: []boot.BootAsset{ + {Name: "asset", Hashes: []string{"y"}}, + }, + Kernel: "k2", + KernelCmdlines: []string{"cm=2"}, + }, + { + Model: "a", + AssetChain: []boot.BootAsset{ + {Name: "asset", Hashes: []string{"y"}}, + }, + Kernel: "k1", + KernelCmdlines: []string{"cm=2"}, + }, + { + Model: "a", + AssetChain: []boot.BootAsset{ + {Name: "asset", Hashes: []string{"y"}}, + }, + Kernel: "k2", + KernelCmdlines: []string{"cm=2"}, + }, + { + Model: "b", + AssetChain: []boot.BootAsset{ + {Name: "asset", Hashes: []string{"x"}}, + }, + Kernel: "k1", + KernelCmdlines: []string{"cm=1"}, + }, + { + Model: "b", + AssetChain: []boot.BootAsset{ + {Name: "asset", Hashes: []string{"x"}}, + }, + Kernel: "k2", + KernelCmdlines: []string{"cm=1"}, + }, + { + Model: "a", + AssetChain: []boot.BootAsset{ + {Name: "asset", Hashes: []string{"x"}}, + }, + Kernel: "k1", + KernelCmdlines: []string{"cm=1"}, + }, + { + Model: "a", + AssetChain: []boot.BootAsset{ + {Name: "asset", Hashes: []string{"x"}}, + }, + Kernel: "k2", + KernelCmdlines: []string{"cm=1"}, + }, + { + Model: "b", + AssetChain: []boot.BootAsset{ + {Name: "asset", Hashes: []string{"x"}}, + }, + Kernel: "k1", + KernelCmdlines: []string{"cm=2"}, + }, + { + Model: "b", + AssetChain: []boot.BootAsset{ + {Name: "asset", Hashes: []string{"x"}}, + }, + Kernel: "k2", + KernelCmdlines: []string{"cm=2"}, + }, + { + Model: "a", + AssetChain: []boot.BootAsset{ + {Name: "asset", Hashes: []string{"x"}}, + }, + Kernel: "k1", + KernelCmdlines: []string{"cm=2"}, + }, + { + Model: "a", + AssetChain: []boot.BootAsset{ + {Name: "asset", Hashes: []string{"x"}}, + }, + Kernel: "k2", + KernelCmdlines: []string{"cm=2"}, + }, + { + Model: "a", + AssetChain: []boot.BootAsset{ + {Name: "asset", Hashes: []string{"y"}}, + }, + Kernel: "k2", + KernelCmdlines: []string{"cm=1", "cm=2"}, + }, + { + Model: "a", + AssetChain: []boot.BootAsset{ + {Name: "asset", Hashes: []string{"y"}}, + }, + Kernel: "k1", + KernelCmdlines: []string{"cm=1", "cm=2"}, + }, + } + predictable := boot.ToPredictableBootChains(chains) + c.Check(predictable, DeepEquals, boot.PredictableBootChains{ + { + Model: "a", + AssetChain: []boot.BootAsset{ + {Name: "asset", Hashes: []string{"x"}}, + }, + Kernel: "k1", + KernelCmdlines: []string{"cm=1"}, + }, + { + Model: "a", + AssetChain: []boot.BootAsset{ + {Name: "asset", Hashes: []string{"x"}}, + }, + Kernel: "k1", + KernelCmdlines: []string{"cm=2"}, + }, + { + Model: "a", + AssetChain: []boot.BootAsset{ + {Name: "asset", Hashes: []string{"x"}}, + }, + Kernel: "k2", + KernelCmdlines: []string{"cm=1"}, + }, + { + Model: "a", + AssetChain: []boot.BootAsset{ + {Name: "asset", Hashes: []string{"x"}}, + }, + Kernel: "k2", + KernelCmdlines: []string{"cm=2"}, + }, + { + Model: "a", + AssetChain: []boot.BootAsset{ + {Name: "asset", Hashes: []string{"y"}}, + }, + Kernel: "k1", + KernelCmdlines: []string{"cm=1"}, + }, + { + Model: "a", + AssetChain: []boot.BootAsset{ + {Name: "asset", Hashes: []string{"y"}}, + }, + Kernel: "k1", + KernelCmdlines: []string{"cm=2"}, + }, + { + Model: "a", + AssetChain: []boot.BootAsset{ + {Name: "asset", Hashes: []string{"y"}}, + }, + Kernel: "k1", + KernelCmdlines: []string{"cm=1", "cm=2"}, + }, + { + Model: "a", + AssetChain: []boot.BootAsset{ + {Name: "asset", Hashes: []string{"y"}}, + }, + Kernel: "k2", + KernelCmdlines: []string{"cm=1"}, + }, + { + Model: "a", + AssetChain: []boot.BootAsset{ + {Name: "asset", Hashes: []string{"y"}}, + }, + Kernel: "k2", + KernelCmdlines: []string{"cm=2"}, + }, + { + Model: "a", + AssetChain: []boot.BootAsset{ + {Name: "asset", Hashes: []string{"y"}}, + }, + Kernel: "k2", + KernelCmdlines: []string{"cm=1", "cm=2"}, + }, + { + Model: "b", + AssetChain: []boot.BootAsset{ + {Name: "asset", Hashes: []string{"x"}}, + }, + Kernel: "k1", + KernelCmdlines: []string{"cm=1"}, + }, + { + Model: "b", + AssetChain: []boot.BootAsset{ + {Name: "asset", Hashes: []string{"x"}}, + }, + Kernel: "k1", + KernelCmdlines: []string{"cm=2"}, + }, + { + Model: "b", + AssetChain: []boot.BootAsset{ + {Name: "asset", Hashes: []string{"x"}}, + }, + Kernel: "k2", + KernelCmdlines: []string{"cm=1"}, + }, + { + Model: "b", + AssetChain: []boot.BootAsset{ + {Name: "asset", Hashes: []string{"x"}}, + }, + Kernel: "k2", + KernelCmdlines: []string{"cm=2"}, + }, + { + Model: "b", + AssetChain: []boot.BootAsset{ + {Name: "asset", Hashes: []string{"y"}}, + }, + Kernel: "k1", + KernelCmdlines: []string{"cm=1"}, + }, + { + Model: "b", + AssetChain: []boot.BootAsset{ + {Name: "asset", Hashes: []string{"y"}}, + }, + Kernel: "k1", + KernelCmdlines: []string{"cm=2"}, + }, + { + Model: "b", + AssetChain: []boot.BootAsset{ + {Name: "asset", Hashes: []string{"y"}}, + }, + Kernel: "k2", + KernelCmdlines: []string{"cm=1"}, + }, + { + Model: "b", + AssetChain: []boot.BootAsset{ + {Name: "asset", Hashes: []string{"y"}}, + }, + Kernel: "k2", + KernelCmdlines: []string{"cm=2"}, + }, + }) +} + +func printChain(c *C, chain *secboot.LoadChain, prefix string) { + c.Logf("%v %v", prefix, chain.BootFile) + for _, n := range chain.Next { + printChain(c, n, prefix+"-") + } +} + +// cPath returns a path under boot assets cache directory +func cPath(p string) string { + return filepath.Join(dirs.SnapBootAssetsDir, p) +} + +// nbf is bootloader.NewBootFile but shorter +var nbf = bootloader.NewBootFile + +func (s *bootchainSuite) TestBootAssetsToLoadChainTrivialKernel(c *C) { + kbl := bootloader.NewBootFile("pc-kernel", "kernel.efi", bootloader.RoleRunMode) + + chains, err := boot.BootAssetsToLoadChains(nil, kbl, nil) + c.Assert(err, IsNil) + + c.Check(chains, DeepEquals, []*secboot.LoadChain{ + secboot.NewLoadChain(nbf("pc-kernel", "kernel.efi", bootloader.RoleRunMode)), + }) +} + +func (s *bootchainSuite) TestBootAssetsToLoadChainErr(c *C) { + kbl := bootloader.NewBootFile("pc-kernel", "kernel.efi", bootloader.RoleRunMode) + + assets := []boot.BootAsset{ + {Name: "shim", Hashes: []string{"hash0"}, Role: bootloader.RoleRecovery}, + {Name: "loader-recovery", Hashes: []string{"hash0"}, Role: bootloader.RoleRecovery}, + {Name: "loader-run", Hashes: []string{"hash0"}, Role: bootloader.RoleRunMode}, + } + + blNames := map[bootloader.Role]string{ + bootloader.RoleRecovery: "recovery-bl", + // missing bootloader name for role "run-mode" + } + // fails when probing the shim asset in the cache + chains, err := boot.BootAssetsToLoadChains(assets, kbl, blNames) + c.Assert(err, ErrorMatches, "file .*/recovery-bl/shim-hash0 not found in boot assets cache") + c.Check(chains, IsNil) + // make it work now + c.Assert(os.MkdirAll(filepath.Dir(cPath("recovery-bl/shim-hash0")), 0755), IsNil) + c.Assert(ioutil.WriteFile(cPath("recovery-bl/shim-hash0"), nil, 0644), IsNil) + + // nested error bubbled up + chains, err = boot.BootAssetsToLoadChains(assets, kbl, blNames) + c.Assert(err, ErrorMatches, "file .*/recovery-bl/loader-recovery-hash0 not found in boot assets cache") + c.Check(chains, IsNil) + // again, make it work + c.Assert(os.MkdirAll(filepath.Dir(cPath("recovery-bl/loader-recovery-hash0")), 0755), IsNil) + c.Assert(ioutil.WriteFile(cPath("recovery-bl/loader-recovery-hash0"), nil, 0644), IsNil) + + // fails on missing bootloader name for role "run-mode" + chains, err = boot.BootAssetsToLoadChains(assets, kbl, blNames) + c.Assert(err, ErrorMatches, `internal error: no bootloader name for boot asset role "run-mode"`) + c.Check(chains, IsNil) +} + +func (s *bootchainSuite) TestBootAssetsToLoadChainSimpleChain(c *C) { + kbl := bootloader.NewBootFile("pc-kernel", "kernel.efi", bootloader.RoleRunMode) + + assets := []boot.BootAsset{ + {Name: "shim", Hashes: []string{"hash0"}, Role: bootloader.RoleRecovery}, + {Name: "loader-recovery", Hashes: []string{"hash0"}, Role: bootloader.RoleRecovery}, + {Name: "loader-run", Hashes: []string{"hash0"}, Role: bootloader.RoleRunMode}, + } + + // mock relevant files in cache + for _, name := range []string{ + "recovery-bl/shim-hash0", + "recovery-bl/loader-recovery-hash0", + "run-bl/loader-run-hash0", + } { + p := filepath.Join(dirs.SnapBootAssetsDir, name) + c.Assert(os.MkdirAll(filepath.Dir(p), 0755), IsNil) + c.Assert(ioutil.WriteFile(p, nil, 0644), IsNil) + } + + blNames := map[bootloader.Role]string{ + bootloader.RoleRecovery: "recovery-bl", + bootloader.RoleRunMode: "run-bl", + } + + chains, err := boot.BootAssetsToLoadChains(assets, kbl, blNames) + c.Assert(err, IsNil) + + c.Logf("got:") + for _, ch := range chains { + printChain(c, ch, "-") + } + + expected := []*secboot.LoadChain{ + secboot.NewLoadChain(nbf("", cPath("recovery-bl/shim-hash0"), bootloader.RoleRecovery), + secboot.NewLoadChain(nbf("", cPath("recovery-bl/loader-recovery-hash0"), bootloader.RoleRecovery), + secboot.NewLoadChain(nbf("", cPath("run-bl/loader-run-hash0"), bootloader.RoleRunMode), + secboot.NewLoadChain(nbf("pc-kernel", "kernel.efi", bootloader.RoleRunMode))))), + } + c.Check(chains, DeepEquals, expected) +} + +func (s *bootchainSuite) TestBootAssetsToLoadChainWithAlternativeChains(c *C) { + kbl := bootloader.NewBootFile("pc-kernel", "kernel.efi", bootloader.RoleRunMode) + + assets := []boot.BootAsset{ + {Name: "shim", Hashes: []string{"hash0", "hash1"}, Role: bootloader.RoleRecovery}, + {Name: "loader-recovery", Hashes: []string{"hash0", "hash1"}, Role: bootloader.RoleRecovery}, + {Name: "loader-run", Hashes: []string{"hash0", "hash1"}, Role: bootloader.RoleRunMode}, + } + + // mock relevant files in cache + for _, name := range []string{ + "recovery-bl/shim-hash0", "recovery-bl/shim-hash1", + "recovery-bl/loader-recovery-hash0", + "recovery-bl/loader-recovery-hash1", + "run-bl/loader-run-hash0", + "run-bl/loader-run-hash1", + } { + p := filepath.Join(dirs.SnapBootAssetsDir, name) + c.Assert(os.MkdirAll(filepath.Dir(p), 0755), IsNil) + c.Assert(ioutil.WriteFile(p, nil, 0644), IsNil) + } + + blNames := map[bootloader.Role]string{ + bootloader.RoleRecovery: "recovery-bl", + bootloader.RoleRunMode: "run-bl", + } + chains, err := boot.BootAssetsToLoadChains(assets, kbl, blNames) + c.Assert(err, IsNil) + + c.Logf("got:") + for _, ch := range chains { + printChain(c, ch, "-") + } + + expected := []*secboot.LoadChain{ + secboot.NewLoadChain(nbf("", cPath("recovery-bl/shim-hash0"), bootloader.RoleRecovery), + secboot.NewLoadChain(nbf("", cPath("recovery-bl/loader-recovery-hash0"), bootloader.RoleRecovery), + secboot.NewLoadChain(nbf("", cPath("run-bl/loader-run-hash0"), bootloader.RoleRunMode), + secboot.NewLoadChain(nbf("pc-kernel", "kernel.efi", bootloader.RoleRunMode))), + secboot.NewLoadChain(nbf("", cPath("run-bl/loader-run-hash1"), bootloader.RoleRunMode), + secboot.NewLoadChain(nbf("pc-kernel", "kernel.efi", bootloader.RoleRunMode)))), + secboot.NewLoadChain(nbf("", cPath("recovery-bl/loader-recovery-hash1"), bootloader.RoleRecovery), + secboot.NewLoadChain(nbf("", cPath("run-bl/loader-run-hash0"), bootloader.RoleRunMode), + secboot.NewLoadChain(nbf("pc-kernel", "kernel.efi", bootloader.RoleRunMode))), + secboot.NewLoadChain(nbf("", cPath("run-bl/loader-run-hash1"), bootloader.RoleRunMode), + secboot.NewLoadChain(nbf("pc-kernel", "kernel.efi", bootloader.RoleRunMode))))), + secboot.NewLoadChain(nbf("", cPath("recovery-bl/shim-hash1"), bootloader.RoleRecovery), + secboot.NewLoadChain(nbf("", cPath("recovery-bl/loader-recovery-hash0"), bootloader.RoleRecovery), + secboot.NewLoadChain(nbf("", cPath("run-bl/loader-run-hash0"), bootloader.RoleRunMode), + secboot.NewLoadChain(nbf("pc-kernel", "kernel.efi", bootloader.RoleRunMode))), + secboot.NewLoadChain(nbf("", cPath("run-bl/loader-run-hash1"), bootloader.RoleRunMode), + secboot.NewLoadChain(nbf("pc-kernel", "kernel.efi", bootloader.RoleRunMode)))), + secboot.NewLoadChain(nbf("", cPath("recovery-bl/loader-recovery-hash1"), bootloader.RoleRecovery), + secboot.NewLoadChain(nbf("", cPath("run-bl/loader-run-hash0"), bootloader.RoleRunMode), + secboot.NewLoadChain(nbf("pc-kernel", "kernel.efi", bootloader.RoleRunMode))), + secboot.NewLoadChain(nbf("", cPath("run-bl/loader-run-hash1"), bootloader.RoleRunMode), + secboot.NewLoadChain(nbf("pc-kernel", "kernel.efi", bootloader.RoleRunMode))))), + } + c.Check(chains, DeepEquals, expected) +} + +func (s *sealSuite) TestReadWriteBootChains(c *C) { + if os.Geteuid() == 0 { + c.Skip("the test cannot be run by the root user") + } + + chains := []boot.BootChain{ + { + BrandID: "mybrand", + Model: "foo", + Grade: "signed", + ModelSignKeyID: "my-key-id", + AssetChain: []boot.BootAsset{ + // hashes will be sorted + {Role: bootloader.RoleRecovery, Name: "shim", Hashes: []string{"x", "y"}}, + {Role: bootloader.RoleRecovery, Name: "loader", Hashes: []string{"c", "d"}}, + {Role: bootloader.RoleRunMode, Name: "loader", Hashes: []string{"z", "x"}}, + }, + Kernel: "pc-kernel-other", + KernelRevision: "2345", + KernelCmdlines: []string{`snapd_recovery_mode=run foo`}, + }, { + BrandID: "mybrand", + Model: "foo", + Grade: "dangerous", + ModelSignKeyID: "my-key-id", + AssetChain: []boot.BootAsset{ + // hashes will be sorted + {Role: bootloader.RoleRecovery, Name: "shim", Hashes: []string{"y", "x"}}, + {Role: bootloader.RoleRecovery, Name: "loader", Hashes: []string{"c", "d"}}, + }, + Kernel: "pc-kernel-recovery", + KernelRevision: "1234", + KernelCmdlines: []string{`snapd_recovery_mode=recover foo`}, + }, + } + + pbc := boot.ToPredictableBootChains(chains) + + rootdir := c.MkDir() + + expected := `{"reseal-count":0,"boot-chains":[{"brand-id":"mybrand","model":"foo","grade":"dangerous","model-sign-key-id":"my-key-id","asset-chain":[{"role":"recovery","name":"shim","hashes":["x","y"]},{"role":"recovery","name":"loader","hashes":["c","d"]}],"kernel":"pc-kernel-recovery","kernel-revision":"1234","kernel-cmdlines":["snapd_recovery_mode=recover foo"]},{"brand-id":"mybrand","model":"foo","grade":"signed","model-sign-key-id":"my-key-id","asset-chain":[{"role":"recovery","name":"shim","hashes":["x","y"]},{"role":"recovery","name":"loader","hashes":["c","d"]},{"role":"run-mode","name":"loader","hashes":["x","z"]}],"kernel":"pc-kernel-other","kernel-revision":"2345","kernel-cmdlines":["snapd_recovery_mode=run foo"]}]} +` + // creates a complete tree and writes a file + err := boot.WriteBootChains(pbc, filepath.Join(dirs.SnapFDEDirUnder(rootdir), "boot-chains"), 0) + c.Assert(err, IsNil) + c.Check(filepath.Join(dirs.SnapFDEDirUnder(rootdir), "boot-chains"), testutil.FileEquals, expected) + + fi, err := os.Stat(filepath.Join(dirs.SnapFDEDirUnder(rootdir), "boot-chains")) + c.Assert(err, IsNil) + c.Check(fi.Mode().Perm(), Equals, os.FileMode(0600)) + + loaded, cnt, err := boot.ReadBootChains(filepath.Join(dirs.SnapFDEDirUnder(rootdir), "boot-chains")) + c.Assert(err, IsNil) + c.Check(loaded, DeepEquals, pbc) + c.Check(cnt, Equals, 0) + // boot chains should be same for reseal purpose + c.Check(boot.PredictableBootChainsEqualForReseal(pbc, loaded), Equals, boot.BootChainEquivalent) + + // write them again with count > 0 + err = boot.WriteBootChains(pbc, filepath.Join(dirs.SnapFDEDirUnder(rootdir), "boot-chains"), 99) + c.Assert(err, IsNil) + + _, cnt, err = boot.ReadBootChains(filepath.Join(dirs.SnapFDEDirUnder(rootdir), "boot-chains")) + c.Assert(err, IsNil) + c.Check(cnt, Equals, 99) + + // make device/fde directory read only so that writing fails + otherRootdir := c.MkDir() + c.Assert(os.MkdirAll(dirs.SnapFDEDirUnder(otherRootdir), 0755), IsNil) + c.Assert(os.Chmod(dirs.SnapFDEDirUnder(otherRootdir), 0000), IsNil) + defer os.Chmod(dirs.SnapFDEDirUnder(otherRootdir), 0755) + + err = boot.WriteBootChains(pbc, filepath.Join(dirs.SnapFDEDirUnder(otherRootdir), "boot-chains"), 0) + c.Assert(err, ErrorMatches, `cannot create a temporary boot chains file: open .*/boot-chains\.[a-zA-Z0-9]+~: permission denied`) + + // make the original file non readable + c.Assert(os.Chmod(filepath.Join(dirs.SnapFDEDirUnder(rootdir), "boot-chains"), 0000), IsNil) + defer os.Chmod(filepath.Join(dirs.SnapFDEDirUnder(rootdir), "boot-chains"), 0755) + loaded, _, err = boot.ReadBootChains(filepath.Join(dirs.SnapFDEDirUnder(rootdir), "boot-chains")) + c.Assert(err, ErrorMatches, "cannot open existing boot chains data file: open .*/boot-chains: permission denied") + c.Check(loaded, IsNil) + + // loading from a file that does not exist yields a nil boot chain + // and 0 count + loaded, cnt, err = boot.ReadBootChains("does-not-exist") + c.Assert(err, IsNil) + c.Check(loaded, IsNil) + c.Check(cnt, Equals, 0) +} diff -Nru snapd-2.45.1+20.04.2/boot/boot.go snapd-2.48.3+20.04/boot/boot.go --- snapd-2.45.1+20.04.2/boot/boot.go 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/boot/boot.go 2021-02-02 08:21:12.000000000 +0000 @@ -23,6 +23,7 @@ "errors" "fmt" + "github.com/snapcore/snapd/asserts" "github.com/snapcore/snapd/bootloader" "github.com/snapcore/snapd/snap" ) @@ -89,6 +90,8 @@ Base() string HasModeenv() bool + + Model() *asserts.Model } // Participant figures out what the BootParticipant is for the given @@ -113,9 +116,12 @@ // bootloaderOptionsForDeviceKernel returns a set of bootloader options that // enable correct kernel extraction and removal for given device func bootloaderOptionsForDeviceKernel(dev Device) *bootloader.Options { + if !dev.HasModeenv() { + return nil + } + // find the run-mode bootloader with its kernel support for UC20 return &bootloader.Options{ - // unified extractable kernel if in uc20 mode - ExtractedRunKernelImage: dev.HasModeenv(), + Role: bootloader.RoleRunMode, } } @@ -163,16 +169,21 @@ return true } -// bootState exposes the boot state for a type of boot snap. +// bootState exposes the boot state for a type of boot snap during +// normal running state, i.e. after the pivot_root and after the initramfs. type bootState interface { // revisions retrieves the revisions of the current snap and // the try snap (only the latter might not be set), and // the status of the trying snap. + // Note that the error could be only specific to the try snap, in which case + // curSnap may still be non-nil and valid. Callers concerned with robustness + // should always inspect a non-nil error with isTrySnapError, and use + // 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. + // is done via the returned bootStateUpdate's commit method. setNext(s snap.PlaceInfo) (rebootRequired bool, u bootStateUpdate, err error) // markSuccessful lazily implements marking the boot @@ -182,6 +193,14 @@ markSuccessful(bootStateUpdate) (bootStateUpdate, error) } +// successfulBootState exposes the state of resources requiring bookkeeping on a +// successful boot. +type successfulBootState interface { + // markSuccessful lazily implements marking the boot + // successful for the given type of resource. + markSuccessful(bootStateUpdate) (bootStateUpdate, error) +} + // bootStateFor finds the right bootState implementation of the given // snap type and Device, if applicable. func bootStateFor(typ snap.Type, dev Device) (s bootState, err error) { @@ -194,9 +213,9 @@ } switch typ { case snap.TypeOS, snap.TypeBase: - return newBootState(snap.TypeBase), nil + return newBootState(snap.TypeBase, dev), nil case snap.TypeKernel: - return newBootState(snap.TypeKernel), nil + return newBootState(snap.TypeKernel, dev), nil default: return nil, fmt.Errorf("internal error: no boot state handling for snap type %q", typ) } @@ -320,6 +339,15 @@ } } + if dev.HasModeenv() { + b := trustedAssetsBootState(dev) + var err error + u, err = b.markSuccessful(u) + if err != nil { + return fmt.Errorf(errPrefix, err) + } + } + if u != nil { if err := u.commit(); err != nil { return fmt.Errorf(errPrefix, err) @@ -348,7 +376,7 @@ opts := &bootloader.Options{ // setup the recovery bootloader - Recovery: true, + Role: bootloader.RoleRecovery, } // TODO:UC20: should the recovery partition stay around as RW during run // mode all the time? diff -Nru snapd-2.45.1+20.04.2/boot/boot_robustness_test.go snapd-2.48.3+20.04/boot/boot_robustness_test.go --- snapd-2.45.1+20.04.2/boot/boot_robustness_test.go 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/boot/boot_robustness_test.go 2021-02-02 08:21:12.000000000 +0000 @@ -192,7 +192,7 @@ } func (s *bootenv20Suite) TestHappyMarkBootSuccessful20KernelUpgradeUnexpectedReboots(c *C) { - coreDev := boottest.MockUC20Device("some-snap") + coreDev := boottest.MockUC20Device("", nil) c.Assert(coreDev.HasModeenv(), Equals, true) tt := []struct { @@ -256,7 +256,7 @@ } func (s *bootenv20Suite) TestHappySetNextBoot20KernelUpgradeUnexpectedReboots(c *C) { - coreDev := boottest.MockUC20Device("pc-kernel") + coreDev := boottest.MockUC20Device("", nil) c.Assert(coreDev.HasModeenv(), Equals, true) tt := []struct { diff -Nru snapd-2.45.1+20.04.2/boot/bootstate16.go snapd-2.48.3+20.04/boot/bootstate16.go --- snapd-2.45.1+20.04.2/boot/bootstate16.go 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/boot/bootstate16.go 2021-02-02 08:21:12.000000000 +0000 @@ -31,7 +31,7 @@ errName string } -func newBootState16(typ snap.Type) bootState { +func newBootState16(typ snap.Type, dev Device) bootState { var varSuffix, errName string switch typ { case snap.TypeKernel: @@ -74,6 +74,7 @@ if vName == "snap_mode" { status = v } else { + // TODO: use trySnapError here somehow? if v == "" { return nil, nil, "", fmt.Errorf("cannot get name and revision of %s (%s): boot variable unset", s16.errName, vName) } @@ -136,14 +137,18 @@ env := u16.env toCommit := u16.toCommit + tryBootVar := fmt.Sprintf("snap_try_%s", s16.varSuffix) + bootVar := fmt.Sprintf("snap_%s", s16.varSuffix) + // snap_mode goes from "" -> "try" -> "trying" -> "" // so if we are not in "trying" mode, nothing to do here if env["snap_mode"] != TryingStatus { + // clean the try var anyways in case it was leftover from a rollback, + // etc. + toCommit[tryBootVar] = "" return u16, nil } - tryBootVar := fmt.Sprintf("snap_try_%s", s16.varSuffix) - bootVar := fmt.Sprintf("snap_%s", s16.varSuffix) // update the boot vars if env[tryBootVar] != "" { toCommit[bootVar] = env[tryBootVar] diff -Nru snapd-2.45.1+20.04.2/boot/bootstate20_bloader_kernel_state.go snapd-2.48.3+20.04/boot/bootstate20_bloader_kernel_state.go --- snapd-2.45.1+20.04.2/boot/bootstate20_bloader_kernel_state.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48.3+20.04/boot/bootstate20_bloader_kernel_state.go 2021-02-02 08:21:12.000000000 +0000 @@ -0,0 +1,295 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2019-2020 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 boot + +import ( + "fmt" + + "github.com/snapcore/snapd/bootloader" + "github.com/snapcore/snapd/snap" +) + +// extractedRunKernelImageBootloaderKernelState implements bootloaderKernelState20 for +// bootloaders that implement ExtractedRunKernelImageBootloader +type extractedRunKernelImageBootloaderKernelState struct { + // the bootloader + ebl bootloader.ExtractedRunKernelImageBootloader + // the current kernel status as read by the bootloader's bootenv + currentKernelStatus string + // the current kernel on the bootloader (not the try-kernel) + currentKernel snap.PlaceInfo +} + +func (bks *extractedRunKernelImageBootloaderKernelState) load() error { + // get the kernel_status + m, err := bks.ebl.GetBootVars("kernel_status") + if err != nil { + return err + } + + bks.currentKernelStatus = m["kernel_status"] + + // get the current kernel for this bootloader to compare during commit() for + // markSuccessful() if we booted the current kernel or not + kernel, err := bks.ebl.Kernel() + if err != nil { + return fmt.Errorf("cannot identify kernel snap with bootloader %s: %v", bks.ebl.Name(), err) + } + + bks.currentKernel = kernel + + return nil +} + +func (bks *extractedRunKernelImageBootloaderKernelState) kernel() snap.PlaceInfo { + return bks.currentKernel +} + +func (bks *extractedRunKernelImageBootloaderKernelState) tryKernel() (snap.PlaceInfo, error) { + return bks.ebl.TryKernel() +} + +func (bks *extractedRunKernelImageBootloaderKernelState) kernelStatus() string { + return bks.currentKernelStatus +} + +func (bks *extractedRunKernelImageBootloaderKernelState) markSuccessfulKernel(sn snap.PlaceInfo) error { + // set the boot vars first, then enable the successful kernel, then disable + // the old try-kernel, see the comment in bootState20MarkSuccessful.commit() + // for details + + // the ordering here is very important for boot reliability! + + // If we have successfully just booted from a try-kernel and are + // marking it successful (this implies that snap_kernel=="trying" as set + // by the boot script), we need to do the following in order (since we + // have the added complexity of moving the kernel symlink): + // 1. Update kernel_status to "" + // 2. Move kernel symlink to point to the new try kernel + // 3. Remove try-kernel symlink + // 4. Remove old kernel from modeenv (this happens one level up from this + // function) + // + // If we got rebooted after step 1, then the bootloader is booting the wrong + // kernel, but is at least booting a known good kernel and snapd in + // user-space would be able to figure out the inconsistency. + // If we got rebooted after step 2, the bootloader would boot from the new + // try-kernel which is okay because we were in the middle of committing + // that new kernel as good and all that's left is for snapd to cleanup + // the left-over try-kernel symlink. + // + // If instead we had moved the kernel symlink first to point to the new try + // kernel, and got rebooted before the kernel_status was updated, we would + // have kernel_status="trying" which would cause the bootloader to think + // the boot failed, and revert to booting using the kernel symlink, but that + // now points to the new kernel we were trying and we did not successfully + // boot from that kernel to know we should trust it. + // + // Removing the old kernel from the modeenv needs to happen after it is + // impossible for the bootloader to boot from that kernel, otherwise we + // could end up in a state where the bootloader doesn't want to boot the + // new kernel, but the initramfs doesn't trust the old kernel and we are + // stuck. As such, do this last, after the symlink no longer exists. + // + // The try-kernel symlink removal should happen last because it will not + // affect anything, except that if it was removed before updating + // kernel_status to "", the bootloader will think that the try kernel failed + // to boot and fall back to booting the old kernel which is safe. + + // always set the boot vars first before mutating any of the kernel symlinks + // etc. + // for markSuccessful, we will always set the status to Default, even if + // technically this boot wasn't "successful" - it was successful in the + // sense that we booted some combination of boot snaps and made it all the + // way to snapd in user space + if bks.currentKernelStatus != DefaultStatus { + m := map[string]string{ + "kernel_status": DefaultStatus, + } + + // set the boot variables + err := bks.ebl.SetBootVars(m) + if err != nil { + return err + } + } + + // if the kernel we booted is not the current one, we must have tried + // a new kernel, so enable that one as the current one now + if bks.currentKernel.Filename() != sn.Filename() { + err := bks.ebl.EnableKernel(sn) + if err != nil { + return err + } + } + + // always disable the try kernel snap to cleanup in case we have upgrade + // failures which leave behind try-kernel.efi + err := bks.ebl.DisableTryKernel() + if err != nil { + return err + } + + return nil +} + +func (bks *extractedRunKernelImageBootloaderKernelState) setNextKernel(sn snap.PlaceInfo, status string) error { + // always enable the try-kernel first, if we did the reverse and got + // rebooted after setting the boot vars but before enabling the try-kernel + // we could get stuck where the bootloader can't find the try-kernel and + // gets stuck waiting for a user to reboot, at which point we would fallback + // see i.e. https://github.com/snapcore/pc-amd64-gadget/issues/36 + if sn.Filename() != bks.currentKernel.Filename() { + err := bks.ebl.EnableTryKernel(sn) + if err != nil { + return err + } + } + + // only if the new kernel status is different from what we read should we + // run SetBootVars() to minimize wear/corruption possibility on the bootenv + if status != bks.currentKernelStatus { + m := map[string]string{ + "kernel_status": status, + } + + // 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 +type envRefExtractedKernelBootloaderKernelState struct { + // the bootloader + bl bootloader.Bootloader + + // the current state of env + env map[string]string + + // the state of env to commit + toCommit map[string]string + + // the current kernel + kern snap.PlaceInfo +} + +func (envbks *envRefExtractedKernelBootloaderKernelState) load() error { + // for uc20, we only care about kernel_status, snap_kernel, and + // snap_try_kernel + m, err := envbks.bl.GetBootVars("kernel_status", "snap_kernel", "snap_try_kernel") + if err != nil { + return err + } + + // the default commit env is the same state as the current env + envbks.env = m + envbks.toCommit = make(map[string]string, len(m)) + for k, v := range m { + envbks.toCommit[k] = v + } + + // snap_kernel is the current kernel snap + // parse the filename here because the kernel() method doesn't return an err + sn, err := snap.ParsePlaceInfoFromSnapFileName(envbks.env["snap_kernel"]) + if err != nil { + return err + } + + envbks.kern = sn + + return nil +} + +func (envbks *envRefExtractedKernelBootloaderKernelState) kernel() snap.PlaceInfo { + return envbks.kern +} + +func (envbks *envRefExtractedKernelBootloaderKernelState) tryKernel() (snap.PlaceInfo, error) { + // empty snap_try_kernel is special case + if envbks.env["snap_try_kernel"] == "" { + return nil, bootloader.ErrNoTryKernelRef + } + sn, err := snap.ParsePlaceInfoFromSnapFileName(envbks.env["snap_try_kernel"]) + if err != nil { + return nil, err + } + + return sn, nil +} + +func (envbks *envRefExtractedKernelBootloaderKernelState) kernelStatus() string { + return envbks.env["kernel_status"] +} + +func (envbks *envRefExtractedKernelBootloaderKernelState) commonStateCommitUpdate(sn snap.PlaceInfo, bootvar string) bool { + envChanged := false + + // check kernel_status + if envbks.env["kernel_status"] != envbks.toCommit["kernel_status"] { + envChanged = true + } + + // if the specified snap is not the current snap, update the bootvar + if sn.Filename() != envbks.kern.Filename() { + envbks.toCommit[bootvar] = sn.Filename() + envChanged = true + } + + return envChanged +} + +func (envbks *envRefExtractedKernelBootloaderKernelState) markSuccessfulKernel(sn snap.PlaceInfo) error { + // the ordering here doesn't matter, as the only actual state we mutate is + // writing the bootloader env vars, so just do that once at the end after + // processing all the changes + + // always set kernel_status to DefaultStatus + envbks.toCommit["kernel_status"] = DefaultStatus + envChanged := envbks.commonStateCommitUpdate(sn, "snap_kernel") + + // if the snap_try_kernel is set, we should unset that to both cleanup after + // a successful trying -> "" transition, but also to cleanup if we got + // rebooted during the process and have it leftover + if envbks.env["snap_try_kernel"] != "" { + envChanged = true + envbks.toCommit["snap_try_kernel"] = "" + } + + if envChanged { + return envbks.bl.SetBootVars(envbks.toCommit) + } + + return nil +} + +func (envbks *envRefExtractedKernelBootloaderKernelState) setNextKernel(sn snap.PlaceInfo, status string) error { + envbks.toCommit["kernel_status"] = status + bootenvChanged := envbks.commonStateCommitUpdate(sn, "snap_try_kernel") + + if bootenvChanged { + return envbks.bl.SetBootVars(envbks.toCommit) + } + + return nil +} diff -Nru snapd-2.45.1+20.04.2/boot/bootstate20.go snapd-2.48.3+20.04/boot/bootstate20.go --- snapd-2.45.1+20.04.2/boot/bootstate20.go 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/boot/bootstate20.go 2021-02-02 08:21:12.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2019 Canonical Ltd + * Copyright (C) 2019-2020 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as @@ -21,42 +21,36 @@ import ( "fmt" + "path/filepath" + "github.com/snapcore/snapd/asserts" "github.com/snapcore/snapd/bootloader" + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/logger" + "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/strutil" ) -func newBootState20(typ snap.Type) bootState { +func newBootState20(typ snap.Type, dev Device) bootState { switch typ { case snap.TypeBase: return &bootState20Base{} case snap.TypeKernel: - return &bootState20Kernel{} + return &bootState20Kernel{ + dev: dev, + } default: panic(fmt.Sprintf("cannot make a bootState20 for snap type %q", typ)) } } -// -// modeenv methods -// - -type bootState20Modeenv struct { - modeenv *Modeenv -} - -func (bsm *bootState20Modeenv) loadModeenv() error { - // don't read modeenv multiple times - if bsm.modeenv != nil { - return nil - } +func loadModeenv() (*Modeenv, error) { modeenv, err := ReadModeenv("") if err != nil { - return fmt.Errorf("cannot get snap revision: unable to read modeenv: %v", err) + return nil, fmt.Errorf("cannot get snap revision: unable to read modeenv: %v", err) } - bsm.modeenv = modeenv - - return nil + return modeenv, nil } // @@ -82,269 +76,112 @@ markSuccessfulKernel(sn snap.PlaceInfo) error } -// extractedRunKernelImageBootloaderKernelState implements bootloaderKernelState20 for -// bootloaders that implement ExtractedRunKernelImageBootloader -type extractedRunKernelImageBootloaderKernelState struct { - // the bootloader - ebl bootloader.ExtractedRunKernelImageBootloader - // the current kernel status as read by the bootloader's bootenv - currentKernelStatus string - // the current kernel on the bootloader (not the try-kernel) - currentKernel snap.PlaceInfo -} +// +// bootStateUpdate for 20 methods +// -func (bks *extractedRunKernelImageBootloaderKernelState) load() error { - // get the kernel_status - m, err := bks.ebl.GetBootVars("kernel_status") - if err != nil { - return err - } +type bootCommitTask func() error - bks.currentKernelStatus = m["kernel_status"] +// bootStateUpdate20 implements the bootStateUpdate interface for both kernel +// and base snaps on UC20. +type bootStateUpdate20 struct { + // tasks to run before the modeenv has been written + preModeenvTasks []bootCommitTask - // get the current kernel for this bootloader to compare during commit() for - // markSuccessful() if we booted the current kernel or not - kernel, err := bks.ebl.Kernel() - if err != nil { - return fmt.Errorf("cannot identify kernel snap with bootloader %s: %v", bks.ebl.Name(), err) - } + // the modeenv that was read from disk + modeenv *Modeenv - bks.currentKernel = kernel + // the modeenv that will be written out in commit + writeModeenv *Modeenv - return nil -} + // tasks to run after the modeenv has been written + postModeenvTasks []bootCommitTask -func (bks *extractedRunKernelImageBootloaderKernelState) kernel() snap.PlaceInfo { - return bks.currentKernel + // model set if a reseal might be necessary + resealModel *asserts.Model } -func (bks *extractedRunKernelImageBootloaderKernelState) tryKernel() (snap.PlaceInfo, error) { - return bks.ebl.TryKernel() +func (u20 *bootStateUpdate20) preModeenv(task bootCommitTask) { + u20.preModeenvTasks = append(u20.preModeenvTasks, task) } -func (bks *extractedRunKernelImageBootloaderKernelState) kernelStatus() string { - return bks.currentKernelStatus -} - -func (bks *extractedRunKernelImageBootloaderKernelState) markSuccessfulKernel(sn snap.PlaceInfo) error { - // set the boot vars first, then enable the successful kernel, then disable - // the old try-kernel, see the comment in bootState20MarkSuccessful.commit() - // for details - - // the ordering here is very important for boot reliability! - - // If we have successfully just booted from a try-kernel and are - // marking it successful (this implies that snap_kernel=="trying" as set - // by the boot script), we need to do the following in order (since we - // have the added complexity of moving the kernel symlink): - // 1. Update kernel_status to "" - // 2. Move kernel symlink to point to the new try kernel - // 3. Remove try-kernel symlink - // 4. Remove old kernel from modeenv (this happens one level up from this - // function) - // - // If we got rebooted after step 1, then the bootloader is booting the wrong - // kernel, but is at least booting a known good kernel and snapd in - // user-space would be able to figure out the inconsistency. - // If we got rebooted after step 2, the bootloader would boot from the new - // try-kernel which is okay because we were in the middle of committing - // that new kernel as good and all that's left is for snapd to cleanup - // the left-over try-kernel symlink. - // - // If instead we had moved the kernel symlink first to point to the new try - // kernel, and got rebooted before the kernel_status was updated, we would - // have kernel_status="trying" which would cause the bootloader to think - // the boot failed, and revert to booting using the kernel symlink, but that - // now points to the new kernel we were trying and we did not successfully - // boot from that kernel to know we should trust it. - // - // Removing the old kernel from the modeenv needs to happen after it is - // impossible for the bootloader to boot from that kernel, otherwise we - // could end up in a state where the bootloader doesn't want to boot the - // new kernel, but the initramfs doesn't trust the old kernel and we are - // stuck. As such, do this last, after the symlink no longer exists. - // - // The try-kernel symlink removal should happen last because it will not - // affect anything, except that if it was removed before updating - // kernel_status to "", the bootloader will think that the try kernel failed - // to boot and fall back to booting the old kernel which is safe. - - // always set the boot vars first before mutating any of the kernel symlinks - // etc. - // for markSuccessful, we will always set the status to Default, even if - // technically this boot wasn't "successful" - it was successful in the - // sense that we booted some combination of boot snaps and made it all the - // way to snapd in user space - if bks.currentKernelStatus != DefaultStatus { - m := map[string]string{ - "kernel_status": DefaultStatus, - } +func (u20 *bootStateUpdate20) postModeenv(task bootCommitTask) { + u20.postModeenvTasks = append(u20.postModeenvTasks, task) +} - // set the boot variables - err := bks.ebl.SetBootVars(m) - if err != nil { - return err - } - } +func (u20 *bootStateUpdate20) resealForModel(model *asserts.Model) { + u20.resealModel = model +} - // if the kernel we booted is not the current one, we must have tried - // a new kernel, so enable that one as the current one now - if bks.currentKernel.Filename() != sn.Filename() { - err := bks.ebl.EnableKernel(sn) +func newBootStateUpdate20(m *Modeenv) (*bootStateUpdate20, error) { + u20 := &bootStateUpdate20{} + if m == nil { + var err error + m, err = loadModeenv() if err != nil { - return err + return nil, err } } - - // always disable the try kernel snap to cleanup in case we have upgrade - // failures which leave behind try-kernel.efi - err := bks.ebl.DisableTryKernel() + // copy the modeenv for the write object + u20.modeenv = m + var err error + u20.writeModeenv, err = m.Copy() if err != nil { - return err + return nil, err } - - return nil + return u20, nil } -func (bks *extractedRunKernelImageBootloaderKernelState) setNextKernel(sn snap.PlaceInfo, status string) error { - // always enable the try-kernel first, if we did the reverse and got - // rebooted after setting the boot vars but before enabling the try-kernel - // we could get stuck where the bootloader can't find the try-kernel and - // gets stuck waiting for a user to reboot, at which point we would fallback - // see i.e. https://github.com/snapcore/pc-amd64-gadget/issues/36 - if sn.Filename() != bks.currentKernel.Filename() { - err := bks.ebl.EnableTryKernel(sn) - if err != nil { +// commit will write out boot state persistently to disk. +func (u20 *bootStateUpdate20) commit() error { + // The actual actions taken here will depend on what things were called + // before commit(), either setNextBoot for a single type of kernel snap, or + // markSuccessful for kernel and/or base snaps. + // It is expected that the caller code is carefully analyzed to avoid + // critical points where a hard system reset during that critical point + // would brick a device or otherwise severely fail an update. + // There are three things that callers can do before calling commit(), + // 1. modify writeModeenv to specify new values for things that will be + // written to disk in the modeenv. + // 2. Add tasks to run before writing the modeenv. + // 3. Add tasks to run after writing the modeenv. + + // first handle any pre-modeenv writing tasks + for _, t := range u20.preModeenvTasks { + if err := t(); err != nil { return err } } - // only if the new kernel status is different from what we read should we - // run SetBootVars() to minimize wear/corruption possibility on the bootenv - if status != bks.currentKernelStatus { - m := map[string]string{ - "kernel_status": status, + modeenvRewritten := false + // next write the modeenv if it changed + if !u20.writeModeenv.deepEqual(u20.modeenv) { + if err := u20.writeModeenv.Write(); err != nil { + return err } - - // 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 -type envRefExtractedKernelBootloaderKernelState struct { - // the bootloader - bl bootloader.Bootloader - - // the current state of env - env map[string]string - - // the state of env to commit - toCommit map[string]string - - // the current kernel - kern snap.PlaceInfo -} - -func (envbks *envRefExtractedKernelBootloaderKernelState) load() error { - // for uc20, we only care about kernel_status, snap_kernel, and - // snap_try_kernel - m, err := envbks.bl.GetBootVars("kernel_status", "snap_kernel", "snap_try_kernel") - if err != nil { - return err - } - - // the default commit env is the same state as the current env - envbks.env = m - envbks.toCommit = make(map[string]string, len(m)) - for k, v := range m { - envbks.toCommit[k] = v - } - - // snap_kernel is the current kernel snap - // parse the filename here because the kernel() method doesn't return an err - sn, err := snap.ParsePlaceInfoFromSnapFileName(envbks.env["snap_kernel"]) - if err != nil { - return err - } - - envbks.kern = sn - - return nil -} - -func (envbks *envRefExtractedKernelBootloaderKernelState) kernel() snap.PlaceInfo { - return envbks.kern -} - -func (envbks *envRefExtractedKernelBootloaderKernelState) tryKernel() (snap.PlaceInfo, error) { - // empty snap_try_kernel is special case - if envbks.env["snap_try_kernel"] == "" { - return nil, bootloader.ErrNoTryKernelRef - } - sn, err := snap.ParsePlaceInfoFromSnapFileName(envbks.env["snap_try_kernel"]) - if err != nil { - return nil, err - } - - return sn, nil -} - -func (envbks *envRefExtractedKernelBootloaderKernelState) kernelStatus() string { - return envbks.env["kernel_status"] -} - -func (envbks *envRefExtractedKernelBootloaderKernelState) commonStateCommitUpdate(sn snap.PlaceInfo, bootvar string) bool { - envChanged := false - - // check kernel_status - if envbks.env["kernel_status"] != envbks.toCommit["kernel_status"] { - envChanged = true + modeenvRewritten = true } - // if the specified snap is not the current snap, update the bootvar - if sn.Filename() != envbks.kern.Filename() { - envbks.toCommit[bootvar] = sn.Filename() - envChanged = true - } - - return envChanged -} - -func (envbks *envRefExtractedKernelBootloaderKernelState) markSuccessfulKernel(sn snap.PlaceInfo) error { - // the ordering here doesn't matter, as the only actual state we mutate is - // writing the bootloader env vars, so just do that once at the end after - // processing all the changes - - // always set kernel_status to DefaultStatus - envbks.toCommit["kernel_status"] = DefaultStatus - envChanged := envbks.commonStateCommitUpdate(sn, "snap_kernel") - - // if the snap_try_kernel is set, we should unset that to both cleanup after - // a successful trying -> "" transition, but also to cleanup if we got - // rebooted during the process and have it leftover - if envbks.env["snap_try_kernel"] != "" { - envChanged = true - envbks.toCommit["snap_try_kernel"] = "" - } - - if envChanged { - return envbks.bl.SetBootVars(envbks.toCommit) + // next reseal using the modeenv values, we do this before any + // post-modeenv tasks so if we are rebooted at any point after + // the reseal even before the post tasks are completed, we + // still boot properly + if u20.resealModel != nil { + // if there is ambiguity whether the boot chains have + // 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.resealModel, u20.writeModeenv, expectReseal); err != nil { + return err + } } - return nil -} - -func (envbks *envRefExtractedKernelBootloaderKernelState) setNextKernel(sn snap.PlaceInfo, status string) error { - envbks.toCommit["kernel_status"] = status - bootenvChanged := envbks.commonStateCommitUpdate(sn, "snap_try_kernel") - - if bootenvChanged { - return envbks.bl.SetBootVars(envbks.toCommit) + // finally handle any post-modeenv writing tasks + for _, t := range u20.postModeenvTasks { + if err := t(); err != nil { + return err + } } return nil @@ -354,27 +191,17 @@ // kernel snap methods // -// bootState20Kernel implements the bootState and bootStateUpdate interfaces for -// kernel snaps on UC20. It is used for setNext() and markSuccessful() - though -// note that for markSuccessful() a different bootStateUpdate implementation is -// returned, see bootState20MarkSuccessful +// bootState20Kernel implements the bootState interface for kernel snaps on +// UC20. It is used for both setNext() and markSuccessful(), with both of those +// methods returning bootStateUpdate20 to be used with bootStateUpdate. type bootState20Kernel struct { bks bootloaderKernelState20 - // the kernel snap that was booted for markSuccessful() - bootedKernelSnap snap.PlaceInfo - - // the kernel snap to try for setNext() - nextKernelSnap snap.PlaceInfo - - // the kernel_status to commit during commit() - commitKernelStatus string + // used to find the bootloader to manipulate the enabled kernel, etc. + blOpts *bootloader.Options + blDir string - // don't embed this struct - it will conflict with embedding - // bootState20Modeenv in bootState20Base when both bootState20Base and - // bootState20Kernel are embedded in bootState20MarkSuccessful - // also we only need to use it with setNext() - kModeenv bootState20Modeenv + dev Device } func (ks20 *bootState20Kernel) loadBootenv() error { @@ -383,17 +210,16 @@ return nil } - // find the bootloader and ensure it's an extracted run kernel image - // bootloader - opts := &bootloader.Options{ - // we want extracted run kernel images for uc20 - // TODO:UC20: the name of this flag is now confusing, as it is being - // slightly abused to tell the uboot bootloader to just look - // in a different directory, even when we don't have an - // actual extracted kernel image for that impl - ExtractedRunKernelImage: true, + // find the run-mode bootloader + var opts *bootloader.Options + if ks20.blOpts != nil { + opts = ks20.blOpts + } else { + opts = &bootloader.Options{ + Role: bootloader.RoleRunMode, + } } - bl, err := bootloader.Find("", opts) + bl, err := bootloader.Find(ks20.blDir, opts) if err != nil { return err } @@ -425,8 +251,9 @@ kern := ks20.bks.kernel() tryKernel, err := ks20.bks.tryKernel() + // if err is ErrNoTryKernelRef, then we will just return nil as the trySnap if err != nil && err != bootloader.ErrNoTryKernelRef { - return nil, nil, "", err + return kern, nil, "", newTrySnapErrorf("cannot identify try kernel snap: %v", err) } if err == nil { @@ -436,241 +263,275 @@ return kern, tryBootSn, status, nil } +func (ks20 *bootState20Kernel) revisionsFromModeenv(*Modeenv) (curSnap, trySnap snap.PlaceInfo, tryingStatus string, err error) { + // the kernel snap doesn't use modeenv at all for getting their revisions + return ks20.revisions() +} + func (ks20 *bootState20Kernel) markSuccessful(update bootStateUpdate) (bootStateUpdate, error) { // call the generic method with this object to do most of the legwork - u, sn, err := selectSuccessfulBootSnap(ks20, update) + u20, sn, err := selectSuccessfulBootSnap(ks20, update) if err != nil { return nil, err } - // save this object inside the update to share bootenv / modeenv between - // multiple calls to markSuccessful for different snap types, but the same - // bootStateUpdate object - u.bootState20Kernel = *ks20 - - // u should always be non-nil if err is nil - u.bootedKernelSnap = sn - return u, nil -} + // XXX: this if arises because some unit tests rely on not setting up kernel + // details and just operating on the base snap but this situation would + // never happen in reality + if sn != nil { + // On commit, mark the kernel successful before rewriting the modeenv + // because if we first rewrote the modeenv then got rebooted before + // marking the kernel successful, the bootloader would see that the boot + // failed to mark it successful and then fall back to the original + // kernel, but that kernel would no longer be in the modeenv, so we + // would die in the initramfs + u20.preModeenv(func() error { return ks20.bks.markSuccessfulKernel(sn) }) + + // On commit, set CurrentKernels as just this kernel because that is the + // successful kernel we booted + u20.writeModeenv.CurrentKernels = []string{sn.Filename()} -func (ks20 *bootState20Kernel) setNext(next snap.PlaceInfo) (rebootRequired bool, u bootStateUpdate, err error) { - // commit() for setNext() also needs to add to the kernels in modeenv - err = ks20.kModeenv.loadModeenv() - if err != nil { - return false, nil, err + // keep track of the model for resealing + u20.resealForModel(ks20.dev.Model()) } - nextStatus, err := genericSetNext(ks20, next) + return u20, nil +} + +func (ks20 *bootState20Kernel) setNext(next snap.PlaceInfo) (rebootRequired bool, u bootStateUpdate, err error) { + u20, nextStatus, err := genericSetNext(ks20, next) if err != nil { return false, nil, err } // if we are setting a snap as a try snap, then we need to reboot rebootRequired = false - ks20.nextKernelSnap = next if nextStatus == TryStatus { rebootRequired = true } - ks20.commitKernelStatus = nextStatus - - // any state changes done so far are consumed in commit() - - return rebootRequired, ks20, nil -} - -// commit for bootState20Kernel is meant only to be used with setNext(). -// For markSuccessful(), use bootState20MarkSuccessful. -func (ks20 *bootState20Kernel) commit() error { - // The ordering of this is very important for boot safety/reliability!!! - - // If we are about to try an update, and need to add the try-kernel symlink, - // we need to do things in this order: - // 1. Add the kernel snap to the modeenv - // 2. Create try-kernel symlink - // 3. Update kernel_status to "try" - // - // This is because if we get rebooted in before 3, kernel_status is still - // unset and boot scripts proceeds to boot with the old kernel, effectively - // ignoring the try-kernel symlink. - // If we did it in the opposite order however, we would set kernel_status to - // "try" and then get rebooted before we could create the try-kernel - // symlink, so the bootloader would try to boot from the non-existent - // try-kernel symlink and become broken. - // - // Adding the kernel snap to the modeenv's list of trusted kernel snaps can - // effectively happen any time before we update the kernel_status to "try" - // for the same reasoning as for creating the try-kernel symlink. Putting it - // first is currently a purely aesthetic choice. - // add the kernel to the modeenv if it is not the current kernel (if it is - // the current kernel then it must already be in the modeenv) currentKernel := ks20.bks.kernel() - if ks20.nextKernelSnap.Filename() != currentKernel.Filename() { - // add the kernel to the modeenv - ks20.kModeenv.modeenv.CurrentKernels = append( - ks20.kModeenv.modeenv.CurrentKernels, - ks20.nextKernelSnap.Filename(), + if next.Filename() != currentKernel.Filename() { + // on commit, add this kernel to the modeenv + u20.writeModeenv.CurrentKernels = append( + u20.writeModeenv.CurrentKernels, + next.Filename(), ) - err := ks20.kModeenv.modeenv.Write() - if err != nil { - return err - } } - err := ks20.bks.setNextKernel(ks20.nextKernelSnap, ks20.commitKernelStatus) - if err != nil { - return err + // On commit, if we are about to try an update, and need to set the next + // kernel before rebooting, we need to do that after updating the modeenv, + // because if we did it before and got rebooted in between setting the next + // 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) }) + + // keep track of the model for resealing + u20.resealForModel(ks20.dev.Model()) + + return rebootRequired, u20, nil +} + +// selectAndCommitSnapInitramfsMount chooses which snap should be mounted +// during the initramfs, and commits that choice if it needs state updated. +// 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) { + // first do the generic choice of which snap to use + first, second, err := genericInitramfsSelectSnap(ks20, modeenv, TryingStatus, "kernel") + if err != nil && err != errTrySnapFallback { + return nil, err } - return nil + if err == errTrySnapFallback { + // this should not actually return, it should immediately reboot + return nil, initramfsReboot() + } + + // now validate the chosen kernel snap against the modeenv CurrentKernel's + // setting + if strutil.ListContains(modeenv.CurrentKernels, first.Filename()) { + return first, nil + } + + // if we didn't trust the first kernel in the modeenv, and second is set as + // a fallback, that means we booted a try kernel which is the first kernel, + // but we need to fallback to the second kernel, but we can't do that in the + // initramfs, we need to reboot so the bootloader boots the fallback kernel + // for us + + if second != nil { + // this should not actually return, it should immediately reboot + return nil, initramfsReboot() + } + + // no fallback expected, so first snap _is_ the only kernel and isn't + // trusted! + // since we have nothing to fallback to, we don't issue a reboot and will + // instead just fail the systemd unit in the initramfs for an operator to + // debug/fix + return nil, fmt.Errorf("fallback kernel snap %q is not trusted in the modeenv", first.Filename()) } // // base snap methods // -// bootState20Kernel implements the bootState and bootStateUpdate interfaces for -// base snaps on UC20. It is used for setNext() and markSuccessful() - though -// note that for markSuccessful() a different bootStateUpdate implementation is -// returned, see bootState20MarkSuccessful -type bootState20Base struct { - bootState20Modeenv - - // the base_status to be written to the modeenv, stored separately to - // eliminate unnecessary writes to the modeenv when it's already in the - // state we want it in - commitBaseStatus string - - // the base snap that was booted for markSuccessful() - bootedBaseSnap snap.PlaceInfo - - // the base snap to try for setNext() - tryBaseSnap snap.PlaceInfo -} - -func (bs20 *bootState20Base) loadModeenv() error { - // don't read modeenv multiple times - if bs20.modeenv != nil { - return nil - } - modeenv, err := ReadModeenv("") - if err != nil { - return fmt.Errorf("cannot get snap revision: unable to read modeenv: %v", err) - } - bs20.modeenv = modeenv - - // default commit status is the current status - bs20.commitBaseStatus = bs20.modeenv.BaseStatus - - return nil -} +// bootState20Base implements the bootState interface for base snaps on UC20. +// It is used for both setNext() and markSuccessful(), with both of those +// methods returning bootStateUpdate20 to be used with bootStateUpdate. +type bootState20Base struct{} // revisions returns the current boot snap and optional try boot snap for the // type specified in bsgeneric. func (bs20 *bootState20Base) revisions() (curSnap, trySnap snap.PlaceInfo, tryingStatus string, err error) { - var bootSn, tryBootSn snap.PlaceInfo - err = bs20.loadModeenv() + modeenv, err := loadModeenv() if err != nil { return nil, nil, "", err } + return bs20.revisionsFromModeenv(modeenv) +} - if bs20.modeenv.Base == "" { +func (bs20 *bootState20Base) revisionsFromModeenv(modeenv *Modeenv) (curSnap, trySnap snap.PlaceInfo, tryingStatus string, err error) { + var bootSn, tryBootSn snap.PlaceInfo + + if modeenv.Base == "" { return nil, nil, "", fmt.Errorf("cannot get snap revision: modeenv base boot variable is empty") } - bootSn, err = snap.ParsePlaceInfoFromSnapFileName(bs20.modeenv.Base) + bootSn, err = snap.ParsePlaceInfoFromSnapFileName(modeenv.Base) if err != nil { return nil, nil, "", fmt.Errorf("cannot get snap revision: modeenv base boot variable is invalid: %v", err) } - if bs20.modeenv.BaseStatus == TryingStatus && bs20.modeenv.TryBase != "" { - tryBootSn, err = snap.ParsePlaceInfoFromSnapFileName(bs20.modeenv.TryBase) + if modeenv.BaseStatus != DefaultStatus && modeenv.TryBase != "" { + tryBootSn, err = snap.ParsePlaceInfoFromSnapFileName(modeenv.TryBase) if err != nil { - return nil, nil, "", fmt.Errorf("cannot get snap revision: modeenv try base boot variable is invalid: %v", err) + return bootSn, nil, "", newTrySnapErrorf("cannot get snap revision: modeenv try base boot variable is invalid: %v", err) } } - return bootSn, tryBootSn, bs20.modeenv.BaseStatus, nil + return bootSn, tryBootSn, modeenv.BaseStatus, nil } func (bs20 *bootState20Base) markSuccessful(update bootStateUpdate) (bootStateUpdate, error) { // call the generic method with this object to do most of the legwork - u, sn, err := selectSuccessfulBootSnap(bs20, update) + u20, sn, err := selectSuccessfulBootSnap(bs20, update) if err != nil { return nil, err } - // save this object inside the update to share bootenv / modeenv between - // multiple calls to markSuccessful for different snap types, but the same - // bootStateUpdate object - u.bootState20Base = *bs20 - - // u should always be non-nil if err is nil - u.bootedBaseSnap = sn - return u, nil + // on commit, always clear the base_status and try_base when marking + // successful, this has the useful side-effect of cleaning up if we have + // base_status=trying but no try_base set, or if we had an issue with + // try_base being invalid + u20.writeModeenv.BaseStatus = DefaultStatus + u20.writeModeenv.TryBase = "" + + // set the base + u20.writeModeenv.Base = sn.Filename() + + return u20, nil } func (bs20 *bootState20Base) setNext(next snap.PlaceInfo) (rebootRequired bool, u bootStateUpdate, err error) { - nextStatus, err := genericSetNext(bs20, next) + u20, nextStatus, err := genericSetNext(bs20, next) if err != nil { return false, nil, err } + // if we are setting a snap as a try snap, then we need to reboot rebootRequired = false if nextStatus == TryStatus { - bs20.tryBaseSnap = next + // only update the try base if we are actually in try status + u20.writeModeenv.TryBase = next.Filename() rebootRequired = true } - bs20.commitBaseStatus = nextStatus - // any state changes done so far are consumed in to commit() + // always update the base status + u20.writeModeenv.BaseStatus = nextStatus - return rebootRequired, bs20, nil + return rebootRequired, u20, nil } -// commit for bootState20Base is meant only to be used with setNext(), for -// markSuccessful(), use bootState20MarkSuccessful. -func (bs20 *bootState20Base) commit() error { - // the ordering here is less important than the kernel commit(), since the - // only operation that has side-effects is writing the modeenv at the end, - // and that uses an atomic file writing operation, so it's not a concern if - // we get rebooted during this snippet like it is with the kernel snap above +// selectAndCommitSnapInitramfsMount chooses which snap should be mounted +// during the early boot sequence, i.e. the initramfs, and commits that +// choice if it needs state updated. +// 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) { + // 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") + // errTrySnapFallback is handled manually by inspecting second below + if err != nil && err != errTrySnapFallback { + return nil, err + } + + modeenvChanged := false - // the TryBase is the snap we are trying - note this could be nil if we - // are calling setNext on the same snap that is current - changed := false - if bs20.tryBaseSnap != nil { - tryBase := bs20.tryBaseSnap.Filename() - if tryBase != bs20.modeenv.TryBase { - bs20.modeenv.TryBase = tryBase - changed = true + // apply the update logic to the choices modeenv + switch modeenv.BaseStatus { + case TryStatus: + // if we were in try status and we have a fallback, then we are in a + // normal try state and we change status to TryingStatus now + // all other cleanup of state is left to user space snapd + if second != nil { + modeenv.BaseStatus = TryingStatus + modeenvChanged = true } + case TryingStatus: + // we tried to boot a try base snap and failed, so we need to reset + // BaseStatus + modeenv.BaseStatus = DefaultStatus + modeenvChanged = true + case DefaultStatus: + // nothing to do + default: + // log a message about invalid setting + logger.Noticef("invalid setting for \"base_status\" in modeenv : %q", modeenv.BaseStatus) } - if bs20.commitBaseStatus != bs20.modeenv.BaseStatus { - bs20.modeenv.BaseStatus = bs20.commitBaseStatus - changed = true + if modeenvChanged { + err = modeenv.Write() + if err != nil { + return nil, err + } } - // only write the modeenv if we actually changed it - if changed { - return bs20.modeenv.Write() - } - return nil + return first, nil } // // generic methods // +type bootState20 interface { + bootState + // revisionsFromModeenv implements bootState.revisions but starting + // from an already loaded Modeenv. + revisionsFromModeenv(*Modeenv) (curSnap, trySnap snap.PlaceInfo, tryingStatus string, err error) +} + // 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 bootState, next snap.PlaceInfo) (setStatus string, err error) { +func genericSetNext(b bootState20, next snap.PlaceInfo) (u20 *bootStateUpdate20, setStatus string, err error) { + u20, err = newBootStateUpdate20(nil) + if err != nil { + return nil, "", err + } + // get the current snap - current, _, _, err := b.revisions() + current, _, _, err := b.revisionsFromModeenv(u20.modeenv) if err != nil { - return "", err + return nil, "", err } // check if the next snap is really the same as the current snap, in which @@ -678,135 +539,184 @@ 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 DefaultStatus, nil + return u20, DefaultStatus, nil } // by default we will set the status as "try" to prepare for an update, // which also by default will require a reboot - return TryStatus, nil + return u20, TryStatus, nil } -// bootState20MarkSuccessful is like bootState20Base and -// bootState20Kernel, but is the combination of both of those things so we can -// mark both snaps successful in one go -type bootState20MarkSuccessful struct { - // base snap - bootState20Base - // kernel snap - bootState20Kernel +func toBootStateUpdate20(update bootStateUpdate) (u20 *bootStateUpdate20, err error) { + // try to extract bootStateUpdate20 out of update + 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) + } + } + if u20 == nil { + // make a new one, also loading modeenv + u20, err = newBootStateUpdate20(nil) + if err != nil { + return nil, err + } + } + return u20, nil } // selectSuccessfulBootSnap inspects the specified boot state to pick what // boot snap should be marked as successful and use as a valid rollback target. // If the first return value is non-nil, the second return value will be the // snap that was booted and should be marked as successful. -// It also loads boot environment state into b. -func selectSuccessfulBootSnap(b bootState, update bootStateUpdate) ( - bsmark *bootState20MarkSuccessful, +func selectSuccessfulBootSnap(b bootState20, update bootStateUpdate) ( + u20 *bootStateUpdate20, bootedSnap snap.PlaceInfo, err error, ) { - // get the try snap and the current status - sn, trySnap, status, err := b.revisions() + u20, err = toBootStateUpdate20(update) if err != nil { return nil, nil, err } - // try to extract bsmark out of update - var ok bool - if update != nil { - if bsmark, ok = update.(*bootState20MarkSuccessful); !ok { - return nil, nil, fmt.Errorf("internal error, cannot thread %T with update for UC20", update) - } - } - - if bsmark == nil { - bsmark = &bootState20MarkSuccessful{} + // get the try snap and the current status + sn, trySnap, status, err := b.revisionsFromModeenv(u20.modeenv) + if err != nil { + return nil, nil, err } // kernel_status and base_status go from "" -> "try" (set by snapd), to // "try" -> "trying" (set by the boot script) // so if we are in "trying" mode, then we should choose the try snap if status == TryingStatus && trySnap != nil { - return bsmark, trySnap, nil + return u20, trySnap, nil } // if we are not in trying then choose the normal snap - return bsmark, sn, nil + return u20, sn, nil } -// commit will persistently write out the boot variables, etc. needed to mark -// the snaps saved in bsmark as successful boot targets/combinations. -// note that this makes the assumption that markSuccessful() has been called for -// both the base and kernel snaps here, if that assumption is not true anymore, -// this could end up auto-cleaning status variables for something it shouldn't -// be. -func (bsmark *bootState20MarkSuccessful) commit() error { - // the base and kernel snap updates will modify the modeenv, so we only - // issue a single write at the end if something changed - modeenvChanged := false +// genericInitramfsSelectSnap will run the logic to choose which snap should be +// mounted during the initramfs using the given bootState and the expected try +// status. The try status is needed because during the initramfs we will have +// different statuses for kernel vs base snaps, where base snap is expected to +// be in "try" mode, but kernel is expected to be in "trying" mode. It returns +// 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) ( + firstChoice, secondChoice snap.PlaceInfo, + err error, +) { + curSnap, trySnap, snapTryStatus, err := bs.revisionsFromModeenv(modeenv) - // for full explanation of the robustness and ordering, see the comments - // on the implementations of bks.markSuccessfulKernel + if err != nil && !isTrySnapError(err) { + // we have no fallback snap! + return nil, nil, fmt.Errorf("fallback %s snap unusable: %v", typeString, err) + } + + // check that the current snap actually exists + file := curSnap.Filename() + snapPath := filepath.Join(dirs.SnapBlobDirUnder(InitramfsWritableDir), 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 + // isn't properly updated and never changes, but snapd thinks it was + // updated and eventually snapd garbage collects old revisions of + // the kernel snap as it is "refreshed" + // for a base, this could happen if the modeenv is manipulated + // out-of-band from snapd + return nil, nil, fmt.Errorf("%s snap %q does not exist on ubuntu-data", typeString, file) + } + + if err != nil && isTrySnapError(err) { + // just log that we had issues with the try snap and continue with + // using the normal snap + logger.Noticef("unable to process try %s snap: %v", typeString, err) + return curSnap, nil, errTrySnapFallback + } + if snapTryStatus != expectedTryStatus { + // the status is unexpected, log if its value is invalid and continue + // with the normal snap + fallbackErr := errTrySnapFallback + switch snapTryStatus { + case DefaultStatus: + fallbackErr = nil + case TryStatus, TryingStatus: + default: + logger.Noticef("\"%s_status\" has an invalid setting: %q", typeString, snapTryStatus) + } + return curSnap, nil, fallbackErr + } + // then we are trying a snap update and there should be a try snap + if trySnap == nil { + // it is unexpected when there isn't one + 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()) + if !osutil.FileExists(trySnapPath) { + // or when the snap file does not exist + logger.Noticef("try-%s snap %q does not exist", typeString, trySnap.Filename()) + return curSnap, nil, errTrySnapFallback + } - // kernel snap first, slightly higher priority + // we have a try snap and everything appears in order + return trySnap, curSnap, nil +} - // bootedKernelSnap will only ever be non-nil if we aren't marking a kernel - // snap successful, i.e. we are only marking a base snap successful - // this shouldn't happen except in tests, but let's be robust against it - // just in case - if bsmark.bootedKernelSnap != nil { - // always mark the kernel snap successful _before_ any other state - // mutating that may happen in bks.markSuccessful, because what we don't - // want to happen is to remove the old kernel and only trust the new - // try kernel before we actually set it up to boot from the new try - // kernel - that would brick us because we wouldn't trust the new kernel - // but the bootloader still thinks it should boot from the old kernel - err := bsmark.bks.markSuccessfulKernel(bsmark.bootedKernelSnap) - if err != nil { - return err - } +// +// non snap boot resources +// - // also always set current_kernels to be just the kernel we booted, for - // same reason we always disable the try-kernel - bsmark.modeenv.CurrentKernels = []string{bsmark.bootedKernelSnap.Filename()} - modeenvChanged = true - } +// bootState20BootAssets implements the successfulBootState interface for trusted +// boot assets UC20. +type bootState20BootAssets struct { + dev Device +} - // base snap next - // the ordering here is less important, since the only operation that - // has side-effects is writing the modeenv at the end, and that uses an - // atomic file writing operation, so it's not a concern if we get - // rebooted during this snippet like it is with the kernel snap above - - // always clear the base_status and try_base when marking successful, this - // has the useful side-effect of cleaning up if we have base_status=trying - // but no try_base set, or if we had an issue with try_base being invalid - if bsmark.modeenv.BaseStatus != DefaultStatus { - modeenvChanged = true - bsmark.modeenv.TryBase = "" - bsmark.modeenv.BaseStatus = DefaultStatus +func (ba20 *bootState20BootAssets) markSuccessful(update bootStateUpdate) (bootStateUpdate, error) { + u20, err := toBootStateUpdate20(update) + if err != nil { + return nil, err } - if bsmark.bootedBaseSnap != nil { - // set the new base as the tried base snap - tryBase := bsmark.bootedBaseSnap.Filename() - if bsmark.modeenv.Base != tryBase { - bsmark.modeenv.Base = tryBase - modeenvChanged = true - } - - // clear the TryBase - if bsmark.modeenv.TryBase != "" { - bsmark.modeenv.TryBase = "" - modeenvChanged = true + if len(u20.modeenv.CurrentTrustedBootAssets) == 0 && len(u20.modeenv.CurrentTrustedRecoveryBootAssets) == 0 { + // not using trusted boot assets, nothing more to do + return update, nil + } + + newM, dropAssets, err := observeSuccessfulBootAssets(u20.writeModeenv) + if err != nil { + return nil, fmt.Errorf("cannot mark successful boot assets: %v", err) + } + // update modeenv + u20.writeModeenv = newM + // keep track of the model for resealing + u20.resealForModel(ba20.dev.Model()) + + if len(dropAssets) == 0 { + // nothing to drop, we're done + return u20, nil + } + + u20.postModeenv(func() error { + cache := newTrustedAssetsCache(dirs.SnapBootAssetsDir) + // drop listed assets from cache + for _, ta := range dropAssets { + err := cache.Remove(ta.blName, ta.name, ta.hash) + if err != nil { + // XXX: should this be a log instead? + return fmt.Errorf("cannot remove unused boot asset %v:%v: %v", ta.name, ta.hash, err) + } } - } + return nil + }) + return u20, nil +} - // write the modeenv - if modeenvChanged { - return bsmark.modeenv.Write() +func trustedAssetsBootState(dev Device) *bootState20BootAssets { + return &bootState20BootAssets{ + dev: dev, } - - return nil } diff -Nru snapd-2.45.1+20.04.2/boot/boottest/bootenv.go snapd-2.48.3+20.04/boot/boottest/bootenv.go --- snapd-2.45.1+20.04.2/boot/boottest/bootenv.go 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/boot/boottest/bootenv.go 2021-02-02 08:21:12.000000000 +0000 @@ -109,14 +109,9 @@ if rollbackKernel && b16.BootVars["snap_kernel"] == "" { return fmt.Errorf("kernel rollback can only be simulated if snap_kernel is set") } - // clean try bootvars and statusVar + // clean only statusVar - the try vars will be cleaned by snapd NOT by the + // bootloader b16.BootVars[b16.statusVar] = "" - if rollbackBase { - b16.BootVars["snap_try_core"] = "" - } - if rollbackKernel { - b16.BootVars["snap_try_kernel"] = "" - } return nil } diff -Nru snapd-2.45.1+20.04.2/boot/boottest/device.go snapd-2.48.3+20.04/boot/boottest/device.go --- snapd-2.45.1+20.04.2/boot/boottest/device.go 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/boot/boottest/device.go 2021-02-02 08:21:12.000000000 +0000 @@ -22,6 +22,7 @@ import ( "strings" + "github.com/snapcore/snapd/asserts" "github.com/snapcore/snapd/boot" ) @@ -29,37 +30,67 @@ bootSnap string mode string uc20 bool + + model *asserts.Model } // MockDevice implements boot.Device. It wraps a string like -// [@], no means classic, no -// defaults to "run". It returns for both -// Base and Kernel, for more control mock a DeviceContext. +// [@], 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 +// control mock a DeviceContext. func MockDevice(s string) boot.Device { - bootsnap, mode := snapAndMode(s) + bootsnap, mode, uc20 := snapAndMode(s) + if uc20 && bootsnap == "" { + panic("MockDevice with no snap name and @mode is unsupported") + } return &mockDevice{ bootSnap: bootsnap, mode: mode, + uc20: uc20, } } // MockUC20Device implements boot.Device and returns true for HasModeenv. -func MockUC20Device(s string) boot.Device { - m := MockDevice(s).(*mockDevice) - m.uc20 = true - return m +// Arguments are mode (empty means "run"), and model. +// If model is nil a default model is used (same as MakeMockUC20Model). +func MockUC20Device(mode string, model *asserts.Model) boot.Device { + if mode == "" { + mode = "run" + } + if model == nil { + model = MakeMockUC20Model() + } + return &mockDevice{ + bootSnap: model.Kernel(), + mode: mode, + uc20: true, + model: model, + } } -func snapAndMode(str string) (snap, mode string) { +func snapAndMode(str string) (snap, mode string, uc20 bool) { parts := strings.SplitN(string(str), "@", 2) if len(parts) == 1 || parts[1] == "" { - return parts[0], "run" + return parts[0], "run", false } - return parts[0], parts[1] + return parts[0], parts[1], true } func (d *mockDevice) Kernel() string { return d.bootSnap } -func (d *mockDevice) Base() string { return d.bootSnap } func (d *mockDevice) Classic() bool { return d.bootSnap == "" } func (d *mockDevice) RunMode() bool { return d.mode == "run" } func (d *mockDevice) HasModeenv() bool { return d.uc20 } +func (d *mockDevice) Base() string { + if d.model != nil { + return d.model.Base() + } + return d.bootSnap +} +func (d *mockDevice) Model() *asserts.Model { + if d.model == nil { + panic("Device.Model called but MockUC20Device not used") + } + return d.model +} diff -Nru snapd-2.45.1+20.04.2/boot/boottest/device_test.go snapd-2.48.3+20.04/boot/boottest/device_test.go --- snapd-2.45.1+20.04.2/boot/boottest/device_test.go 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/boot/boottest/device_test.go 2021-02-02 08:21:12.000000000 +0000 @@ -41,19 +41,9 @@ c.Check(dev.RunMode(), Equals, true) c.Check(dev.HasModeenv(), Equals, false) - dev = boottest.MockDevice("@run") - c.Check(dev.Classic(), Equals, true) - c.Check(dev.Kernel(), Equals, "") - c.Check(dev.Base(), Equals, "") - c.Check(dev.RunMode(), Equals, true) - c.Check(dev.HasModeenv(), Equals, false) + c.Check(func() { dev.Model() }, PanicMatches, "Device.Model called.*") - dev = boottest.MockDevice("@recover") - c.Check(dev.Classic(), Equals, true) - c.Check(dev.Kernel(), Equals, "") - c.Check(dev.Base(), Equals, "") - c.Check(dev.RunMode(), Equals, false) - c.Check(dev.HasModeenv(), Equals, false) + c.Check(func() { boottest.MockDevice("@run") }, Panics, "MockDevice with no snap name and @mode is unsupported") } func (s *boottestSuite) TestMockDeviceBaseOrKernel(c *C) { @@ -63,38 +53,59 @@ c.Check(dev.Base(), 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") dev = boottest.MockDevice("boot-snap@run") c.Check(dev.Classic(), Equals, false) c.Check(dev.Kernel(), Equals, "boot-snap") c.Check(dev.Base(), Equals, "boot-snap") c.Check(dev.RunMode(), Equals, true) - c.Check(dev.HasModeenv(), Equals, false) + c.Check(dev.HasModeenv(), Equals, true) + c.Check(func() { dev.Model() }, PanicMatches, "Device.Model called.*") dev = boottest.MockDevice("boot-snap@recover") c.Check(dev.Classic(), Equals, false) c.Check(dev.Kernel(), Equals, "boot-snap") c.Check(dev.Base(), Equals, "boot-snap") c.Check(dev.RunMode(), Equals, false) - c.Check(dev.HasModeenv(), Equals, false) + c.Check(dev.HasModeenv(), Equals, true) + c.Check(func() { dev.Model() }, PanicMatches, "Device.Model called.*") } func (s *boottestSuite) TestMockUC20Device(c *C) { - dev := boottest.MockUC20Device("boot-snap") - c.Check(dev.HasModeenv(), Equals, true) - - dev = boottest.MockUC20Device("boot-snap@run") + dev := boottest.MockUC20Device("", nil) c.Check(dev.HasModeenv(), Equals, true) + c.Check(dev.Classic(), Equals, false) + c.Check(dev.RunMode(), Equals, true) + c.Check(dev.Kernel(), Equals, "pc-kernel") + c.Check(dev.Base(), Equals, "core20") - dev = boottest.MockUC20Device("boot-snap@recover") - c.Check(dev.HasModeenv(), Equals, true) + c.Check(dev.Model().Model(), Equals, "my-model-uc20") - dev = boottest.MockUC20Device("") - c.Check(dev.HasModeenv(), Equals, true) + dev = boottest.MockUC20Device("run", nil) + c.Check(dev.RunMode(), Equals, true) - dev = boottest.MockUC20Device("@run") - c.Check(dev.HasModeenv(), Equals, true) + dev = boottest.MockUC20Device("recover", nil) + c.Check(dev.RunMode(), Equals, false) - dev = boottest.MockUC20Device("@recover") - c.Check(dev.HasModeenv(), Equals, true) + model := boottest.MakeMockUC20Model(map[string]interface{}{ + "model": "other-model-uc20", + "snaps": []interface{}{ + map[string]interface{}{ + "name": "pc-linux", + "id": "pclinuxdidididididididididididid", + "type": "kernel", + }, + map[string]interface{}{ + "name": "pc", + "id": "pcididididididididididididididid", + "type": "gadget", + }, + }, + }) + dev = boottest.MockUC20Device("recover", model) + c.Check(dev.RunMode(), Equals, false) + c.Check(dev.Kernel(), Equals, "pc-linux") + c.Check(dev.Model().Model(), Equals, "other-model-uc20") + c.Check(dev.Model(), Equals, model) } diff -Nru snapd-2.45.1+20.04.2/boot/boottest/model.go snapd-2.48.3+20.04/boot/boottest/model.go --- snapd-2.45.1+20.04.2/boot/boottest/model.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48.3+20.04/boot/boottest/model.go 2021-02-02 08:21:12.000000000 +0000 @@ -0,0 +1,70 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2020 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 boottest + +import ( + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/asserts/assertstest" +) + +func MakeMockModel(overrides ...map[string]interface{}) *asserts.Model { + headers := map[string]interface{}{ + "type": "model", + "authority-id": "my-brand", + "series": "16", + "brand-id": "my-brand", + "model": "my-model", + "display-name": "My Model", + "architecture": "amd64", + "base": "core18", + "gadget": "pc=18", + "kernel": "pc-kernel=18", + "timestamp": "2018-01-01T08:00:00+00:00", + } + return assertstest.FakeAssertion(append([]map[string]interface{}{headers}, overrides...)...).(*asserts.Model) +} + +func MakeMockUC20Model(overrides ...map[string]interface{}) *asserts.Model { + headers := map[string]interface{}{ + "type": "model", + "authority-id": "my-brand", + "series": "16", + "brand-id": "my-brand", + "model": "my-model-uc20", + "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", + }, + }, + } + return assertstest.FakeAssertion(append([]map[string]interface{}{headers}, overrides...)...).(*asserts.Model) +} diff -Nru snapd-2.45.1+20.04.2/boot/boot_test.go snapd-2.48.3+20.04/boot/boot_test.go --- snapd-2.45.1+20.04.2/boot/boot_test.go 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/boot/boot_test.go 2021-02-02 08:21:12.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2014-2019 Canonical Ltd + * Copyright (C) 2014-2020 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as @@ -21,6 +21,7 @@ import ( "errors" + "fmt" "io/ioutil" "os" "path/filepath" @@ -28,14 +29,18 @@ . "gopkg.in/check.v1" + "github.com/snapcore/snapd/asserts" "github.com/snapcore/snapd/boot" "github.com/snapcore/snapd/boot/boottest" "github.com/snapcore/snapd/bootloader" "github.com/snapcore/snapd/bootloader/bootloadertest" "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/secboot" + "github.com/snapcore/snapd/seed" "github.com/snapcore/snapd/snap" "github.com/snapcore/snapd/snap/snaptest" "github.com/snapcore/snapd/testutil" + "github.com/snapcore/snapd/timings" ) func TestBoot(t *testing.T) { TestingT(t) } @@ -43,18 +48,20 @@ type baseBootenvSuite struct { testutil.BaseTest + rootdir string bootdir string } func (s *baseBootenvSuite) SetUpTest(c *C) { s.BaseTest.SetUpTest(c) - dirs.SetRootDir(c.MkDir()) + s.rootdir = c.MkDir() + dirs.SetRootDir(s.rootdir) s.AddCleanup(func() { dirs.SetRootDir("") }) restore := snap.MockSanitizePlugsSlots(func(snapInfo *snap.Info) {}) s.AddCleanup(restore) - s.bootdir = filepath.Join(dirs.GlobalRootDir, "boot") + s.bootdir = filepath.Join(s.rootdir, "boot") } func (s *baseBootenvSuite) forceBootloader(bloader bootloader.Bootloader) { @@ -62,6 +69,13 @@ s.AddCleanup(func() { bootloader.Force(nil) }) } +func (s *baseBootenvSuite) stampSealedKeys(c *C, rootdir string) { + stamp := filepath.Join(dirs.SnapFDEDirUnder(rootdir), "sealed-keys") + c.Assert(os.MkdirAll(filepath.Dir(stamp), 0755), IsNil) + err := ioutil.WriteFile(stamp, nil, 0644) + c.Assert(err, IsNil) +} + type bootenvSuite struct { baseBootenvSuite @@ -80,10 +94,12 @@ type baseBootenv20Suite struct { baseBootenvSuite - kern1 snap.PlaceInfo - kern2 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 normalDefaultState *bootenv20Setup normalTryingKernelState *bootenv20Setup @@ -98,6 +114,11 @@ s.kern2, err = snap.ParsePlaceInfoFromSnapFileName("pc-kernel_2.snap") c.Assert(err, IsNil) + s.ukern1, err = snap.ParsePlaceInfoFromSnapFileName("pc-kernel_x1.snap") + c.Assert(err, IsNil) + s.ukern2, err = snap.ParsePlaceInfoFromSnapFileName("pc-kernel_x2.snap") + c.Assert(err, IsNil) + s.base1, err = snap.ParsePlaceInfoFromSnapFileName("core20_1.snap") c.Assert(err, IsNil) s.base2, err = snap.ParsePlaceInfoFromSnapFileName("core20_2.snap") @@ -130,6 +151,8 @@ // state for after trying a new kernel for robustness tests, etc. s.normalTryingKernelState = &bootenv20Setup{ modeenv: &boot.Modeenv{ + // operating mode is run + Mode: "run", // base is base1 Base: s.base1.Filename(), // no try base @@ -197,8 +220,8 @@ // multiple modeenvs from a single test and just call the restore // function in between the parts of the test that use different modeenvs r := func() { - emptyModeenv := &boot.Modeenv{} - c.Assert(emptyModeenv.WriteTo(""), IsNil) + defaultModeenv := &boot.Modeenv{Mode: "run"} + c.Assert(defaultModeenv.WriteTo(""), IsNil) } cleanups = append(cleanups, r) } @@ -220,12 +243,12 @@ case *bootloadertest.MockExtractedRunKernelImageBootloader: // then we can use the advanced methods on it if opts.kern != nil { - r := vbl.SetRunKernelImageEnabledKernel(opts.kern) + r := vbl.SetEnabledKernel(opts.kern) cleanups = append(cleanups, r) } if opts.tryKern != nil { - r := vbl.SetRunKernelImageEnabledTryKernel(opts.tryKern) + r := vbl.SetEnabledTryKernel(opts.tryKern) cleanups = append(cleanups, r) } @@ -259,6 +282,8 @@ err := bl.SetBootVars(origEnv) c.Assert(err, IsNil) }) + default: + c.Fatalf("unsupported bootloader %T", bl) } return func() { @@ -371,7 +396,7 @@ } func (s *bootenv20Suite) TestCurrentBoot20NameAndRevision(c *C) { - coreDev := boottest.MockUC20Device("some-snap") + coreDev := boottest.MockUC20Device("", nil) c.Assert(coreDev.HasModeenv(), Equals, true) r := setupUC20Bootenv( @@ -399,7 +424,7 @@ // only difference between this test and TestCurrentBoot20NameAndRevision is the // base bootloader which doesn't support ExtractedRunKernelImageBootloader. func (s *bootenv20EnvRefKernelSuite) TestCurrentBoot20NameAndRevision(c *C) { - coreDev := boottest.MockUC20Device("some-snap") + coreDev := boottest.MockUC20Device("", nil) c.Assert(coreDev.HasModeenv(), Equals, true) r := setupUC20Bootenv( @@ -527,10 +552,10 @@ for i, t := range table { dev := boottest.MockDevice(t.model) - bp := boot.Participant(t.with, t.with.GetType(), dev) + 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.GetType(), dev)) + c.Check(bp, DeepEquals, boot.NewCoreBootParticipant(t.with, t.with.Type(), dev)) } } } @@ -573,8 +598,75 @@ } } +func (s *bootenvSuite) TestMarkBootSuccessfulKernelStatusTryingNoTryKernelSnapCleansUp(c *C) { + coreDev := boottest.MockDevice("some-snap") + + // set all the same vars as if we were doing trying, except don't set a try + // kernel + + err := s.bootloader.SetBootVars(map[string]string{ + "snap_kernel": "kernel_41.snap", + "snap_mode": boot.TryingStatus, + }) + c.Assert(err, IsNil) + + // mark successful + err = boot.MarkBootSuccessful(coreDev) + c.Assert(err, IsNil) + + // check that the bootloader variables were cleaned + expected := map[string]string{ + "snap_mode": boot.DefaultStatus, + "snap_kernel": "kernel_41.snap", + "snap_try_kernel": "", + } + m, err := s.bootloader.GetBootVars("snap_mode", "snap_try_kernel", "snap_kernel") + c.Assert(err, IsNil) + c.Assert(m, DeepEquals, expected) + + // do it again, verify it's still okay + err = boot.MarkBootSuccessful(coreDev) + c.Assert(err, IsNil) + m2, err := s.bootloader.GetBootVars("snap_mode", "snap_try_kernel", "snap_kernel") + c.Assert(err, IsNil) + c.Assert(m2, DeepEquals, expected) +} + +func (s *bootenvSuite) TestMarkBootSuccessfulTryKernelKernelStatusDefaultCleansUp(c *C) { + coreDev := boottest.MockDevice("some-snap") + + // set an errant snap_try_kernel + err := s.bootloader.SetBootVars(map[string]string{ + "snap_kernel": "kernel_41.snap", + "snap_try_kernel": "kernel_42.snap", + "snap_mode": boot.DefaultStatus, + }) + c.Assert(err, IsNil) + + // mark successful + err = boot.MarkBootSuccessful(coreDev) + c.Assert(err, IsNil) + + // check that the bootloader variables were cleaned + expected := map[string]string{ + "snap_mode": boot.DefaultStatus, + "snap_kernel": "kernel_41.snap", + "snap_try_kernel": "", + } + m, err := s.bootloader.GetBootVars("snap_mode", "snap_try_kernel", "snap_kernel") + c.Assert(err, IsNil) + c.Assert(m, DeepEquals, expected) + + // do it again, verify it's still okay + err = boot.MarkBootSuccessful(coreDev) + c.Assert(err, IsNil) + m2, err := s.bootloader.GetBootVars("snap_mode", "snap_try_kernel", "snap_kernel") + c.Assert(err, IsNil) + c.Assert(m2, DeepEquals, expected) +} + func (s *bootenv20Suite) TestCoreKernel20(c *C) { - coreDev := boottest.MockUC20Device("pc-kernel") + coreDev := boottest.MockUC20Device("", nil) c.Assert(coreDev.HasModeenv(), Equals, true) r := setupUC20Bootenv( @@ -609,7 +701,7 @@ } func (s *bootenv20Suite) TestCoreParticipant20SetNextSameKernelSnap(c *C) { - coreDev := boottest.MockUC20Device("pc-kernel") + coreDev := boottest.MockUC20Device("", nil) c.Assert(coreDev.HasModeenv(), Equals, true) r := setupUC20Bootenv( @@ -652,7 +744,7 @@ } func (s *bootenv20EnvRefKernelSuite) TestCoreParticipant20SetNextSameKernelSnap(c *C) { - coreDev := boottest.MockUC20Device("pc-kernel") + coreDev := boottest.MockUC20Device("", nil) c.Assert(coreDev.HasModeenv(), Equals, true) r := setupUC20Bootenv( @@ -693,7 +785,7 @@ } func (s *bootenv20Suite) TestCoreParticipant20SetNextNewKernelSnap(c *C) { - coreDev := boottest.MockUC20Device("pc-kernel") + coreDev := boottest.MockUC20Device("", nil) c.Assert(coreDev.HasModeenv(), Equals, true) r := setupUC20Bootenv( @@ -730,17 +822,90 @@ c.Assert(m2.CurrentKernels, DeepEquals, []string{s.kern1.Filename(), s.kern2.Filename()}) } -func (s *bootenv20EnvRefKernelSuite) TestCoreParticipant20SetNextNewKernelSnap(c *C) { - coreDev := boottest.MockUC20Device("pc-kernel") +func (s *bootenv20Suite) TestCoreParticipant20SetNextNewKernelSnapWithReseal(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 + c.Assert(os.MkdirAll(filepath.Join(dirs.SnapBootAssetsDir, "trusted"), 0755), IsNil) + for _, name := range []string{ + "asset-" + dataHash, + } { + c.Assert(ioutil.WriteFile(filepath.Join(dirs.SnapBootAssetsDir, "trusted", name), nil, 0644), IsNil) + } + + assetBf := bootloader.NewBootFile("", filepath.Join(dirs.SnapBootAssetsDir, "trusted", fmt.Sprintf("asset-%s", dataHash)), bootloader.RoleRunMode) + runKernelBf := bootloader.NewBootFile(filepath.Join(s.kern1.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) + m := &boot.Modeenv{ + Mode: "run", + Base: s.base1.Filename(), + CurrentKernels: []string{s.kern1.Filename()}, + CurrentTrustedBootAssets: boot.BootAssetsMap{ + "asset": {dataHash}, + }, + } + r := setupUC20Bootenv( c, - s.bootloader, - s.normalDefaultState, + 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, DeepEquals, coreDev.Model()) + for _, ch := range mp.EFILoadChains { + printChain(c, ch, "-") + } + c.Check(mp.EFILoadChains, DeepEquals, []*secboot.LoadChain{ + secboot.NewLoadChain(assetBf, + secboot.NewLoadChain(runKernelBf)), + secboot.NewLoadChain(assetBf, + // TODO:UC20: once mock trusted assets + // bootloader can generated boot files for the + // kernel this will use candidate kernel + secboot.NewLoadChain(runKernelBf)), + }) + // actual paths are seen only here + c.Check(tab.BootChainKernelPath, DeepEquals, []string{ + s.kern1.MountFile(), + 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 @@ -752,8 +917,9 @@ c.Assert(rebootRequired, Equals, true) // make sure the env was updated - m := s.bootloader.BootVars - c.Assert(m, DeepEquals, map[string]string{ + 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.TryStatus, "snap_kernel": s.kern1.Filename(), "snap_try_kernel": s.kern2.Filename(), @@ -763,137 +929,515 @@ m2, err := boot.ReadModeenv("") c.Assert(err, IsNil) c.Assert(m2.CurrentKernels, DeepEquals, []string{s.kern1.Filename(), s.kern2.Filename()}) + + c.Check(resealCalls, Equals, 1) } -func (s *bootenv20Suite) TestMarkBootSuccessful20KernelStatusTryingNoKernelSnapCleansUp(c *C) { - coreDev := boottest.MockUC20Device("some-snap") +func (s *bootenv20Suite) TestCoreParticipant20SetNextNewUnassertedKernelSnapWithReseal(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 + c.Assert(os.MkdirAll(filepath.Join(dirs.SnapBootAssetsDir, "trusted"), 0755), IsNil) + for _, name := range []string{ + "asset-" + dataHash, + } { + c.Assert(ioutil.WriteFile(filepath.Join(dirs.SnapBootAssetsDir, "trusted", name), nil, 0644), IsNil) + } + + assetBf := bootloader.NewBootFile("", filepath.Join(dirs.SnapBootAssetsDir, "trusted", fmt.Sprintf("asset-%s", dataHash)), bootloader.RoleRunMode) + runKernelBf := bootloader.NewBootFile(filepath.Join(s.ukern1.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) - // set all the same vars as if we were doing trying, except don't set a try - // kernel + m := &boot.Modeenv{ + Mode: "run", + Base: s.base1.Filename(), + CurrentKernels: []string{s.ukern1.Filename()}, + CurrentTrustedBootAssets: boot.BootAssetsMap{ + "asset": {dataHash}, + }, + } + r := setupUC20Bootenv( c, - s.bootloader, + tab.MockBootloader, &bootenv20Setup{ - modeenv: &boot.Modeenv{ - Mode: "run", - Base: s.base1.Filename(), - CurrentKernels: []string{s.kern1.Filename(), s.kern2.Filename()}, - }, - kern: s.kern1, - // no try-kernel - kernStatus: boot.TryingStatus, + modeenv: m, + kern: s.ukern1, + kernStatus: boot.DefaultStatus, }, ) defer r() - // mark successful - err := boot.MarkBootSuccessful(coreDev) - c.Assert(err, IsNil) - - // check that the bootloader variable was cleaned - expected := map[string]string{"kernel_status": boot.DefaultStatus} - c.Assert(s.bootloader.BootVars, DeepEquals, expected) - - // check that MarkBootSuccessful didn't enable a kernel (since there was no - // try kernel) - _, nEnableCalls := s.bootloader.GetRunKernelImageFunctionSnapCalls("EnableKernel") - c.Assert(nEnableCalls, Equals, 0) + 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, DeepEquals, uc20Model) + for _, ch := range mp.EFILoadChains { + printChain(c, ch, "-") + } + c.Check(mp.EFILoadChains, DeepEquals, []*secboot.LoadChain{ + secboot.NewLoadChain(assetBf, + secboot.NewLoadChain(runKernelBf)), + secboot.NewLoadChain(assetBf, + // TODO:UC20: once mock trusted assets + // bootloader can generated boot files for the + // kernel this will use candidate kernel + secboot.NewLoadChain(runKernelBf)), + }) + // actual paths are seen only here + c.Check(tab.BootChainKernelPath, DeepEquals, []string{ + s.ukern1.MountFile(), + s.ukern2.MountFile(), + }) + return nil + }) + defer restore() - // we will always end up disabling a try-kernel though as cleanup - _, nDisableTryCalls := s.bootloader.GetRunKernelImageFunctionSnapCalls("DisableTryKernel") - c.Assert(nDisableTryCalls, Equals, 1) + // 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) - // do it again, verify it's still okay - err = boot.MarkBootSuccessful(coreDev) + // make the kernel used on next boot + rebootRequired, err := bootKern.SetNextBoot() c.Assert(err, IsNil) - c.Assert(s.bootloader.BootVars, DeepEquals, expected) - - // no new enabled kernels - _, nEnableCalls = s.bootloader.GetRunKernelImageFunctionSnapCalls("EnableKernel") - c.Assert(nEnableCalls, Equals, 0) + c.Assert(rebootRequired, Equals, true) - // again we will try to cleanup any leftover try-kernels - _, nDisableTryCalls = s.bootloader.GetRunKernelImageFunctionSnapCalls("DisableTryKernel") - c.Assert(nDisableTryCalls, Equals, 2) + 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.TryStatus, + "snap_kernel": s.ukern1.Filename(), + "snap_try_kernel": s.ukern2.Filename(), + }) - // check that the modeenv re-wrote the CurrentKernels + // and that the modeenv now has this kernel listed m2, err := boot.ReadModeenv("") c.Assert(err, IsNil) - c.Assert(m2.CurrentKernels, DeepEquals, []string{s.kern1.Filename()}) + c.Assert(m2.CurrentKernels, DeepEquals, []string{s.ukern1.Filename(), s.ukern2.Filename()}) + + c.Check(resealCalls, Equals, 1) } -func (s *bootenv20EnvRefKernelSuite) TestMarkBootSuccessful20KernelStatusTryingNoKernelSnapCleansUp(c *C) { - coreDev := boottest.MockUC20Device("some-snap") +func (s *bootenv20Suite) TestCoreParticipant20SetNextSameKernelSnapNoReseal(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 + c.Assert(os.MkdirAll(filepath.Join(dirs.SnapBootAssetsDir, "trusted"), 0755), IsNil) + for _, name := range []string{ + "asset-" + dataHash, + } { + c.Assert(ioutil.WriteFile(filepath.Join(dirs.SnapBootAssetsDir, "trusted", name), nil, 0644), IsNil) + } + + 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) - // set all the same vars as if we were doing trying, except don't set a try - // kernel + m := &boot.Modeenv{ + Mode: "run", + Base: s.base1.Filename(), + CurrentKernels: []string{s.kern1.Filename()}, + CurrentTrustedBootAssets: boot.BootAssetsMap{ + "asset": {dataHash}, + }, + } + r := setupUC20Bootenv( c, - s.bootloader, + tab.MockBootloader, &bootenv20Setup{ - modeenv: &boot.Modeenv{ - Mode: "run", - Base: s.base1.Filename(), - CurrentKernels: []string{s.kern1.Filename(), s.kern2.Filename()}, - }, - kern: s.kern1, - // no try-kernel - kernStatus: boot.TryingStatus, + modeenv: m, + kern: s.kern1, + kernStatus: boot.DefaultStatus, }, ) defer r() - // mark successful - err := boot.MarkBootSuccessful(coreDev) + 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.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 sure the env was updated - expected := map[string]string{ + // make the kernel used on next boot + rebootRequired, err := bootKern.SetNextBoot() + c.Assert(err, IsNil) + c.Assert(rebootRequired, Equals, 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": "", - } - c.Assert(s.bootloader.BootVars, DeepEquals, expected) - - // do it again, verify it's still okay - err = boot.MarkBootSuccessful(coreDev) - c.Assert(err, IsNil) - - c.Assert(s.bootloader.BootVars, DeepEquals, expected) + }) - // check that the modeenv re-wrote the CurrentKernels + // 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) TestMarkBootSuccessful20BaseStatusTryingNoTryBaseSnapCleansUp(c *C) { +func (s *bootenv20Suite) TestCoreParticipant20SetNextSameUnassertedKernelSnapNoReseal(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 + c.Assert(os.MkdirAll(filepath.Join(dirs.SnapBootAssetsDir, "trusted"), 0755), IsNil) + for _, name := range []string{ + "asset-" + dataHash, + } { + c.Assert(ioutil.WriteFile(filepath.Join(dirs.SnapBootAssetsDir, "trusted", name), nil, 0644), IsNil) + } + + 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(), - // no TryBase set - BaseStatus: boot.TryingStatus, + Mode: "run", + Base: s.base1.Filename(), + CurrentKernels: []string{s.ukern1.Filename()}, + CurrentTrustedBootAssets: boot.BootAssetsMap{ + "asset": {dataHash}, + }, } + r := setupUC20Bootenv( c, - s.bootloader, + tab.MockBootloader, &bootenv20Setup{ - modeenv: m, - // no kernel setup necessary + modeenv: m, + kern: s.ukern1, + kernStatus: boot.DefaultStatus, }, ) defer r() - coreDev := boottest.MockUC20Device("core20") - c.Assert(coreDev.HasModeenv(), Equals, true) + resealCalls := 0 + restore := boot.MockSecbootResealKeys(func(params *secboot.ResealKeysParams) error { + resealCalls++ + return fmt.Errorf("unexpected call") + }) + defer restore() - // mark successful - err := boot.MarkBootSuccessful(coreDev) + // 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) - // check that the modeenv base_status was re-written to default + // make the kernel used on next boot + rebootRequired, err := bootKern.SetNextBoot() + c.Assert(err, IsNil) + c.Assert(rebootRequired, Equals, 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 *bootenv20EnvRefKernelSuite) TestCoreParticipant20SetNextNewKernelSnap(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() + c.Assert(err, IsNil) + c.Assert(rebootRequired, Equals, true) + + // make sure the env was updated + m := s.bootloader.BootVars + c.Assert(m, DeepEquals, map[string]string{ + "kernel_status": boot.TryStatus, + "snap_kernel": s.kern1.Filename(), + "snap_try_kernel": s.kern2.Filename(), + }) + + // and that the modeenv now has this kernel listed + m2, err := boot.ReadModeenv("") + c.Assert(err, IsNil) + c.Assert(m2.CurrentKernels, DeepEquals, []string{s.kern1.Filename(), s.kern2.Filename()}) +} + +func (s *bootenv20Suite) TestMarkBootSuccessful20KernelStatusTryingNoKernelSnapCleansUp(c *C) { + coreDev := boottest.MockUC20Device("", nil) + c.Assert(coreDev.HasModeenv(), Equals, true) + + // set all the same vars as if we were doing trying, except don't set a try + // kernel + r := setupUC20Bootenv( + c, + s.bootloader, + &bootenv20Setup{ + modeenv: &boot.Modeenv{ + Mode: "run", + Base: s.base1.Filename(), + CurrentKernels: []string{s.kern1.Filename(), s.kern2.Filename()}, + }, + kern: s.kern1, + // no try-kernel + kernStatus: boot.TryingStatus, + }, + ) + defer r() + + // mark successful + err := boot.MarkBootSuccessful(coreDev) + c.Assert(err, IsNil) + + // check that the bootloader variable was cleaned + expected := map[string]string{"kernel_status": boot.DefaultStatus} + c.Assert(s.bootloader.BootVars, DeepEquals, expected) + + // check that MarkBootSuccessful didn't enable a kernel (since there was no + // try kernel) + _, nEnableCalls := s.bootloader.GetRunKernelImageFunctionSnapCalls("EnableKernel") + c.Assert(nEnableCalls, Equals, 0) + + // we will always end up disabling a try-kernel though as cleanup + _, nDisableTryCalls := s.bootloader.GetRunKernelImageFunctionSnapCalls("DisableTryKernel") + c.Assert(nDisableTryCalls, Equals, 1) + + // do it again, verify it's still okay + err = boot.MarkBootSuccessful(coreDev) + c.Assert(err, IsNil) + c.Assert(s.bootloader.BootVars, DeepEquals, expected) + + // no new enabled kernels + _, nEnableCalls = s.bootloader.GetRunKernelImageFunctionSnapCalls("EnableKernel") + c.Assert(nEnableCalls, Equals, 0) + + // again we will try to cleanup any leftover try-kernels + _, nDisableTryCalls = s.bootloader.GetRunKernelImageFunctionSnapCalls("DisableTryKernel") + c.Assert(nDisableTryCalls, Equals, 2) + + // check that the modeenv re-wrote the CurrentKernels + m2, err := boot.ReadModeenv("") + c.Assert(err, IsNil) + c.Assert(m2.CurrentKernels, DeepEquals, []string{s.kern1.Filename()}) +} + +func (s *bootenv20EnvRefKernelSuite) TestMarkBootSuccessful20KernelStatusTryingNoKernelSnapCleansUp(c *C) { + coreDev := boottest.MockUC20Device("", nil) + c.Assert(coreDev.HasModeenv(), Equals, true) + + // set all the same vars as if we were doing trying, except don't set a try + // kernel + r := setupUC20Bootenv( + c, + s.bootloader, + &bootenv20Setup{ + modeenv: &boot.Modeenv{ + Mode: "run", + Base: s.base1.Filename(), + CurrentKernels: []string{s.kern1.Filename(), s.kern2.Filename()}, + }, + kern: s.kern1, + // no try-kernel + kernStatus: boot.TryingStatus, + }, + ) + defer r() + + // mark successful + err := boot.MarkBootSuccessful(coreDev) + c.Assert(err, IsNil) + + // make sure the env was updated + expected := map[string]string{ + "kernel_status": boot.DefaultStatus, + "snap_kernel": s.kern1.Filename(), + "snap_try_kernel": "", + } + c.Assert(s.bootloader.BootVars, DeepEquals, expected) + + // do it again, verify it's still okay + err = boot.MarkBootSuccessful(coreDev) + c.Assert(err, IsNil) + + c.Assert(s.bootloader.BootVars, DeepEquals, expected) + + // check that the modeenv re-wrote the CurrentKernels + m2, err := boot.ReadModeenv("") + c.Assert(err, IsNil) + c.Assert(m2.CurrentKernels, DeepEquals, []string{s.kern1.Filename()}) +} + +func (s *bootenv20Suite) TestMarkBootSuccessful20BaseStatusTryingNoTryBaseSnapCleansUp(c *C) { + m := &boot.Modeenv{ + Mode: "run", + Base: s.base1.Filename(), + // no TryBase set + BaseStatus: boot.TryingStatus, + } + r := setupUC20Bootenv( + c, + s.bootloader, + &bootenv20Setup{ + modeenv: m, + // no kernel setup necessary + }, + ) + defer r() + + coreDev := boottest.MockUC20Device("", nil) + c.Assert(coreDev.HasModeenv(), Equals, true) + + // mark successful + err := boot.MarkBootSuccessful(coreDev) + c.Assert(err, IsNil) + + // check that the modeenv base_status was re-written to default m2, err := boot.ReadModeenv("") c.Assert(err, IsNil) c.Assert(m2.BaseStatus, Equals, boot.DefaultStatus) @@ -912,7 +1456,7 @@ } func (s *bootenv20Suite) TestCoreParticipant20SetNextSameBaseSnap(c *C) { - coreDev := boottest.MockUC20Device("core20") + coreDev := boottest.MockUC20Device("", nil) c.Assert(coreDev.HasModeenv(), Equals, true) m := &boot.Modeenv{ @@ -950,7 +1494,7 @@ } func (s *bootenv20Suite) TestCoreParticipant20SetNextNewBaseSnap(c *C) { - coreDev := boottest.MockUC20Device("core20") + coreDev := boottest.MockUC20Device("", nil) c.Assert(coreDev.HasModeenv(), Equals, true) // default state @@ -986,6 +1530,61 @@ c.Assert(m2.TryBase, Equals, s.base2.Filename()) } +func (s *bootenv20Suite) TestCoreParticipant20SetNextNewBaseSnapNoReseal(c *C) { + // checked by resealKeyToModeenv + s.stampSealedKeys(c, dirs.GlobalRootDir) + + tab := s.bootloaderWithTrustedAssets(c, []string{"asset"}) + + coreDev := boottest.MockUC20Device("", nil) + c.Assert(coreDev.HasModeenv(), Equals, true) + + resealCalls := 0 + restore := boot.MockSecbootResealKeys(func(params *secboot.ResealKeysParams) error { + resealCalls++ + return nil + }) + defer restore() + + // we should not even need to build boot chains + tab.BootChainErr = errors.New("boom") + + // default state + m := &boot.Modeenv{ + Mode: "run", + Base: s.base1.Filename(), + } + 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() + c.Assert(err, IsNil) + c.Assert(rebootRequired, Equals, true) + + // make sure the modeenv was updated + m2, err := boot.ReadModeenv("") + c.Assert(err, IsNil) + c.Assert(m2.Base, Equals, m.Base) + c.Assert(m2.BaseStatus, Equals, boot.TryStatus) + c.Assert(m2.TryBase, Equals, s.base2.Filename()) + + // no reseal + c.Check(resealCalls, Equals, 0) +} + func (s *bootenvSuite) TestMarkBootSuccessfulAllSnap(c *C) { coreDev := boottest.MockDevice("some-snap") @@ -1013,7 +1612,7 @@ } func (s *bootenv20Suite) TestMarkBootSuccessful20AllSnap(c *C) { - coreDev := boottest.MockUC20Device("some-snap") + coreDev := boottest.MockUC20Device("", nil) c.Assert(coreDev.HasModeenv(), Equals, true) // bonus points: we were trying both a base snap and a kernel snap @@ -1077,7 +1676,7 @@ } func (s *bootenv20EnvRefKernelSuite) TestMarkBootSuccessful20AllSnap(c *C) { - coreDev := boottest.MockUC20Device("some-snap") + coreDev := boottest.MockUC20Device("", nil) c.Assert(coreDev.HasModeenv(), Equals, true) // bonus points: we were trying both a base snap and a kernel snap @@ -1190,7 +1789,7 @@ ) defer r() - coreDev := boottest.MockUC20Device("some-snap") + coreDev := boottest.MockUC20Device("", nil) c.Assert(coreDev.HasModeenv(), Equals, true) // mark successful @@ -1229,6 +1828,99 @@ c.Assert(nDisableTryCalls, Equals, 2) } +func (s *bootenv20Suite) TestMarkBootSuccessful20KernelUpdateWithReseal(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 + c.Assert(os.MkdirAll(filepath.Join(dirs.SnapBootAssetsDir, "trusted"), 0755), IsNil) + for _, name := range []string{ + "asset-" + dataHash, + } { + c.Assert(ioutil.WriteFile(filepath.Join(dirs.SnapBootAssetsDir, "trusted", name), nil, 0644), IsNil) + } + + assetBf := bootloader.NewBootFile("", filepath.Join(dirs.SnapBootAssetsDir, "trusted", fmt.Sprintf("asset-%s", dataHash)), bootloader.RoleRunMode) + runKernelBf := bootloader.NewBootFile(filepath.Join(s.kern1.Filename()), "kernel.efi", bootloader.RoleRunMode) + + tab.BootChainList = []bootloader.BootFile{ + bootloader.NewBootFile("", "asset", bootloader.RoleRunMode), + runKernelBf, + } + + // trying a kernel snap + m := &boot.Modeenv{ + Mode: "run", + Base: s.base1.Filename(), + CurrentKernels: []string{s.kern1.Filename(), s.kern2.Filename()}, + CurrentTrustedBootAssets: boot.BootAssetsMap{ + "asset": {dataHash}, + }, + } + r := setupUC20Bootenv( + c, + tab.MockBootloader, + &bootenv20Setup{ + modeenv: m, + kern: s.kern1, + tryKern: s.kern2, + kernStatus: boot.TryingStatus, + }, + ) + defer r() + + coreDev := boottest.MockUC20Device("", nil) + c.Assert(coreDev.HasModeenv(), Equals, true) + + 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, DeepEquals, coreDev.Model()) + for _, ch := range mp.EFILoadChains { + printChain(c, ch, "-") + } + c.Check(mp.EFILoadChains, DeepEquals, []*secboot.LoadChain{ + secboot.NewLoadChain(assetBf, + secboot.NewLoadChain(runKernelBf)), + }) + return nil + }) + defer restore() + + // mark successful + err := boot.MarkBootSuccessful(coreDev) + c.Assert(err, IsNil) + + // check the bootloader variables + expected := map[string]string{ + "kernel_status": boot.DefaultStatus, + "snap_kernel": s.kern2.Filename(), + "snap_try_kernel": boot.DefaultStatus, + } + c.Assert(tab.BootVars, DeepEquals, expected) + + // check that the new kernel is the only one in modeenv + m2, err := boot.ReadModeenv("") + c.Assert(err, IsNil) + c.Assert(m2.CurrentKernels, DeepEquals, []string{s.kern2.Filename()}) + + c.Check(resealCalls, Equals, 1) +} + func (s *bootenv20EnvRefKernelSuite) TestMarkBootSuccessful20KernelUpdate(c *C) { // trying a kernel snap m := &boot.Modeenv{ @@ -1248,7 +1940,7 @@ ) defer r() - coreDev := boottest.MockUC20Device("some-snap") + coreDev := boottest.MockUC20Device("", nil) c.Assert(coreDev.HasModeenv(), Equals, true) // mark successful @@ -1295,7 +1987,7 @@ ) defer r() - coreDev := boottest.MockUC20Device("some-snap") + coreDev := boottest.MockUC20Device("", nil) c.Assert(coreDev.HasModeenv(), Equals, true) // mark successful @@ -1321,6 +2013,549 @@ c.Assert(m3.BaseStatus, Equals, "") } +func (s *bootenv20Suite) bootloaderWithTrustedAssets(c *C, trustedAssets []string) *bootloadertest.MockTrustedAssetsBootloader { + // TODO:UC20: this should be an ExtractedRecoveryKernelImageBootloader + // because that would reflect our main currently supported + // trusted assets bootloader (grub) + tab := bootloadertest.Mock("trusted", "").WithTrustedAssets() + bootloader.Force(tab) + tab.TrustedAssetsList = trustedAssets + s.AddCleanup(func() { bootloader.Force(nil) }) + return tab +} + +func (s *bootenv20Suite) TestMarkBootSuccessful20BootAssetsUpdateHappy(c *C) { + // checked by resealKeyToModeenv + s.stampSealedKeys(c, dirs.GlobalRootDir) + + tab := s.bootloaderWithTrustedAssets(c, []string{"asset", "shim"}) + + data := []byte("foobar") + // SHA3-384 + dataHash := "0fa8abfbdaf924ad307b74dd2ed183b9a4a398891a2f6bac8fd2db7041b77f068580f9c6c66f699b496c2da1cbcc7ed8" + shim := []byte("shim") + shimHash := "dac0063e831d4b2e7a330426720512fc50fa315042f0bb30f9d1db73e4898dcb89119cac41fdfa62137c8931a50f9d7b" + + c.Assert(os.MkdirAll(boot.InitramfsUbuntuBootDir, 0755), IsNil) + c.Assert(os.MkdirAll(boot.InitramfsUbuntuSeedDir, 0755), IsNil) + // only asset for ubuntu + c.Assert(ioutil.WriteFile(filepath.Join(boot.InitramfsUbuntuBootDir, "asset"), data, 0644), IsNil) + // shim and asset for seed + c.Assert(ioutil.WriteFile(filepath.Join(boot.InitramfsUbuntuSeedDir, "asset"), data, 0644), IsNil) + c.Assert(ioutil.WriteFile(filepath.Join(boot.InitramfsUbuntuSeedDir, "shim"), shim, 0644), IsNil) + + // mock the files in cache + c.Assert(os.MkdirAll(filepath.Join(dirs.SnapBootAssetsDir, "trusted"), 0755), IsNil) + for _, name := range []string{ + "shim-recoveryshimhash", + "shim-" + shimHash, + "asset-assethash", + "asset-recoveryassethash", + "asset-" + dataHash, + } { + c.Assert(ioutil.WriteFile(filepath.Join(dirs.SnapBootAssetsDir, "trusted", name), nil, 0644), IsNil) + } + + shimBf := bootloader.NewBootFile("", filepath.Join(dirs.SnapBootAssetsDir, "trusted", fmt.Sprintf("shim-%s", shimHash)), bootloader.RoleRecovery) + assetBf := bootloader.NewBootFile("", filepath.Join(dirs.SnapBootAssetsDir, "trusted", fmt.Sprintf("asset-%s", dataHash)), bootloader.RoleRecovery) + runKernelBf := bootloader.NewBootFile(filepath.Join(s.kern1.Filename()), "kernel.efi", bootloader.RoleRunMode) + recoveryKernelBf := bootloader.NewBootFile("/var/lib/snapd/seed/snaps/pc-kernel_1.snap", "kernel.efi", bootloader.RoleRecovery) + + tab.BootChainList = []bootloader.BootFile{ + bootloader.NewBootFile("", "shim", bootloader.RoleRecovery), + bootloader.NewBootFile("", "asset", bootloader.RoleRecovery), + runKernelBf, + } + tab.RecoveryBootChainList = []bootloader.BootFile{ + bootloader.NewBootFile("", "shim", bootloader.RoleRecovery), + bootloader.NewBootFile("", "asset", bootloader.RoleRecovery), + recoveryKernelBf, + } + + uc20Model := boottest.MakeMockUC20Model() + + restore := boot.MockSeedReadSystemEssential(func(seedDir, label string, essentialTypes []snap.Type, tm timings.Measurer) (*asserts.Model, []*seed.Snap, error) { + kernelSnap := &seed.Snap{ + Path: "/var/lib/snapd/seed/snaps/pc-linux_1.snap", + SideInfo: &snap.SideInfo{ + Revision: snap.Revision{N: 1}, + RealName: "pc-linux", + }, + } + return uc20Model, []*seed.Snap{kernelSnap}, nil + }) + defer restore() + + // we were trying an update of boot assets + m := &boot.Modeenv{ + Mode: "run", + Base: s.base1.Filename(), + CurrentKernels: []string{s.kern1.Filename()}, + CurrentTrustedBootAssets: boot.BootAssetsMap{ + "asset": {"assethash", dataHash}, + }, + CurrentTrustedRecoveryBootAssets: boot.BootAssetsMap{ + "asset": {"recoveryassethash", dataHash}, + "shim": {"recoveryshimhash", shimHash}, + }, + CurrentRecoverySystems: []string{"system"}, + } + r := setupUC20Bootenv( + c, + tab.MockBootloader, + &bootenv20Setup{ + modeenv: m, + kern: s.kern1, + kernStatus: boot.DefaultStatus, + }, + ) + defer r() + + coreDev := boottest.MockUC20Device("", uc20Model) + c.Assert(coreDev.HasModeenv(), Equals, true) + + 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, DeepEquals, uc20Model) + for _, ch := range mp.EFILoadChains { + printChain(c, ch, "-") + } + switch resealCalls { + case 1: + c.Check(mp.EFILoadChains, DeepEquals, []*secboot.LoadChain{ + secboot.NewLoadChain(shimBf, + secboot.NewLoadChain(assetBf, + secboot.NewLoadChain(runKernelBf))), + secboot.NewLoadChain(shimBf, + secboot.NewLoadChain(assetBf, + secboot.NewLoadChain(recoveryKernelBf))), + }) + case 2: + c.Check(mp.EFILoadChains, DeepEquals, []*secboot.LoadChain{ + secboot.NewLoadChain(shimBf, + secboot.NewLoadChain(assetBf, + secboot.NewLoadChain(recoveryKernelBf))), + }) + default: + c.Errorf("unexpected additional call to secboot.ResealKey (call # %d)", resealCalls) + } + return nil + }) + defer restore() + + // mark successful + err := boot.MarkBootSuccessful(coreDev) + c.Assert(err, IsNil) + + // check the modeenv + m2, err := boot.ReadModeenv("") + c.Assert(err, IsNil) + // update assets are in the list + c.Check(m2.CurrentTrustedBootAssets, DeepEquals, boot.BootAssetsMap{ + "asset": {dataHash}, + }) + c.Check(m2.CurrentTrustedRecoveryBootAssets, DeepEquals, boot.BootAssetsMap{ + "asset": {dataHash}, + "shim": {shimHash}, + }) + // unused files were dropped from cache + checkContentGlob(c, filepath.Join(dirs.SnapBootAssetsDir, "trusted", "*"), []string{ + filepath.Join(dirs.SnapBootAssetsDir, "trusted", "asset-"+dataHash), + filepath.Join(dirs.SnapBootAssetsDir, "trusted", "shim-"+shimHash), + }) + c.Check(resealCalls, Equals, 2) +} + +func (s *bootenv20Suite) TestMarkBootSuccessful20BootAssetsStableStateHappy(c *C) { + // checked by resealKeyToModeenv + s.stampSealedKeys(c, dirs.GlobalRootDir) + + tab := s.bootloaderWithTrustedAssets(c, []string{"nested/asset", "shim"}) + + data := []byte("foobar") + // SHA3-384 + dataHash := "0fa8abfbdaf924ad307b74dd2ed183b9a4a398891a2f6bac8fd2db7041b77f068580f9c6c66f699b496c2da1cbcc7ed8" + shim := []byte("shim") + shimHash := "dac0063e831d4b2e7a330426720512fc50fa315042f0bb30f9d1db73e4898dcb89119cac41fdfa62137c8931a50f9d7b" + + c.Assert(os.MkdirAll(filepath.Join(boot.InitramfsUbuntuBootDir, "nested"), 0755), IsNil) + c.Assert(os.MkdirAll(filepath.Join(boot.InitramfsUbuntuSeedDir, "nested"), 0755), IsNil) + // only asset for ubuntu-boot + c.Assert(ioutil.WriteFile(filepath.Join(boot.InitramfsUbuntuBootDir, "nested/asset"), data, 0644), IsNil) + // shim and asset for ubuntu-seed + c.Assert(ioutil.WriteFile(filepath.Join(boot.InitramfsUbuntuSeedDir, "nested/asset"), data, 0644), IsNil) + c.Assert(ioutil.WriteFile(filepath.Join(boot.InitramfsUbuntuSeedDir, "shim"), shim, 0644), IsNil) + + // mock the files in cache + c.Assert(os.MkdirAll(filepath.Join(dirs.SnapBootAssetsDir, "trusted"), 0755), IsNil) + for _, name := range []string{ + "shim-" + shimHash, + "asset-" + dataHash, + } { + c.Assert(ioutil.WriteFile(filepath.Join(dirs.SnapBootAssetsDir, "trusted", name), nil, 0644), IsNil) + } + + runKernelBf := bootloader.NewBootFile(filepath.Join(s.kern1.Filename()), "kernel.efi", bootloader.RoleRunMode) + recoveryKernelBf := bootloader.NewBootFile("/var/lib/snapd/seed/snaps/pc-kernel_1.snap", "kernel.efi", bootloader.RoleRecovery) + + tab.BootChainList = []bootloader.BootFile{ + bootloader.NewBootFile("", "shim", bootloader.RoleRecovery), + bootloader.NewBootFile("", "asset", bootloader.RoleRecovery), + runKernelBf, + } + tab.RecoveryBootChainList = []bootloader.BootFile{ + bootloader.NewBootFile("", "shim", bootloader.RoleRecovery), + bootloader.NewBootFile("", "asset", bootloader.RoleRecovery), + recoveryKernelBf, + } + + uc20Model := boottest.MakeMockUC20Model() + + restore := boot.MockSeedReadSystemEssential(func(seedDir, label string, essentialTypes []snap.Type, tm timings.Measurer) (*asserts.Model, []*seed.Snap, error) { + kernelSnap := &seed.Snap{ + Path: "/var/lib/snapd/seed/snaps/pc-kernel-recovery_1.snap", + SideInfo: &snap.SideInfo{ + Revision: snap.Revision{N: 1}, + RealName: "pc-kernel-recovery", + }, + } + return uc20Model, []*seed.Snap{kernelSnap}, nil + }) + defer restore() + + // we were trying an update of boot assets + m := &boot.Modeenv{ + Mode: "run", + Base: s.base1.Filename(), + CurrentKernels: []string{s.kern1.Filename()}, + CurrentTrustedBootAssets: boot.BootAssetsMap{ + "asset": {dataHash}, + }, + CurrentTrustedRecoveryBootAssets: boot.BootAssetsMap{ + "asset": {dataHash}, + "shim": {shimHash}, + }, + CurrentRecoverySystems: []string{"system"}, + } + r := setupUC20Bootenv( + c, + tab.MockBootloader, + &bootenv20Setup{ + modeenv: m, + kern: s.kern1, + kernStatus: boot.DefaultStatus, + }, + ) + defer r() + + coreDev := boottest.MockUC20Device("", uc20Model) + c.Assert(coreDev.HasModeenv(), Equals, true) + + resealCalls := 0 + restore = boot.MockSecbootResealKeys(func(params *secboot.ResealKeysParams) error { + resealCalls++ + return nil + }) + defer restore() + + // 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.RoleRecovery, Name: "shim", + Hashes: []string{ + "dac0063e831d4b2e7a330426720512fc50fa315042f0bb30f9d1db73e4898dcb89119cac41fdfa62137c8931a50f9d7b", + }, + }, { + Role: bootloader.RoleRecovery, Name: "asset", Hashes: []string{ + "0fa8abfbdaf924ad307b74dd2ed183b9a4a398891a2f6bac8fd2db7041b77f068580f9c6c66f699b496c2da1cbcc7ed8", + }, + }}, + Kernel: "pc-kernel", + KernelRevision: "1", + KernelCmdlines: []string{"snapd_recovery_mode=run"}, + }, { + BrandID: "my-brand", + Model: "my-model-uc20", + Grade: "dangerous", + ModelSignKeyID: "Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij", + AssetChain: []boot.BootAsset{{ + Role: bootloader.RoleRecovery, Name: "shim", + Hashes: []string{ + "dac0063e831d4b2e7a330426720512fc50fa315042f0bb30f9d1db73e4898dcb89119cac41fdfa62137c8931a50f9d7b", + }, + }, { + Role: bootloader.RoleRecovery, Name: "asset", Hashes: []string{ + "0fa8abfbdaf924ad307b74dd2ed183b9a4a398891a2f6bac8fd2db7041b77f068580f9c6c66f699b496c2da1cbcc7ed8", + }, + }}, + Kernel: "pc-kernel-recovery", + KernelRevision: "1", + KernelCmdlines: []string{"snapd_recovery_mode=recover snapd_recovery_system=system"}, + }} + + recoveryBootChains := []boot.BootChain{bootChains[1]} + + err := boot.WriteBootChains(boot.ToPredictableBootChains(bootChains), filepath.Join(dirs.SnapFDEDir, "boot-chains"), 0) + c.Assert(err, IsNil) + + err = boot.WriteBootChains(boot.ToPredictableBootChains(recoveryBootChains), filepath.Join(dirs.SnapFDEDir, "recovery-boot-chains"), 0) + c.Assert(err, IsNil) + + // mark successful + err = boot.MarkBootSuccessful(coreDev) + c.Assert(err, IsNil) + + // modeenv is unchanged + m2, err := boot.ReadModeenv("") + c.Assert(err, IsNil) + c.Check(m2.CurrentTrustedBootAssets, DeepEquals, m.CurrentTrustedBootAssets) + c.Check(m2.CurrentTrustedRecoveryBootAssets, DeepEquals, m.CurrentTrustedRecoveryBootAssets) + // files are still in cache + checkContentGlob(c, filepath.Join(dirs.SnapBootAssetsDir, "trusted", "*"), []string{ + filepath.Join(dirs.SnapBootAssetsDir, "trusted", "asset-"+dataHash), + filepath.Join(dirs.SnapBootAssetsDir, "trusted", "shim-"+shimHash), + }) + + // boot chains were built + c.Check(tab.BootChainKernelPath, DeepEquals, []string{ + s.kern1.MountFile(), + }) + // no actual reseal + c.Check(resealCalls, Equals, 0) +} + +func (s *bootenv20Suite) TestMarkBootSuccessful20BootUnassertedKernelAssetsStableStateHappy(c *C) { + // checked by resealKeyToModeenv + s.stampSealedKeys(c, dirs.GlobalRootDir) + + tab := s.bootloaderWithTrustedAssets(c, []string{"nested/asset", "shim"}) + + data := []byte("foobar") + // SHA3-384 + dataHash := "0fa8abfbdaf924ad307b74dd2ed183b9a4a398891a2f6bac8fd2db7041b77f068580f9c6c66f699b496c2da1cbcc7ed8" + shim := []byte("shim") + shimHash := "dac0063e831d4b2e7a330426720512fc50fa315042f0bb30f9d1db73e4898dcb89119cac41fdfa62137c8931a50f9d7b" + + c.Assert(os.MkdirAll(filepath.Join(boot.InitramfsUbuntuBootDir, "nested"), 0755), IsNil) + c.Assert(os.MkdirAll(filepath.Join(boot.InitramfsUbuntuSeedDir, "nested"), 0755), IsNil) + // only asset for ubuntu-boot + c.Assert(ioutil.WriteFile(filepath.Join(boot.InitramfsUbuntuBootDir, "nested/asset"), data, 0644), IsNil) + // shim and asset for ubuntu-seed + c.Assert(ioutil.WriteFile(filepath.Join(boot.InitramfsUbuntuSeedDir, "nested/asset"), data, 0644), IsNil) + c.Assert(ioutil.WriteFile(filepath.Join(boot.InitramfsUbuntuSeedDir, "shim"), shim, 0644), IsNil) + + // mock the files in cache + c.Assert(os.MkdirAll(filepath.Join(dirs.SnapBootAssetsDir, "trusted"), 0755), IsNil) + for _, name := range []string{ + "shim-" + shimHash, + "asset-" + dataHash, + } { + c.Assert(ioutil.WriteFile(filepath.Join(dirs.SnapBootAssetsDir, "trusted", name), nil, 0644), IsNil) + } + + runKernelBf := bootloader.NewBootFile(filepath.Join(s.ukern1.Filename()), "kernel.efi", bootloader.RoleRunMode) + recoveryKernelBf := bootloader.NewBootFile("/var/lib/snapd/seed/snaps/pc-kernel_1.snap", "kernel.efi", bootloader.RoleRecovery) + + tab.BootChainList = []bootloader.BootFile{ + bootloader.NewBootFile("", "shim", bootloader.RoleRecovery), + bootloader.NewBootFile("", "asset", bootloader.RoleRecovery), + runKernelBf, + } + tab.RecoveryBootChainList = []bootloader.BootFile{ + bootloader.NewBootFile("", "shim", bootloader.RoleRecovery), + bootloader.NewBootFile("", "asset", bootloader.RoleRecovery), + recoveryKernelBf, + } + + uc20Model := boottest.MakeMockUC20Model() + + restore := boot.MockSeedReadSystemEssential(func(seedDir, label string, essentialTypes []snap.Type, tm timings.Measurer) (*asserts.Model, []*seed.Snap, error) { + kernelSnap := &seed.Snap{ + Path: "/var/lib/snapd/seed/snaps/pc-kernel-recovery_1.snap", + SideInfo: &snap.SideInfo{ + Revision: snap.Revision{N: 1}, + RealName: "pc-kernel-recovery", + }, + } + return uc20Model, []*seed.Snap{kernelSnap}, nil + }) + defer restore() + + // we were trying an update of boot assets + m := &boot.Modeenv{ + Mode: "run", + Base: s.base1.Filename(), + CurrentKernels: []string{s.ukern1.Filename()}, + CurrentTrustedBootAssets: boot.BootAssetsMap{ + "asset": {dataHash}, + }, + CurrentTrustedRecoveryBootAssets: boot.BootAssetsMap{ + "asset": {dataHash}, + "shim": {shimHash}, + }, + CurrentRecoverySystems: []string{"system"}, + } + r := setupUC20Bootenv( + c, + tab.MockBootloader, + &bootenv20Setup{ + modeenv: m, + kern: s.ukern1, + kernStatus: boot.DefaultStatus, + }, + ) + defer r() + + coreDev := boottest.MockUC20Device("", uc20Model) + c.Assert(coreDev.HasModeenv(), Equals, true) + + resealCalls := 0 + restore = boot.MockSecbootResealKeys(func(params *secboot.ResealKeysParams) error { + resealCalls++ + return nil + }) + defer restore() + + // 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.RoleRecovery, Name: "shim", + Hashes: []string{ + "dac0063e831d4b2e7a330426720512fc50fa315042f0bb30f9d1db73e4898dcb89119cac41fdfa62137c8931a50f9d7b", + }, + }, { + Role: bootloader.RoleRecovery, Name: "asset", Hashes: []string{ + "0fa8abfbdaf924ad307b74dd2ed183b9a4a398891a2f6bac8fd2db7041b77f068580f9c6c66f699b496c2da1cbcc7ed8", + }, + }}, + Kernel: "pc-kernel", + // unasserted kernel snap + KernelRevision: "", + KernelCmdlines: []string{"snapd_recovery_mode=run"}, + }, { + BrandID: "my-brand", + Model: "my-model-uc20", + Grade: "dangerous", + ModelSignKeyID: "Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij", + AssetChain: []boot.BootAsset{{ + Role: bootloader.RoleRecovery, Name: "shim", + Hashes: []string{ + "dac0063e831d4b2e7a330426720512fc50fa315042f0bb30f9d1db73e4898dcb89119cac41fdfa62137c8931a50f9d7b", + }, + }, { + Role: bootloader.RoleRecovery, Name: "asset", Hashes: []string{ + "0fa8abfbdaf924ad307b74dd2ed183b9a4a398891a2f6bac8fd2db7041b77f068580f9c6c66f699b496c2da1cbcc7ed8", + }, + }}, + Kernel: "pc-kernel-recovery", + KernelRevision: "1", + KernelCmdlines: []string{"snapd_recovery_mode=recover snapd_recovery_system=system"}, + }} + + recoveryBootChains := []boot.BootChain{bootChains[1]} + + err := boot.WriteBootChains(boot.ToPredictableBootChains(bootChains), filepath.Join(dirs.SnapFDEDir, "boot-chains"), 0) + c.Assert(err, IsNil) + + err = boot.WriteBootChains(boot.ToPredictableBootChains(recoveryBootChains), filepath.Join(dirs.SnapFDEDir, "recovery-boot-chains"), 0) + c.Assert(err, IsNil) + + // mark successful + err = boot.MarkBootSuccessful(coreDev) + c.Assert(err, IsNil) + + // modeenv is unchanged + m2, err := boot.ReadModeenv("") + c.Assert(err, IsNil) + c.Check(m2.CurrentTrustedBootAssets, DeepEquals, m.CurrentTrustedBootAssets) + c.Check(m2.CurrentTrustedRecoveryBootAssets, DeepEquals, m.CurrentTrustedRecoveryBootAssets) + // files are still in cache + checkContentGlob(c, filepath.Join(dirs.SnapBootAssetsDir, "trusted", "*"), []string{ + filepath.Join(dirs.SnapBootAssetsDir, "trusted", "asset-"+dataHash), + filepath.Join(dirs.SnapBootAssetsDir, "trusted", "shim-"+shimHash), + }) + + // boot chains were built + c.Check(tab.BootChainKernelPath, DeepEquals, []string{ + s.ukern1.MountFile(), + }) + // no actual reseal + c.Check(resealCalls, Equals, 0) +} + +func (s *bootenv20Suite) TestMarkBootSuccessful20BootAssetsUpdateUnexpectedAsset(c *C) { + tab := s.bootloaderWithTrustedAssets(c, []string{"EFI/asset"}) + + data := []byte("foobar") + // SHA3-384 + dataHash := "0fa8abfbdaf924ad307b74dd2ed183b9a4a398891a2f6bac8fd2db7041b77f068580f9c6c66f699b496c2da1cbcc7ed8" + + c.Assert(os.MkdirAll(filepath.Join(boot.InitramfsUbuntuBootDir, "EFI"), 0755), IsNil) + c.Assert(os.MkdirAll(filepath.Join(boot.InitramfsUbuntuSeedDir, "EFI"), 0755), IsNil) + c.Assert(ioutil.WriteFile(filepath.Join(boot.InitramfsUbuntuBootDir, "EFI/asset"), data, 0644), IsNil) + c.Assert(ioutil.WriteFile(filepath.Join(boot.InitramfsUbuntuSeedDir, "EFI/asset"), data, 0644), IsNil) + // mock some state in the cache + c.Assert(os.MkdirAll(filepath.Join(dirs.SnapBootAssetsDir, "trusted"), 0755), IsNil) + for _, name := range []string{"asset-one", "asset-two"} { + c.Assert(ioutil.WriteFile(filepath.Join(dirs.SnapBootAssetsDir, "trusted", name), nil, 0644), IsNil) + } + + // we were trying an update of boot assets + m := &boot.Modeenv{ + Mode: "run", + Base: s.base1.Filename(), + CurrentKernels: []string{s.kern1.Filename()}, + CurrentTrustedBootAssets: boot.BootAssetsMap{ + // hash will not match + "asset": {"one", "two"}, + }, + CurrentTrustedRecoveryBootAssets: boot.BootAssetsMap{ + "asset": {"one", "two"}, + }, + } + r := setupUC20Bootenv( + c, + tab.MockBootloader, + &bootenv20Setup{ + modeenv: m, + kern: s.kern1, + kernStatus: boot.DefaultStatus, + }, + ) + defer r() + + coreDev := boottest.MockUC20Device("", nil) + c.Assert(coreDev.HasModeenv(), Equals, true) + + // mark successful + err := boot.MarkBootSuccessful(coreDev) + c.Assert(err, ErrorMatches, fmt.Sprintf(`cannot mark boot successful: cannot mark successful boot assets: system booted with unexpected run mode bootloader asset "EFI/asset" hash %s`, dataHash)) + + // check the modeenv + m2, err := boot.ReadModeenv("") + c.Assert(err, IsNil) + // modeenv is unchaged + c.Check(m2.CurrentTrustedBootAssets, DeepEquals, m.CurrentTrustedBootAssets) + c.Check(m2.CurrentTrustedRecoveryBootAssets, DeepEquals, m.CurrentTrustedRecoveryBootAssets) + // nothing was removed from cache + checkContentGlob(c, filepath.Join(dirs.SnapBootAssetsDir, "trusted", "*"), []string{ + filepath.Join(dirs.SnapBootAssetsDir, "trusted", "asset-one"), + filepath.Join(dirs.SnapBootAssetsDir, "trusted", "asset-two"), + }) +} + type recoveryBootenv20Suite struct { baseBootenvSuite @@ -1337,7 +2572,7 @@ s.bootloader = bootloadertest.Mock("mock", c.MkDir()) s.forceBootloader(s.bootloader) - s.dev = boottest.MockUC20Device("some-snap") + s.dev = boottest.MockUC20Device("", nil) } func (s *recoveryBootenv20Suite) TestSetRecoveryBootSystemAndModeHappy(c *C) { @@ -1380,7 +2615,7 @@ err = boot.SetRecoveryBootSystemAndMode(s.dev, "1234", "install") c.Assert(err, IsNil) - bl, err := bootloader.Find(boot.InitramfsUbuntuSeedDir, &bootloader.Options{Recovery: true}) + bl, err := bootloader.Find(boot.InitramfsUbuntuSeedDir, &bootloader.Options{Role: bootloader.RoleRecovery}) c.Assert(err, IsNil) blvars, err := bl.GetBootVars("snapd_recovery_mode", "snapd_recovery_system") diff -Nru snapd-2.45.1+20.04.2/boot/cmdline.go snapd-2.48.3+20.04/boot/cmdline.go --- snapd-2.45.1+20.04.2/boot/cmdline.go 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/boot/cmdline.go 2021-02-02 08:21:12.000000000 +0000 @@ -20,13 +20,13 @@ package boot import ( - "bufio" - "bytes" + "errors" "fmt" - "io/ioutil" - "strings" + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/bootloader" "github.com/snapcore/snapd/logger" + "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/strutil" ) @@ -42,43 +42,36 @@ ) var ( - // the kernel commandline - can be overridden in tests - procCmdline = "/proc/cmdline" - validModes = []string{ModeInstall, ModeRecover, ModeRun} ) -func whichModeAndRecoverySystem(cmdline []byte) (mode string, sysLabel string, err error) { - scanner := bufio.NewScanner(bytes.NewBuffer(cmdline)) - scanner.Split(bufio.ScanWords) - - for scanner.Scan() { - w := scanner.Text() - if strings.HasPrefix(w, "snapd_recovery_mode=") { - if mode != "" { - return "", "", fmt.Errorf("cannot specify mode more than once") - } - mode = strings.SplitN(w, "=", 2)[1] - if mode == "" { - mode = ModeInstall - } - if !strutil.ListContains(validModes, mode) { - return "", "", fmt.Errorf("cannot use unknown mode %q", mode) - } - } - if strings.HasPrefix(w, "snapd_recovery_system=") { - if sysLabel != "" { - return "", "", fmt.Errorf("cannot specify recovery system label more than once") - } - sysLabel = strings.SplitN(w, "=", 2)[1] - } - } - if err := scanner.Err(); err != nil { +// ModeAndRecoverySystemFromKernelCommandLine returns the current system mode +// and the recovery system label as passed in the kernel command line by the +// bootloader. +func ModeAndRecoverySystemFromKernelCommandLine() (mode, sysLabel string, err error) { + m, err := osutil.KernelCommandLineKeyValues("snapd_recovery_mode", "snapd_recovery_system") + if err != nil { return "", "", err } + var modeOk bool + mode, modeOk = m["snapd_recovery_mode"] + + // no mode specified gets interpreted as install + if modeOk { + if mode == "" { + mode = ModeInstall + } else if !strutil.ListContains(validModes, mode) { + return "", "", fmt.Errorf("cannot use unknown mode %q", mode) + } + } + + sysLabel = m["snapd_recovery_system"] + switch { case mode == "" && sysLabel == "": return "", "", fmt.Errorf("cannot detect mode nor recovery system to use") + case mode == "" && sysLabel != "": + return "", "", fmt.Errorf("cannot specify system label without a mode") case mode == ModeInstall && sysLabel == "": return "", "", fmt.Errorf("cannot specify install mode without system label") case mode == ModeRun && sysLabel != "": @@ -89,22 +82,82 @@ return mode, sysLabel, nil } -// ModeAndRecoverySystemFromKernelCommandLine returns the current system mode -// and the recovery system label as passed in the kernel command line by the -// bootloader. -func ModeAndRecoverySystemFromKernelCommandLine() (mode, sysLabel string, err error) { - cmdline, err := ioutil.ReadFile(procCmdline) +var errBootConfigNotManaged = errors.New("boot config is not managed") + +func getBootloaderManagingItsAssets(where string, opts *bootloader.Options) (bootloader.TrustedAssetsBootloader, error) { + bl, err := bootloader.Find(where, opts) if err != nil { - return "", "", err + return nil, fmt.Errorf("internal error: cannot find trusted assets bootloader under %q: %v", where, err) + } + mbl, ok := bl.(bootloader.TrustedAssetsBootloader) + if !ok { + // the bootloader cannot manage its scripts + return nil, errBootConfigNotManaged } - return whichModeAndRecoverySystem(cmdline) + return mbl, nil } -// MockProcCmdline overrides the path to /proc/cmdline. For use in tests. -func MockProcCmdline(newPath string) (restore func()) { - oldProcCmdline := procCmdline - procCmdline = newPath - return func() { - procCmdline = oldProcCmdline +const ( + currentEdition = iota + candidateEdition +) + +func composeCommandLine(model *asserts.Model, currentOrCandidate int, mode, system string) (string, error) { + if model.Grade() == asserts.ModelGradeUnset { + return "", nil } + if mode != ModeRun && mode != ModeRecover { + return "", fmt.Errorf("internal error: unsupported command line mode %q", mode) + } + // get the run mode bootloader under the native run partition layout + opts := &bootloader.Options{ + Role: bootloader.RoleRunMode, + NoSlashBoot: true, + } + bootloaderRootDir := InitramfsUbuntuBootDir + modeArg := "snapd_recovery_mode=run" + systemArg := "" + if mode == ModeRecover { + // dealing with recovery system bootloader + opts.Role = bootloader.RoleRecovery + bootloaderRootDir = InitramfsUbuntuSeedDir + // recovery mode & system command line arguments + modeArg = "snapd_recovery_mode=recover" + systemArg = fmt.Sprintf("snapd_recovery_system=%v", system) + } + mbl, err := getBootloaderManagingItsAssets(bootloaderRootDir, opts) + if err != nil { + if err == errBootConfigNotManaged { + return "", nil + } + return "", err + } + // TODO:UC20: fetch extra args from gadget + extraArgs := "" + if currentOrCandidate == currentEdition { + return mbl.CommandLine(modeArg, systemArg, extraArgs) + } else { + return mbl.CandidateCommandLine(modeArg, systemArg, extraArgs) + } +} + +// ComposeRecoveryCommandLine composes the kernel command line used when booting +// a given system in recover mode. +func ComposeRecoveryCommandLine(model *asserts.Model, system string) (string, error) { + return composeCommandLine(model, currentEdition, ModeRecover, system) +} + +// ComposeCommandLine composes the kernel command line used when booting the +// system in run mode. +func ComposeCommandLine(model *asserts.Model) (string, error) { + return composeCommandLine(model, currentEdition, ModeRun, "") +} + +// TODO:UC20: add helper to compose candidate command line for a recovery system + +// ComposeCandidateCommandLine composes the kernel command line used when +// booting the system in run mode with the current built-in edition of managed +// boot assets. +func ComposeCandidateCommandLine(model *asserts.Model) (string, error) { + return composeCommandLine(model, candidateEdition, ModeRun, "") } diff -Nru snapd-2.45.1+20.04.2/boot/cmdline_test.go snapd-2.48.3+20.04/boot/cmdline_test.go --- snapd-2.45.1+20.04.2/boot/cmdline_test.go 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/boot/cmdline_test.go 2021-02-02 08:21:12.000000000 +0000 @@ -27,6 +27,10 @@ . "gopkg.in/check.v1" "github.com/snapcore/snapd/boot" + "github.com/snapcore/snapd/boot/boottest" + "github.com/snapcore/snapd/bootloader" + "github.com/snapcore/snapd/bootloader/bootloadertest" + "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/testutil" ) @@ -44,7 +48,7 @@ err := os.MkdirAll(filepath.Join(s.rootDir, "proc"), 0755) c.Assert(err, IsNil) - restore := boot.MockProcCmdline(filepath.Join(s.rootDir, "proc/cmdline")) + restore := osutil.MockProcCmdline(filepath.Join(s.rootDir, "proc/cmdline")) s.AddCleanup(restore) } @@ -82,13 +86,19 @@ cmd: "snapd_recovery_mode=install foo=bar", err: `cannot specify install mode without system label`, }, { - // boot scripts couldn't decide on mode - cmd: "snapd_recovery_mode=install snapd_recovery_system=1234 snapd_recovery_mode=run", - err: "cannot specify mode more than once", - }, { - // boot scripts couldn't decide which system to use - cmd: "snapd_recovery_system=not-this-one snapd_recovery_mode=install snapd_recovery_system=1234", - err: "cannot specify recovery system label more than once", + cmd: "snapd_recovery_system=1234", + err: `cannot specify system label without a mode`, + }, { + // multiple kernel command line params end up using the last one - this + // effectively matches the kernel handling too + cmd: "snapd_recovery_mode=install snapd_recovery_system=1234 snapd_recovery_mode=run", + mode: "run", + // label gets unset because it's not used for run mode + label: "", + }, { + cmd: "snapd_recovery_system=not-this-one snapd_recovery_mode=install snapd_recovery_system=1234", + mode: "install", + label: "1234", }} { c.Logf("tc: %q", tc) s.mockProcCmdlineContent(c, tc.cmd) @@ -103,3 +113,89 @@ } } } + +func (s *kernelCommandLineSuite) TestComposeCommandLineNotManagedHappy(c *C) { + model := boottest.MakeMockUC20Model() + + bl := bootloadertest.Mock("btloader", c.MkDir()) + bootloader.Force(bl) + defer bootloader.Force(nil) + + cmdline, err := boot.ComposeRecoveryCommandLine(model, "20200314") + c.Assert(err, IsNil) + c.Assert(cmdline, Equals, "") + + cmdline, err = boot.ComposeCommandLine(model) + c.Assert(err, IsNil) + c.Assert(cmdline, Equals, "") + + tbl := bl.WithTrustedAssets() + bootloader.Force(tbl) + + cmdline, err = boot.ComposeRecoveryCommandLine(model, "20200314") + c.Assert(err, IsNil) + c.Assert(cmdline, Equals, "snapd_recovery_mode=recover snapd_recovery_system=20200314") + + cmdline, err = boot.ComposeCommandLine(model) + c.Assert(err, IsNil) + c.Assert(cmdline, Equals, "snapd_recovery_mode=run") +} + +func (s *kernelCommandLineSuite) TestComposeCommandLineNotUC20(c *C) { + model := boottest.MakeMockModel() + + bl := bootloadertest.Mock("btloader", c.MkDir()) + bootloader.Force(bl) + defer bootloader.Force(nil) + cmdline, err := boot.ComposeRecoveryCommandLine(model, "20200314") + c.Assert(err, IsNil) + c.Check(cmdline, Equals, "") + + cmdline, err = boot.ComposeCommandLine(model) + c.Assert(err, IsNil) + c.Check(cmdline, Equals, "") +} + +func (s *kernelCommandLineSuite) TestComposeCommandLineManagedHappy(c *C) { + model := boottest.MakeMockUC20Model() + + tbl := bootloadertest.Mock("btloader", c.MkDir()).WithTrustedAssets() + bootloader.Force(tbl) + defer bootloader.Force(nil) + + tbl.StaticCommandLine = "panic=-1" + + cmdline, err := boot.ComposeRecoveryCommandLine(model, "20200314") + c.Assert(err, IsNil) + c.Assert(cmdline, Equals, "snapd_recovery_mode=recover snapd_recovery_system=20200314 panic=-1") + cmdline, err = boot.ComposeCommandLine(model) + c.Assert(err, IsNil) + c.Assert(cmdline, Equals, "snapd_recovery_mode=run panic=-1") + + cmdline, err = boot.ComposeRecoveryCommandLine(model, "20200314") + c.Assert(err, IsNil) + c.Assert(cmdline, Equals, "snapd_recovery_mode=recover snapd_recovery_system=20200314 panic=-1") + cmdline, err = boot.ComposeCommandLine(model) + c.Assert(err, IsNil) + c.Assert(cmdline, Equals, "snapd_recovery_mode=run panic=-1") +} + +func (s *kernelCommandLineSuite) TestComposeCandidateCommandLineManagedHappy(c *C) { + model := boottest.MakeMockUC20Model() + + tbl := bootloadertest.Mock("btloader", c.MkDir()).WithTrustedAssets() + bootloader.Force(tbl) + defer bootloader.Force(nil) + + tbl.StaticCommandLine = "panic=-1" + tbl.CandidateStaticCommandLine = "candidate panic=-1" + + cmdline, err := boot.ComposeCandidateCommandLine(model) + c.Assert(err, IsNil) + c.Assert(cmdline, Equals, "snapd_recovery_mode=run candidate panic=-1") + + // managed status is effectively ignored + cmdline, err = boot.ComposeCandidateCommandLine(model) + c.Assert(err, IsNil) + c.Assert(cmdline, Equals, "snapd_recovery_mode=run candidate panic=-1") +} diff -Nru snapd-2.45.1+20.04.2/boot/debug.go snapd-2.48.3+20.04/boot/debug.go --- snapd-2.45.1+20.04.2/boot/debug.go 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/boot/debug.go 2021-02-02 08:21:12.000000000 +0000 @@ -24,28 +24,55 @@ "io" "github.com/snapcore/snapd/bootloader" + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/osutil" ) // DumpBootVars writes a dump of the snapd bootvars to the given writer func DumpBootVars(w io.Writer, dir string, uc20 bool) error { - bloader, err := bootloader.Find(dir, nil) - if err != nil { - return err + opts := &bootloader.Options{ + NoSlashBoot: dir != "" && dir != "/", + } + switch dir { + // is it any of the well-known UC20 boot partition mount locations? + case InitramfsUbuntuBootDir: + opts.Role = bootloader.RoleRunMode + uc20 = true + case InitramfsUbuntuSeedDir: + opts.Role = bootloader.RoleRecovery + uc20 = true + } + if !opts.NoSlashBoot && !uc20 { + // this may still be a UC20 system + if osutil.FileExists(dirs.SnapModeenvFile) { + uc20 = true + } + } + allKeys := []string{ + "snap_mode", + "snap_core", + "snap_try_core", + "snap_kernel", + "snap_try_kernel", } - var allKeys []string if uc20 { - // TODO:UC20: what about snapd_recovery_kernel, snapd_recovery_mode, and - // snapd_recovery_system? - allKeys = []string{"kernel_status"} - } else { + if !opts.NoSlashBoot { + // no root directory set, default ot run mode + opts.Role = bootloader.RoleRunMode + } + // keys relevant to all uc20 bootloader implementations allKeys = []string{ - "snap_mode", - "snap_core", - "snap_try_core", + "snapd_recovery_mode", + "snapd_recovery_system", + "snapd_recovery_kernel", "snap_kernel", - "snap_try_kernel", + "kernel_status", } } + bloader, err := bootloader.Find(dir, opts) + if err != nil { + return err + } bootVars, err := bloader.GetBootVars(allKeys...) if err != nil { diff -Nru snapd-2.45.1+20.04.2/boot/errors.go snapd-2.48.3+20.04/boot/errors.go --- snapd-2.45.1+20.04.2/boot/errors.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48.3+20.04/boot/errors.go 2021-02-02 08:21:12.000000000 +0000 @@ -0,0 +1,49 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2020 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 boot + +import ( + "errors" + "fmt" +) + +// trySnapError is an error that only applies to the try snaps where multiple +// snaps are returned, this is mainly and primarily used in revisions(). +type trySnapError string + +func (sre trySnapError) Error() string { + return string(sre) +} + +func newTrySnapErrorf(format string, args ...interface{}) error { + return trySnapError(fmt.Sprintf(format, args...)) +} + +// isTrySnapError returns true if the given error is an error resulting from +// accessing information about the try snap or the trying status. +func isTrySnapError(err error) bool { + switch err.(type) { + case trySnapError: + return true + } + return false +} + +var errTrySnapFallback = errors.New("fallback to original snap") diff -Nru snapd-2.45.1+20.04.2/boot/export_test.go snapd-2.48.3+20.04/boot/export_test.go --- snapd-2.45.1+20.04.2/boot/export_test.go 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/boot/export_test.go 2021-02-02 08:21:12.000000000 +0000 @@ -20,7 +20,14 @@ package boot import ( + "fmt" + + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/bootloader" + "github.com/snapcore/snapd/secboot" + "github.com/snapcore/snapd/seed" "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/timings" ) func NewCoreBootParticipant(s snap.PlaceInfo, t snap.Type, dev Device) *coreBootParticipant { @@ -40,3 +47,144 @@ func (m *Modeenv) WasRead() bool { return m.read } + +func (m *Modeenv) DeepEqual(m2 *Modeenv) bool { + return m.deepEqual(m2) +} + +var ( + ModeenvKnownKeys = modeenvKnownKeys + + MarshalModeenvEntryTo = marshalModeenvEntryTo + UnmarshalModeenvValueFromCfg = unmarshalModeenvValueFromCfg + + NewTrustedAssetsCache = newTrustedAssetsCache + + ObserveSuccessfulBootWithAssets = observeSuccessfulBootAssets + SealKeyToModeenv = sealKeyToModeenv + ResealKeyToModeenv = resealKeyToModeenv + RecoveryBootChainsForSystems = recoveryBootChainsForSystems + SealKeyModelParams = sealKeyModelParams +) + +type BootAssetsMap = bootAssetsMap +type TrackedAsset = trackedAsset + +func (t *TrackedAsset) Equals(blName, name, hash string) error { + equal := t.hash == hash && + t.name == name && + t.blName == blName + if !equal { + return fmt.Errorf("not equal to bootloader %q tracked asset %v:%v", t.blName, t.name, t.hash) + } + return nil +} + +func (o *TrustedAssetsInstallObserver) CurrentTrustedBootAssetsMap() BootAssetsMap { + return o.currentTrustedBootAssetsMap() +} + +func (o *TrustedAssetsInstallObserver) CurrentTrustedRecoveryBootAssetsMap() BootAssetsMap { + return o.currentTrustedRecoveryBootAssetsMap() +} + +func (o *TrustedAssetsInstallObserver) CurrentDataEncryptionKey() secboot.EncryptionKey { + return o.dataEncryptionKey +} + +func (o *TrustedAssetsInstallObserver) CurrentSaveEncryptionKey() secboot.EncryptionKey { + return o.saveEncryptionKey +} + +func MockSecbootSealKeys(f func(keys []secboot.SealKeyRequest, params *secboot.SealKeysParams) error) (restore func()) { + old := secbootSealKeys + secbootSealKeys = f + return func() { + secbootSealKeys = old + } +} + +func MockSecbootResealKeys(f func(params *secboot.ResealKeysParams) error) (restore func()) { + old := secbootResealKeys + secbootResealKeys = f + return func() { + secbootResealKeys = old + } +} + +func MockSeedReadSystemEssential(f func(seedDir, label string, essentialTypes []snap.Type, tm timings.Measurer) (*asserts.Model, []*seed.Snap, error)) (restore func()) { + old := seedReadSystemEssential + seedReadSystemEssential = f + return func() { + seedReadSystemEssential = old + } +} + +func (o *TrustedAssetsUpdateObserver) InjectChangedAsset(blName, assetName, hash string, recovery bool) { + ta := &trackedAsset{ + blName: blName, + name: assetName, + hash: hash, + } + if !recovery { + o.changedAssets = append(o.changedAssets, ta) + } else { + o.seedChangedAssets = append(o.seedChangedAssets, ta) + } +} + +type BootAsset = bootAsset +type BootChain = bootChain +type PredictableBootChains = predictableBootChains + +const ( + BootChainEquivalent = bootChainEquivalent + BootChainDifferent = bootChainDifferent + BootChainUnrevisioned = bootChainUnrevisioned +) + +var ( + ToPredictableBootAsset = toPredictableBootAsset + ToPredictableBootChain = toPredictableBootChain + ToPredictableBootChains = toPredictableBootChains + PredictableBootChainsEqualForReseal = predictableBootChainsEqualForReseal + BootAssetsToLoadChains = bootAssetsToLoadChains + BootAssetLess = bootAssetLess + WriteBootChains = writeBootChains + ReadBootChains = readBootChains + IsResealNeeded = isResealNeeded +) + +func (b *bootChain) SetModelAssertion(model *asserts.Model) { + b.model = model +} + +func (b *bootChain) SetKernelBootFile(kbf bootloader.BootFile) { + b.kernelBootFile = kbf +} + +func (b *bootChain) KernelBootFile() bootloader.BootFile { + return b.kernelBootFile +} + +func MockHasFDESetupHook(f func() (bool, error)) (restore func()) { + oldHasFDESetupHook := HasFDESetupHook + HasFDESetupHook = f + return func() { + HasFDESetupHook = oldHasFDESetupHook + } +} + +func MockRunFDESetupHook(f func(string, *FDESetupHookParams) ([]byte, error)) (restore func()) { + oldRunFDESetupHook := RunFDESetupHook + RunFDESetupHook = f + return func() { RunFDESetupHook = oldRunFDESetupHook } +} + +func MockResealKeyToModeenvUsingFDESetupHook(f func(string, *asserts.Model, *Modeenv, bool) error) (restore func()) { + old := resealKeyToModeenvUsingFDESetupHook + resealKeyToModeenvUsingFDESetupHook = f + return func() { + resealKeyToModeenvUsingFDESetupHook = old + } +} diff -Nru snapd-2.45.1+20.04.2/boot/initramfs20dirs.go snapd-2.48.3+20.04/boot/initramfs20dirs.go --- snapd-2.45.1+20.04.2/boot/initramfs20dirs.go 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/boot/initramfs20dirs.go 2021-02-02 08:21:12.000000000 +0000 @@ -38,6 +38,10 @@ // during the initramfs, typically used in recover mode. InitramfsHostUbuntuDataDir string + // InitramfsHostWritableDir is the location of the host writable + // partition during the initramfs, typically used in recover mode. + InitramfsHostWritableDir string + // InitramfsUbuntuBootDir is the location of ubuntu-boot during the // initramfs. InitramfsUbuntuBootDir string @@ -46,6 +50,10 @@ // initramfs. InitramfsUbuntuSeedDir string + // InitramfsUbuntuSaveDir is the location of ubuntu-save during the + // initramfs. + InitramfsUbuntuSaveDir string + // InitramfsWritableDir is the location of the writable partition during the // initramfs. Note that this may refer to a temporary filesystem or a // physical partition depending on what system mode the system is in. @@ -56,20 +64,37 @@ // partition. InstallHostWritableDir string - // InitramfsEncryptionKeyDir is the location of the encrypted partition keys - // during the initramfs. - InitramfsEncryptionKeyDir string + // InstallHostFDEDataDir is the location of the FDE data during install mode. + InstallHostFDEDataDir string + + // InstallHostFDESaveDir is the directory of the FDE data on the + // ubuntu-save partition during install mode. For other modes, + // use dirs.SnapSaveFDEDirUnder(). + InstallHostFDESaveDir string + + // InitramfsSeedEncryptionKeyDir is the location of the encrypted partition + // keys during the initramfs on ubuntu-seed. + InitramfsSeedEncryptionKeyDir string + + // InitramfsBootEncryptionKeyDir is the location of the encrypted partition + // keys during the initramfs on ubuntu-boot. + InitramfsBootEncryptionKeyDir string ) func setInitramfsDirVars(rootdir string) { InitramfsRunMntDir = filepath.Join(rootdir, "run/mnt") InitramfsDataDir = filepath.Join(InitramfsRunMntDir, "data") InitramfsHostUbuntuDataDir = filepath.Join(InitramfsRunMntDir, "host", "ubuntu-data") + InitramfsHostWritableDir = filepath.Join(InitramfsHostUbuntuDataDir, "system-data") InitramfsUbuntuBootDir = filepath.Join(InitramfsRunMntDir, "ubuntu-boot") InitramfsUbuntuSeedDir = filepath.Join(InitramfsRunMntDir, "ubuntu-seed") + InitramfsUbuntuSaveDir = filepath.Join(InitramfsRunMntDir, "ubuntu-save") InstallHostWritableDir = filepath.Join(InitramfsRunMntDir, "ubuntu-data", "system-data") + InstallHostFDEDataDir = dirs.SnapFDEDirUnder(InstallHostWritableDir) + InstallHostFDESaveDir = filepath.Join(InitramfsUbuntuSaveDir, "device/fde") InitramfsWritableDir = filepath.Join(InitramfsDataDir, "system-data") - InitramfsEncryptionKeyDir = filepath.Join(InitramfsUbuntuSeedDir, "device/fde") + InitramfsSeedEncryptionKeyDir = filepath.Join(InitramfsUbuntuSeedDir, "device/fde") + InitramfsBootEncryptionKeyDir = filepath.Join(InitramfsUbuntuBootDir, "device/fde") } func init() { diff -Nru snapd-2.45.1+20.04.2/boot/initramfs.go snapd-2.48.3+20.04/boot/initramfs.go --- snapd-2.45.1+20.04.2/boot/initramfs.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48.3+20.04/boot/initramfs.go 2021-02-02 08:21:12.000000000 +0000 @@ -0,0 +1,121 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2020 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 boot + +import ( + "os/exec" + "time" + + "github.com/snapcore/snapd/bootloader" + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/snap" +) + +// InitramfsRunModeSelectSnapsToMount returns a map of the snap paths to mount +// for the specified snap types. +func InitramfsRunModeSelectSnapsToMount( + typs []snap.Type, + modeenv *Modeenv, +) (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) + switch typ { + case snap.TypeBase: + bs := &bootState20Base{} + selectSnapFn = bs.selectAndCommitSnapInitramfsMount + case snap.TypeKernel: + blOpts := &bootloader.Options{ + Role: bootloader.RoleRunMode, + NoSlashBoot: true, + } + blDir := InitramfsUbuntuBootDir + bs := &bootState20Kernel{ + blDir: blDir, + blOpts: blOpts, + } + selectSnapFn = bs.selectAndCommitSnapInitramfsMount + } + sn, err = selectSnapFn(modeenv) + if err != nil { + return nil, err + } + + m[typ] = sn + } + + return m, nil +} + +// EnsureNextBootToRunMode will mark the bootenv of the recovery bootloader such +// that recover mode is now ready to switch back to run mode upon any reboot. +func EnsureNextBootToRunMode(systemLabel string) error { + // at the end of the initramfs we need to set the bootenv such that a reboot + // now at any point will rollback to run mode without additional config or + // actions + + opts := &bootloader.Options{ + // setup the recovery bootloader + Role: bootloader.RoleRecovery, + } + + bl, err := bootloader.Find(InitramfsUbuntuSeedDir, opts) + if err != nil { + return err + } + + m := map[string]string{ + "snapd_recovery_system": systemLabel, + "snapd_recovery_mode": "run", + } + return bl.SetBootVars(m) +} + +// initramfsReboot triggers a reboot from the initramfs immediately +var initramfsReboot = func() error { + if osutil.IsTestBinary() { + panic("initramfsReboot must be mocked in tests") + } + + out, err := exec.Command("/sbin/reboot").CombinedOutput() + if err != nil { + return osutil.OutputErr(out, err) + } + + // reboot command in practice seems to not return, but apparently it is + // theoretically possible it could return, so to account for this we will + // loop for a "long" time waiting for the system to be rebooted, and panic + // after a timeout so that if something goes wrong with the reboot we do + // exit with some info about the expected reboot + time.Sleep(10 * time.Minute) + panic("expected reboot to happen within 10 minutes after calling /sbin/reboot") +} + +func MockInitramfsReboot(f func() error) (restore func()) { + osutil.MustBeTestBinary("initramfsReboot only can be mocked in tests") + old := initramfsReboot + initramfsReboot = f + return func() { + initramfsReboot = old + } +} diff -Nru snapd-2.45.1+20.04.2/boot/initramfs_test.go snapd-2.48.3+20.04/boot/initramfs_test.go --- snapd-2.45.1+20.04.2/boot/initramfs_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48.3+20.04/boot/initramfs_test.go 2021-02-02 08:21:12.000000000 +0000 @@ -0,0 +1,632 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2020 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 boot_test + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/boot" + "github.com/snapcore/snapd/boot/boottest" + "github.com/snapcore/snapd/bootloader" + "github.com/snapcore/snapd/bootloader/bootloadertest" + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/snap" +) + +type initramfsSuite struct { + baseBootenvSuite +} + +var _ = Suite(&initramfsSuite{}) + +func (s *initramfsSuite) SetUpTest(c *C) { + s.baseBootenvSuite.SetUpTest(c) +} + +func (s *initramfsSuite) TestEnsureNextBootToRunMode(c *C) { + // with no bootloader available we can't mark successful + err := boot.EnsureNextBootToRunMode("label") + c.Assert(err, ErrorMatches, "cannot determine bootloader") + + // forcing a bootloader works + bloader := bootloadertest.Mock("mock", c.MkDir()) + bootloader.Force(bloader) + defer bootloader.Force(nil) + + err = boot.EnsureNextBootToRunMode("label") + c.Assert(err, IsNil) + + // the bloader vars have been updated + m, err := bloader.GetBootVars("snapd_recovery_mode", "snapd_recovery_system") + c.Assert(err, IsNil) + c.Assert(m, DeepEquals, map[string]string{ + "snapd_recovery_mode": "run", + "snapd_recovery_system": "label", + }) +} + +func (s *initramfsSuite) TestEnsureNextBootToRunModeRealBootloader(c *C) { + // create a real grub.cfg on ubuntu-seed + err := os.MkdirAll(filepath.Join(boot.InitramfsUbuntuSeedDir, "EFI/ubuntu"), 0755) + c.Assert(err, IsNil) + + err = ioutil.WriteFile(filepath.Join(boot.InitramfsUbuntuSeedDir, "EFI/ubuntu", "grub.cfg"), nil, 0644) + c.Assert(err, IsNil) + + err = boot.EnsureNextBootToRunMode("somelabel") + c.Assert(err, IsNil) + + opts := &bootloader.Options{ + // setup the recovery bootloader + Role: bootloader.RoleRecovery, + } + bloader, err := bootloader.Find(boot.InitramfsUbuntuSeedDir, opts) + c.Assert(err, IsNil) + c.Assert(bloader.Name(), Equals, "grub") + + // the bloader vars have been updated + m, err := bloader.GetBootVars("snapd_recovery_mode", "snapd_recovery_system") + c.Assert(err, IsNil) + c.Assert(m, DeepEquals, map[string]string{ + "snapd_recovery_mode": "run", + "snapd_recovery_system": "somelabel", + }) +} + +func makeSnapFilesOnInitramfsUbuntuData(c *C, comment CommentInterface, snaps ...snap.PlaceInfo) (restore func()) { + // also make sure the snaps also exist on ubuntu-data + snapDir := dirs.SnapBlobDirUnder(boot.InitramfsWritableDir) + err := os.MkdirAll(snapDir, 0755) + c.Assert(err, IsNil, comment) + paths := make([]string, 0, len(snaps)) + for _, sn := range snaps { + snPath := filepath.Join(snapDir, sn.Filename()) + paths = append(paths, snPath) + err = ioutil.WriteFile(snPath, nil, 0644) + c.Assert(err, IsNil, comment) + } + return func() { + for _, path := range paths { + err := os.Remove(path) + c.Assert(err, IsNil, comment) + } + } +} + +func (s *initramfsSuite) TestInitramfsRunModeSelectSnapsToMount(c *C) { + // make some snap infos we will use in the tests + kernel1, err := snap.ParsePlaceInfoFromSnapFileName("pc-kernel_1.snap") + c.Assert(err, IsNil) + + kernel2, err := snap.ParsePlaceInfoFromSnapFileName("pc-kernel_2.snap") + c.Assert(err, IsNil) + + base1, err := snap.ParsePlaceInfoFromSnapFileName("core20_1.snap") + c.Assert(err, IsNil) + + base2, err := snap.ParsePlaceInfoFromSnapFileName("core20_2.snap") + c.Assert(err, IsNil) + + baseT := snap.TypeBase + kernelT := snap.TypeKernel + + tt := []struct { + m *boot.Modeenv + expectedM *boot.Modeenv + typs []snap.Type + kernel snap.PlaceInfo + trykernel snap.PlaceInfo + blvars map[string]string + snapsToMake []snap.PlaceInfo + expected map[snap.Type]snap.PlaceInfo + errPattern string + comment string + expRebootPanic string + }{ + // + // default paths + // + + // default base path + { + m: &boot.Modeenv{Mode: "run", Base: base1.Filename()}, + typs: []snap.Type{baseT}, + snapsToMake: []snap.PlaceInfo{base1}, + expected: map[snap.Type]snap.PlaceInfo{baseT: base1}, + comment: "default base path", + }, + // default kernel path + { + 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}, + comment: "default kernel path", + }, + + // + // happy kernel upgrade paths + // + + // kernel upgrade path + { + m: &boot.Modeenv{Mode: "run", CurrentKernels: []string{kernel1.Filename(), kernel2.Filename()}}, + kernel: kernel1, + trykernel: kernel2, + typs: []snap.Type{kernelT}, + blvars: map[string]string{"kernel_status": boot.TryingStatus}, + snapsToMake: []snap.PlaceInfo{kernel1, kernel2}, + expected: map[snap.Type]snap.PlaceInfo{kernelT: kernel2}, + comment: "successful kernel upgrade path", + }, + // extraneous kernel extracted/set, but kernel_status is default, + // so the bootloader will ignore that and boot the default kernel + // note that this test case is a bit ambiguous as we don't actually know + // in the initramfs that the bootloader actually booted the default + // kernel, we are just assuming that the bootloader implementation in + // the real world is robust enough to only boot the try kernel if and + // only if kernel_status is not DefaultStatus + { + m: &boot.Modeenv{Mode: "run", CurrentKernels: []string{kernel1.Filename(), kernel2.Filename()}}, + kernel: kernel1, + trykernel: kernel2, + typs: []snap.Type{kernelT}, + blvars: map[string]string{"kernel_status": boot.DefaultStatus}, + snapsToMake: []snap.PlaceInfo{kernel1, kernel2}, + expected: map[snap.Type]snap.PlaceInfo{kernelT: kernel1}, + comment: "fallback kernel upgrade path, due to kernel_status empty (default)", + }, + + // + // unhappy reboot fallback kernel paths + // + + // kernel upgrade path, but reboots to fallback due to untrusted kernel from modeenv + { + m: &boot.Modeenv{Mode: "run", CurrentKernels: []string{kernel1.Filename()}}, + kernel: kernel1, + trykernel: kernel2, + typs: []snap.Type{kernelT}, + blvars: map[string]string{"kernel_status": boot.TryingStatus}, + snapsToMake: []snap.PlaceInfo{kernel1, kernel2}, + expRebootPanic: "reboot due to modeenv untrusted try kernel", + 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 + { + m: &boot.Modeenv{Mode: "run", CurrentKernels: []string{kernel1.Filename(), kernel2.Filename()}}, + kernel: kernel1, + trykernel: kernel2, + typs: []snap.Type{kernelT}, + blvars: map[string]string{"kernel_status": boot.TryingStatus}, + snapsToMake: []snap.PlaceInfo{kernel1}, + expRebootPanic: "reboot due to try kernel file not existing", + comment: "fallback kernel upgrade path, due to try kernel file not existing", + }, + // kernel upgrade path, but reboots to fallback due to invalid kernel_status + { + m: &boot.Modeenv{Mode: "run", CurrentKernels: []string{kernel1.Filename(), kernel2.Filename()}}, + kernel: kernel1, + trykernel: kernel2, + typs: []snap.Type{kernelT}, + blvars: map[string]string{"kernel_status": boot.TryStatus}, + snapsToMake: []snap.PlaceInfo{kernel1, kernel2}, + expRebootPanic: "reboot due to kernel_status wrong", + comment: "fallback kernel upgrade path, due to kernel_status wrong", + }, + + // + // unhappy initramfs fail kernel paths + // + + // fallback kernel not trusted in modeenv + { + m: &boot.Modeenv{Mode: "run"}, + kernel: kernel1, + typs: []snap.Type{kernelT}, + snapsToMake: []snap.PlaceInfo{kernel1}, + errPattern: fmt.Sprintf("fallback kernel snap %q is not trusted in the modeenv", kernel1.Filename()), + comment: "fallback kernel not trusted in modeenv", + }, + // fallback kernel file doesn't exist + { + m: &boot.Modeenv{Mode: "run", CurrentKernels: []string{kernel1.Filename()}}, + kernel: kernel1, + typs: []snap.Type{kernelT}, + errPattern: fmt.Sprintf("kernel snap %q does not exist on ubuntu-data", kernel1.Filename()), + comment: "fallback kernel file doesn't exist", + }, + + // + // happy base upgrade paths + // + + // successful base upgrade path + { + m: &boot.Modeenv{ + Mode: "run", + Base: base1.Filename(), + TryBase: base2.Filename(), + BaseStatus: boot.TryStatus, + }, + expectedM: &boot.Modeenv{ + Mode: "run", + Base: base1.Filename(), + TryBase: base2.Filename(), + BaseStatus: boot.TryingStatus, + }, + typs: []snap.Type{baseT}, + snapsToMake: []snap.PlaceInfo{base1, base2}, + expected: map[snap.Type]snap.PlaceInfo{baseT: base2}, + comment: "successful base upgrade path", + }, + // base upgrade path, but uses fallback due to try base file not existing + { + m: &boot.Modeenv{ + Mode: "run", + Base: base1.Filename(), + TryBase: base2.Filename(), + BaseStatus: boot.TryStatus, + }, + expectedM: &boot.Modeenv{ + Mode: "run", + Base: base1.Filename(), + TryBase: base2.Filename(), + BaseStatus: boot.TryStatus, + }, + typs: []snap.Type{baseT}, + snapsToMake: []snap.PlaceInfo{base1}, + expected: map[snap.Type]snap.PlaceInfo{baseT: base1}, + comment: "fallback base upgrade path, due to missing try base file", + }, + // base upgrade path, but uses fallback due to base_status trying + { + m: &boot.Modeenv{ + Mode: "run", + Base: base1.Filename(), + TryBase: base2.Filename(), + BaseStatus: boot.TryingStatus, + }, + expectedM: &boot.Modeenv{ + Mode: "run", + Base: base1.Filename(), + TryBase: base2.Filename(), + BaseStatus: boot.DefaultStatus, + }, + typs: []snap.Type{baseT}, + snapsToMake: []snap.PlaceInfo{base1, base2}, + expected: map[snap.Type]snap.PlaceInfo{baseT: base1}, + comment: "fallback base upgrade path, due to base_status trying", + }, + // base upgrade path, but uses fallback due to base_status default + { + m: &boot.Modeenv{ + Mode: "run", + Base: base1.Filename(), + TryBase: base2.Filename(), + BaseStatus: boot.DefaultStatus, + }, + expectedM: &boot.Modeenv{ + Mode: "run", + Base: base1.Filename(), + TryBase: base2.Filename(), + BaseStatus: boot.DefaultStatus, + }, + typs: []snap.Type{baseT}, + snapsToMake: []snap.PlaceInfo{base1, base2}, + expected: map[snap.Type]snap.PlaceInfo{baseT: base1}, + comment: "fallback base upgrade path, due to missing base_status", + }, + + // + // unhappy base paths + // + + // base snap unset + { + m: &boot.Modeenv{Mode: "run"}, + typs: []snap.Type{baseT}, + snapsToMake: []snap.PlaceInfo{base1}, + errPattern: "fallback base snap unusable: cannot get snap revision: modeenv base boot variable is empty", + comment: "base snap unset in modeenv", + }, + // base snap file doesn't exist + { + 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()), + comment: "base snap unset in modeenv", + }, + // unhappy, but silent path with fallback, due to invalid try base snap name + { + m: &boot.Modeenv{ + Mode: "run", + Base: base1.Filename(), + TryBase: "bogusname", + BaseStatus: boot.TryStatus, + }, + typs: []snap.Type{baseT}, + snapsToMake: []snap.PlaceInfo{base1}, + expected: map[snap.Type]snap.PlaceInfo{baseT: base1}, + comment: "corrupted base snap name", + }, + + // + // combined cases + // + + // default + { + m: &boot.Modeenv{ + Mode: "run", + Base: base1.Filename(), + CurrentKernels: []string{kernel1.Filename()}, + }, + expectedM: &boot.Modeenv{ + Mode: "run", + Base: base1.Filename(), + CurrentKernels: []string{kernel1.Filename()}, + }, + kernel: kernel1, + typs: []snap.Type{baseT, kernelT}, + snapsToMake: []snap.PlaceInfo{base1, kernel1}, + expected: map[snap.Type]snap.PlaceInfo{ + baseT: base1, + kernelT: kernel1, + }, + comment: "default combined kernel + base", + }, + // combined, upgrade only the kernel + { + m: &boot.Modeenv{ + Mode: "run", + Base: base1.Filename(), + CurrentKernels: []string{kernel1.Filename(), kernel2.Filename()}, + }, + expectedM: &boot.Modeenv{ + Mode: "run", + Base: base1.Filename(), + CurrentKernels: []string{kernel1.Filename(), kernel2.Filename()}, + }, + kernel: kernel1, + trykernel: kernel2, + typs: []snap.Type{baseT, kernelT}, + blvars: map[string]string{"kernel_status": boot.TryingStatus}, + snapsToMake: []snap.PlaceInfo{base1, kernel1, kernel2}, + expected: map[snap.Type]snap.PlaceInfo{ + baseT: base1, + kernelT: kernel2, + }, + comment: "combined kernel + base, successful kernel upgrade", + }, + // combined, upgrade only the base + { + m: &boot.Modeenv{ + Mode: "run", + Base: base1.Filename(), + TryBase: base2.Filename(), + BaseStatus: boot.TryStatus, + CurrentKernels: []string{kernel1.Filename()}, + }, + expectedM: &boot.Modeenv{ + Mode: "run", + Base: base1.Filename(), + TryBase: base2.Filename(), + BaseStatus: boot.TryingStatus, + CurrentKernels: []string{kernel1.Filename()}, + }, + kernel: kernel1, + typs: []snap.Type{baseT, kernelT}, + snapsToMake: []snap.PlaceInfo{base1, base2, kernel1}, + expected: map[snap.Type]snap.PlaceInfo{ + baseT: base2, + kernelT: kernel1, + }, + comment: "combined kernel + base, successful base upgrade", + }, + // bonus points: combined upgrade kernel and base + { + m: &boot.Modeenv{ + Mode: "run", + Base: base1.Filename(), + TryBase: base2.Filename(), + BaseStatus: boot.TryStatus, + CurrentKernels: []string{kernel1.Filename(), kernel2.Filename()}, + }, + expectedM: &boot.Modeenv{ + Mode: "run", + Base: base1.Filename(), + TryBase: base2.Filename(), + BaseStatus: boot.TryingStatus, + CurrentKernels: []string{kernel1.Filename(), kernel2.Filename()}, + }, + kernel: kernel1, + trykernel: kernel2, + typs: []snap.Type{baseT, kernelT}, + blvars: map[string]string{"kernel_status": boot.TryingStatus}, + snapsToMake: []snap.PlaceInfo{base1, base2, kernel1, kernel2}, + expected: map[snap.Type]snap.PlaceInfo{ + baseT: base2, + kernelT: kernel2, + }, + comment: "combined kernel + base, successful base + kernel upgrade", + }, + // combined, fallback upgrade on kernel + { + m: &boot.Modeenv{ + Mode: "run", + Base: base1.Filename(), + CurrentKernels: []string{kernel1.Filename(), kernel2.Filename()}, + }, + expectedM: &boot.Modeenv{ + Mode: "run", + Base: base1.Filename(), + CurrentKernels: []string{kernel1.Filename(), kernel2.Filename()}, + }, + kernel: kernel1, + trykernel: kernel2, + typs: []snap.Type{baseT, kernelT}, + blvars: map[string]string{"kernel_status": boot.DefaultStatus}, + snapsToMake: []snap.PlaceInfo{base1, kernel1, kernel2}, + expected: map[snap.Type]snap.PlaceInfo{ + baseT: base1, + kernelT: kernel1, + }, + comment: "combined kernel + base, fallback kernel upgrade, due to missing boot var", + }, + // combined, fallback upgrade on base + { + m: &boot.Modeenv{ + Mode: "run", + Base: base1.Filename(), + TryBase: base2.Filename(), + BaseStatus: boot.TryingStatus, + CurrentKernels: []string{kernel1.Filename()}, + }, + expectedM: &boot.Modeenv{ + Mode: "run", + Base: base1.Filename(), + TryBase: base2.Filename(), + BaseStatus: boot.DefaultStatus, + CurrentKernels: []string{kernel1.Filename()}, + }, + kernel: kernel1, + typs: []snap.Type{baseT, kernelT}, + snapsToMake: []snap.PlaceInfo{base1, base2, kernel1}, + expected: map[snap.Type]snap.PlaceInfo{ + baseT: base1, + kernelT: kernel1, + }, + comment: "combined kernel + base, fallback base upgrade, due to base_status trying", + }, + } + + // do both the normal uc20 bootloader and the env ref bootloader + bloaderTable := []struct { + bl interface { + bootloader.Bootloader + SetEnabledKernel(s snap.PlaceInfo) (restore func()) + SetEnabledTryKernel(s snap.PlaceInfo) (restore func()) + } + name string + }{ + { + boottest.MockUC20RunBootenv(bootloadertest.Mock("mock", c.MkDir())), + "env ref extracted kernel", + }, + { + boottest.MockUC20EnvRefExtractedKernelRunBootenv(bootloadertest.Mock("mock", c.MkDir())), + "extracted run kernel image", + }, + } + + for _, tbl := range bloaderTable { + bl := tbl.bl + for _, t := range tt { + var cleanups []func() + + comment := Commentf("[%s] %s", tbl.name, t.comment) + + // we use a panic to simulate a reboot + if t.expRebootPanic != "" { + r := boot.MockInitramfsReboot(func() error { + panic(t.expRebootPanic) + }) + cleanups = append(cleanups, r) + } + + bootloader.Force(bl) + cleanups = append(cleanups, func() { bootloader.Force(nil) }) + + // set the bl kernel / try kernel + if t.kernel != nil { + cleanups = append(cleanups, bl.SetEnabledKernel(t.kernel)) + } + + if t.trykernel != nil { + cleanups = append(cleanups, bl.SetEnabledTryKernel(t.trykernel)) + } + + if t.blvars != nil { + c.Assert(bl.SetBootVars(t.blvars), IsNil, comment) + cleanBootVars := make(map[string]string, len(t.blvars)) + for k := range t.blvars { + cleanBootVars[k] = "" + } + cleanups = append(cleanups, func() { + c.Assert(bl.SetBootVars(cleanBootVars), IsNil, comment) + }) + } + + if len(t.snapsToMake) != 0 { + r := makeSnapFilesOnInitramfsUbuntuData(c, 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) + // 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(err, IsNil, comment) + + m, err := boot.ReadModeenv(boot.InitramfsWritableDir) + c.Assert(err, IsNil, comment) + + if t.expRebootPanic != "" { + f := func() { boot.InitramfsRunModeSelectSnapsToMount(t.typs, m) } + c.Assert(f, PanicMatches, t.expRebootPanic, comment) + } else { + mountSnaps, err := boot.InitramfsRunModeSelectSnapsToMount(t.typs, m) + if t.errPattern != "" { + c.Assert(err, ErrorMatches, t.errPattern, comment) + } else { + c.Assert(err, IsNil, comment) + c.Assert(mountSnaps, DeepEquals, t.expected, comment) + } + } + + // check that the modeenv changed as expected + if t.expectedM != nil { + newM, err := boot.ReadModeenv(boot.InitramfsWritableDir) + c.Assert(err, IsNil, comment) + c.Assert(newM.Base, Equals, t.expectedM.Base, comment) + c.Assert(newM.BaseStatus, Equals, t.expectedM.BaseStatus, comment) + c.Assert(newM.TryBase, Equals, t.expectedM.TryBase, comment) + + // shouldn't be changing in the initramfs, but be safe + c.Assert(newM.CurrentKernels, DeepEquals, t.expectedM.CurrentKernels, comment) + } + + // clean up + for _, r := range cleanups { + r() + } + } + } +} diff -Nru snapd-2.45.1+20.04.2/boot/kernel_os_test.go snapd-2.48.3+20.04/boot/kernel_os_test.go --- snapd-2.45.1+20.04.2/boot/kernel_os_test.go 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/boot/kernel_os_test.go 2021-02-02 08:21:12.000000000 +0000 @@ -77,7 +77,7 @@ info.RealName = "core" info.Revision = snap.R(100) - bs := boot.NewCoreBootParticipant(info, info.GetType(), coreDev) + bs := boot.NewCoreBootParticipant(info, info.Type(), coreDev) reboot, err := bs.SetNextBoot() c.Assert(err, IsNil) @@ -99,7 +99,7 @@ info.RealName = "core18" info.Revision = snap.R(1818) - bs := boot.NewCoreBootParticipant(info, info.GetType(), coreDev) + bs := boot.NewCoreBootParticipant(info, info.Type(), coreDev) reboot, err := bs.SetNextBoot() c.Assert(err, IsNil) @@ -148,7 +148,7 @@ } func (s *bootenv20Suite) TestSetNextBoot20ForKernel(c *C) { - coreDev := boottest.MockUC20Device("pc-kernel") + coreDev := boottest.MockUC20Device("", nil) c.Assert(coreDev.HasModeenv(), Equals, true) r := setupUC20Bootenv( @@ -191,7 +191,7 @@ } func (s *bootenv20EnvRefKernelSuite) TestSetNextBoot20ForKernel(c *C) { - coreDev := boottest.MockUC20Device("pc-kernel") + coreDev := boottest.MockUC20Device("", nil) c.Assert(coreDev.HasModeenv(), Equals, true) r := setupUC20Bootenv( @@ -245,7 +245,7 @@ } func (s *bootenv20Suite) TestSetNextBoot20ForKernelForTheSameKernel(c *C) { - coreDev := boottest.MockUC20Device("pc-kernel") + coreDev := boottest.MockUC20Device("", nil) c.Assert(coreDev.HasModeenv(), Equals, true) r := setupUC20Bootenv( @@ -288,7 +288,7 @@ } func (s *bootenv20EnvRefKernelSuite) TestSetNextBoot20ForKernelForTheSameKernel(c *C) { - coreDev := boottest.MockUC20Device("pc-kernel") + coreDev := boottest.MockUC20Device("", nil) c.Assert(coreDev.HasModeenv(), Equals, true) r := setupUC20Bootenv( @@ -348,7 +348,7 @@ } func (s *bootenv20Suite) TestSetNextBoot20ForKernelForTheSameKernelTryMode(c *C) { - coreDev := boottest.MockUC20Device("pc-kernel") + coreDev := boottest.MockUC20Device("", nil) c.Assert(coreDev.HasModeenv(), Equals, true) // set all the same vars as if we were doing trying, except don't set a try @@ -402,7 +402,7 @@ } func (s *bootenv20EnvRefKernelSuite) TestSetNextBoot20ForKernelForTheSameKernelTryMode(c *C) { - coreDev := boottest.MockUC20Device("pc-kernel") + coreDev := boottest.MockUC20Device("", nil) c.Assert(coreDev.HasModeenv(), Equals, true) // set all the same vars as if we were doing trying, except don't set a try @@ -477,9 +477,11 @@ func (s *ubootSuite) forceUC20UbootBootloader(c *C) { bootloader.Force(nil) - // to find the uboot bootloader we need to pass in NoSlashBoot because - // that's where the gadget assets get installed to + // for the uboot bootloader InstallBootConfig we pass in + // NoSlashBoot because that's where the gadget assets get + // installed to installOpts := &bootloader.Options{ + Role: bootloader.RoleRunMode, NoSlashBoot: true, } @@ -505,10 +507,9 @@ err = os.Rename(fn, targetFile) c.Assert(err, IsNil) - // however when finding the bootloader, since we want it to show up as the - // "runtime" bootloader, just use ExtractedRunKernelImage + // find the run mode bootloader under /boot runtimeOpts := &bootloader.Options{ - ExtractedRunKernelImage: true, + Role: bootloader.RoleRunMode, } bloader, err := bootloader.Find("", runtimeOpts) diff -Nru snapd-2.45.1+20.04.2/boot/makebootable.go snapd-2.48.3+20.04/boot/makebootable.go --- snapd-2.45.1+20.04.2/boot/makebootable.go 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/boot/makebootable.go 2021-02-02 08:21:12.000000000 +0000 @@ -54,13 +54,13 @@ // rootdir points to an image filesystem (UC 16/18), image recovery // filesystem (UC20 at prepare-image time) or ephemeral system (UC20 // install mode). -func MakeBootable(model *asserts.Model, rootdir string, bootWith *BootableSet) error { +func MakeBootable(model *asserts.Model, rootdir string, bootWith *BootableSet, sealer *TrustedAssetsInstallObserver) error { if model.Grade() == asserts.ModelGradeUnset { return makeBootable16(model, rootdir, bootWith) } if !bootWith.Recovery { - return makeBootable20RunMode(model, rootdir, bootWith) + return makeBootable20RunMode(model, rootdir, bootWith, sealer) } return makeBootable20(model, rootdir, bootWith) } @@ -159,7 +159,7 @@ opts := &bootloader.Options{ PrepareImageTime: true, // setup the recovery bootloader - Recovery: true, + Role: bootloader.RoleRecovery, } // install the bootloader configuration from the gadget @@ -181,6 +181,8 @@ // ubuntu-seed blVars := map[string]string{ "snapd_recovery_system": bootWith.RecoverySystemLabel, + // always set the mode as install + "snapd_recovery_mode": ModeInstall, } if err := bl.SetBootVars(blVars); err != nil { return fmt.Errorf("cannot set recovery environment: %v", err) @@ -225,9 +227,8 @@ return nil } -func makeBootable20RunMode(model *asserts.Model, rootdir string, bootWith *BootableSet) error { +func makeBootable20RunMode(model *asserts.Model, rootdir string, bootWith *BootableSet, sealer *TrustedAssetsInstallObserver) error { // TODO:UC20: - // - create grub.cfg instead of using the gadget one // - 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 @@ -256,10 +257,27 @@ } } + // replicate the boot assets cache in host's writable + if err := CopyBootAssetsCacheToRoot(InstallHostWritableDir); err != nil { + return fmt.Errorf("cannot replicate boot assets cache: %v", err) + } + + var currentTrustedBootAssets bootAssetsMap + var currentTrustedRecoveryBootAssets bootAssetsMap + if sealer != nil { + currentTrustedBootAssets = sealer.currentTrustedBootAssetsMap() + currentTrustedRecoveryBootAssets = sealer.currentTrustedRecoveryBootAssetsMap() + } + recoverySystemLabel := filepath.Base(bootWith.RecoverySystemDir) // write modeenv on the ubuntu-data partition modeenv := &Modeenv{ Mode: "run", - RecoverySystem: filepath.Base(bootWith.RecoverySystemDir), + RecoverySystem: recoverySystemLabel, + // default to the system we were installed from + CurrentRecoverySystems: []string{recoverySystemLabel}, + CurrentTrustedBootAssets: currentTrustedBootAssets, + CurrentTrustedRecoveryBootAssets: currentTrustedRecoveryBootAssets, + // keep this comment to make gofmt 1.9 happy Base: filepath.Base(bootWith.BasePath), CurrentKernels: []string{bootWith.Kernel.Filename()}, BrandID: model.BrandID(), @@ -272,15 +290,19 @@ // get the ubuntu-boot bootloader and extract the kernel there opts := &bootloader.Options{ + // Bootloader for run mode + Role: bootloader.RoleRunMode, // At this point the run mode bootloader is under the native - // layout, no /boot mount. + // run partition layout, no /boot mount. NoSlashBoot: true, - // Bootloader that supports kernel asset extraction - ExtractedRunKernelImage: true, } - bl, err := bootloader.Find(InitramfsUbuntuBootDir, opts) + // the bootloader config may have been installed when the ubuntu-boot + // partition was created, but for a trusted assets the bootloader config + // will be installed further down; for now identify the run mode + // bootloader by looking at the gadget + bl, err := bootloader.ForGadget(bootWith.UnpackedGadgetDir, InitramfsUbuntuBootDir, opts) if err != nil { - return fmt.Errorf("internal error: cannot find run system bootloader: %v", err) + return fmt.Errorf("internal error: cannot identify run system bootloader: %v", err) } // extract the kernel first and mark kernel_status ready @@ -330,12 +352,30 @@ return fmt.Errorf("cannot set run system environment: %v", err) } + _, ok = bl.(bootloader.TrustedAssetsBootloader) + if ok { + // the bootloader can manage its boot config + + // installing boot config must be performed after the boot + // partition has been populated with gadget data + if err := bl.InstallBootConfig(bootWith.UnpackedGadgetDir, opts); err != nil { + return fmt.Errorf("cannot install managed bootloader assets: %v", err) + } + } + + if sealer != nil { + // seal the encryption key to the parameters specified in modeenv + if err := sealKeyToModeenv(sealer.dataEncryptionKey, sealer.saveEncryptionKey, model, modeenv); err != nil { + return err + } + } + // LAST step: update recovery bootloader environment to indicate that we // transition to run mode now opts = &bootloader.Options{ // let the bootloader know we will be touching the recovery // partition - Recovery: true, + Role: bootloader.RoleRecovery, } bl, err = bootloader.Find(InitramfsUbuntuSeedDir, opts) if err != nil { diff -Nru snapd-2.45.1+20.04.2/boot/makebootable_test.go snapd-2.48.3+20.04/boot/makebootable_test.go --- snapd-2.45.1+20.04.2/boot/makebootable_test.go 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/boot/makebootable_test.go 2021-02-02 08:21:12.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2014-2019 Canonical Ltd + * Copyright (C) 2014-2020 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,17 +28,23 @@ . "gopkg.in/check.v1" "github.com/snapcore/snapd/asserts" - "github.com/snapcore/snapd/asserts/assertstest" "github.com/snapcore/snapd/boot" + "github.com/snapcore/snapd/boot/boottest" "github.com/snapcore/snapd/bootloader" + "github.com/snapcore/snapd/bootloader/assets" "github.com/snapcore/snapd/bootloader/bootloadertest" + "github.com/snapcore/snapd/bootloader/grubenv" "github.com/snapcore/snapd/bootloader/ubootenv" "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/gadget" "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/secboot" + "github.com/snapcore/snapd/seed" "github.com/snapcore/snapd/snap" "github.com/snapcore/snapd/snap/snapfile" "github.com/snapcore/snapd/snap/snaptest" "github.com/snapcore/snapd/testutil" + "github.com/snapcore/snapd/timings" ) type makeBootableSuite struct { @@ -74,31 +80,15 @@ } func (s *makeBootableSuite) TestMakeBootable(c *C) { - dirs.SetRootDir("") - - headers := map[string]interface{}{ - "type": "model", - "authority-id": "my-brand", - "series": "16", - "brand-id": "my-brand", - "model": "my-model", - "display-name": "My Model", - "architecture": "amd64", - "base": "core18", - "gadget": "pc=18", - "kernel": "pc-kernel=18", - "timestamp": "2018-01-01T08:00:00+00:00", - } - model := assertstest.FakeAssertion(headers).(*asserts.Model) + bootloader.Force(nil) + model := boottest.MakeMockModel() grubCfg := []byte("#grub cfg") unpackedGadgetDir := c.MkDir() err := ioutil.WriteFile(filepath.Join(unpackedGadgetDir, "grub.conf"), grubCfg, 0644) c.Assert(err, IsNil) - rootdir := c.MkDir() - - seedSnapsDirs := filepath.Join(rootdir, "/var/lib/snapd/seed", "snaps") + seedSnapsDirs := filepath.Join(s.rootdir, "/var/lib/snapd/seed", "snaps") err = os.MkdirAll(seedSnapsDirs, 0755) c.Assert(err, IsNil) @@ -125,34 +115,31 @@ UnpackedGadgetDir: unpackedGadgetDir, } - err = boot.MakeBootable(model, rootdir, bootWith) + err = boot.MakeBootable(model, s.rootdir, bootWith, nil) c.Assert(err, IsNil) // check the bootloader config - m, err := s.bootloader.GetBootVars("snap_kernel", "snap_core", "snap_menuentry") - c.Assert(err, IsNil) - c.Check(m["snap_kernel"], Equals, "pc-kernel_5.snap") - c.Check(m["snap_core"], Equals, "core18_3.snap") - c.Check(m["snap_menuentry"], Equals, "My Model") - - // kernel was extracted as needed - c.Check(s.bootloader.ExtractKernelAssetsCalls, DeepEquals, []snap.PlaceInfo{kernelInfo}) + seedGenv := grubenv.NewEnv(filepath.Join(s.rootdir, "boot/grub/grubenv")) + c.Assert(seedGenv.Load(), IsNil) + c.Check(seedGenv.Get("snap_kernel"), Equals, "pc-kernel_5.snap") + c.Check(seedGenv.Get("snap_core"), Equals, "core18_3.snap") + c.Check(seedGenv.Get("snap_menuentry"), Equals, "My Model") // check symlinks from snap blob dir - kernelBlob := filepath.Join(dirs.SnapBlobDirUnder(rootdir), kernelInfo.Filename()) - dst, err := os.Readlink(filepath.Join(dirs.SnapBlobDirUnder(rootdir), kernelInfo.Filename())) + kernelBlob := filepath.Join(dirs.SnapBlobDirUnder(s.rootdir), kernelInfo.Filename()) + dst, err := os.Readlink(filepath.Join(dirs.SnapBlobDirUnder(s.rootdir), kernelInfo.Filename())) c.Assert(err, IsNil) c.Check(dst, Equals, "../seed/snaps/pc-kernel_5.snap") c.Check(kernelBlob, testutil.FilePresent) - baseBlob := filepath.Join(dirs.SnapBlobDirUnder(rootdir), baseInfo.Filename()) - dst, err = os.Readlink(filepath.Join(dirs.SnapBlobDirUnder(rootdir), baseInfo.Filename())) + baseBlob := filepath.Join(dirs.SnapBlobDirUnder(s.rootdir), baseInfo.Filename()) + dst, err = os.Readlink(filepath.Join(dirs.SnapBlobDirUnder(s.rootdir), baseInfo.Filename())) c.Assert(err, IsNil) c.Check(dst, Equals, "../seed/snaps/core18_3.snap") c.Check(baseBlob, testutil.FilePresent) // check that the bootloader (grub here) configuration was copied - c.Check(filepath.Join(rootdir, "boot", "grub/grub.cfg"), testutil.FileEquals, grubCfg) + c.Check(filepath.Join(s.rootdir, "boot", "grub/grub.cfg"), testutil.FileEquals, grubCfg) } type makeBootable20Suite struct { @@ -184,50 +171,24 @@ s.forceBootloader(s.bootloader) } -func makeMockUC20Model() *asserts.Model { - headers := map[string]interface{}{ - "type": "model", - "authority-id": "my-brand", - "series": "16", - "brand-id": "my-brand", - "model": "my-model-uc20", - "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-linux", - "id": "pclinuxdidididididididididididid", - "type": "kernel", - }, - map[string]interface{}{ - "name": "pc", - "id": "pcididididididididididididididid", - "type": "gadget", - }, - }, - } - return assertstest.FakeAssertion(headers).(*asserts.Model) -} - func (s *makeBootable20Suite) TestMakeBootable20(c *C) { - dirs.SetRootDir("") - - model := makeMockUC20Model() + bootloader.Force(nil) + model := boottest.MakeMockUC20Model() unpackedGadgetDir := c.MkDir() grubRecoveryCfg := []byte("#grub-recovery cfg") + grubRecoveryCfgAsset := []byte("#grub-recovery cfg from assets") err := ioutil.WriteFile(filepath.Join(unpackedGadgetDir, "grub-recovery.conf"), grubRecoveryCfg, 0644) + restore := assets.MockInternal("grub-recovery.cfg", grubRecoveryCfgAsset) + defer restore() + c.Assert(err, IsNil) grubCfg := []byte("#grub cfg") err = ioutil.WriteFile(filepath.Join(unpackedGadgetDir, "grub.conf"), grubCfg, 0644) c.Assert(err, IsNil) - rootdir := c.MkDir() // on uc20 the seed layout if different - seedSnapsDirs := filepath.Join(rootdir, "/snaps") + seedSnapsDirs := filepath.Join(s.rootdir, "/snaps") err = os.MkdirAll(seedSnapsDirs, 0755) c.Assert(err, IsNil) @@ -261,34 +222,33 @@ Recovery: true, } - err = boot.MakeBootable(model, rootdir, bootWith) + err = boot.MakeBootable(model, s.rootdir, bootWith, nil) c.Assert(err, IsNil) // ensure only a single file got copied (the grub.cfg) - files, err := filepath.Glob(filepath.Join(rootdir, "EFI/ubuntu/*")) + files, err := filepath.Glob(filepath.Join(s.rootdir, "EFI/ubuntu/*")) c.Assert(err, IsNil) - c.Check(files, HasLen, 1) - // check that the recovery bootloader configuration was copied with + // grub.cfg and grubenv + c.Check(files, HasLen, 2) + // check that the recovery bootloader configuration was installed with // the correct content - c.Check(filepath.Join(rootdir, "EFI/ubuntu/grub.cfg"), testutil.FileEquals, grubRecoveryCfg) + c.Check(filepath.Join(s.rootdir, "EFI/ubuntu/grub.cfg"), testutil.FileEquals, grubRecoveryCfgAsset) // ensure no /boot was setup - c.Check(filepath.Join(rootdir, "boot"), testutil.FileAbsent) + c.Check(filepath.Join(s.rootdir, "boot"), testutil.FileAbsent) // ensure the correct recovery system configuration was set - c.Check(s.bootloader.RecoverySystemDir, Equals, recoverySystemDir) - c.Check(s.bootloader.RecoverySystemBootVars, DeepEquals, map[string]string{ - "snapd_recovery_kernel": "/snaps/pc-kernel_5.snap", - }) - c.Check(s.bootloader.BootVars, DeepEquals, map[string]string{ - "snapd_recovery_system": label, - }) + seedGenv := grubenv.NewEnv(filepath.Join(s.rootdir, "EFI/ubuntu/grubenv")) + c.Assert(seedGenv.Load(), IsNil) + c.Check(seedGenv.Get("snapd_recovery_system"), Equals, label) + + systemGenv := grubenv.NewEnv(filepath.Join(s.rootdir, recoverySystemDir, "grubenv")) + c.Assert(systemGenv.Load(), IsNil) + c.Check(systemGenv.Get("snapd_recovery_kernel"), Equals, "/snaps/pc-kernel_5.snap") } func (s *makeBootable20Suite) TestMakeBootable20UnsetRecoverySystemLabelError(c *C) { - dirs.SetRootDir("") - - model := makeMockUC20Model() + model := boottest.MakeMockUC20Model() unpackedGadgetDir := c.MkDir() grubRecoveryCfg := []byte("#grub-recovery cfg") @@ -298,8 +258,6 @@ err = ioutil.WriteFile(filepath.Join(unpackedGadgetDir, "grub.conf"), grubCfg, 0644) c.Assert(err, IsNil) - rootdir := c.MkDir() - label := "20191209" recoverySystemDir := filepath.Join("/systems", label) bootWith := &boot.BootableSet{ @@ -308,34 +266,28 @@ Recovery: true, } - err = boot.MakeBootable(model, rootdir, bootWith) + err = boot.MakeBootable(model, s.rootdir, bootWith, nil) c.Assert(err, ErrorMatches, "internal error: recovery system label unset") } func (s *makeBootable20Suite) TestMakeBootable20MultipleRecoverySystemsError(c *C) { - dirs.SetRootDir("") - - model := makeMockUC20Model() + model := boottest.MakeMockUC20Model() bootWith := &boot.BootableSet{Recovery: true} - rootdir := c.MkDir() - err := os.MkdirAll(filepath.Join(rootdir, "systems/20191204"), 0755) + err := os.MkdirAll(filepath.Join(s.rootdir, "systems/20191204"), 0755) c.Assert(err, IsNil) - err = os.MkdirAll(filepath.Join(rootdir, "systems/20191205"), 0755) + err = os.MkdirAll(filepath.Join(s.rootdir, "systems/20191205"), 0755) c.Assert(err, IsNil) - err = boot.MakeBootable(model, rootdir, bootWith) + err = boot.MakeBootable(model, s.rootdir, bootWith, nil) c.Assert(err, ErrorMatches, "cannot make multiple recovery systems bootable yet") } func (s *makeBootable20Suite) TestMakeBootable20RunMode(c *C) { - dirs.SetRootDir("") bootloader.Force(nil) - model := makeMockUC20Model() - rootdir := c.MkDir() - dirs.SetRootDir(rootdir) - seedSnapsDirs := filepath.Join(rootdir, "/snaps") + model := boottest.MakeMockUC20Model() + seedSnapsDirs := filepath.Join(s.rootdir, "/snaps") err := os.MkdirAll(seedSnapsDirs, 0755) c.Assert(err, IsNil) @@ -344,7 +296,19 @@ mockSeedGrubCfg := filepath.Join(mockSeedGrubDir, "grub.cfg") err = os.MkdirAll(filepath.Dir(mockSeedGrubCfg), 0755) c.Assert(err, IsNil) - err = ioutil.WriteFile(mockSeedGrubCfg, nil, 0644) + err = ioutil.WriteFile(mockSeedGrubCfg, []byte("# Snapd-Boot-Config-Edition: 1\n"), 0644) + c.Assert(err, IsNil) + + // setup recovery boot assets + err = os.MkdirAll(filepath.Join(boot.InitramfsUbuntuSeedDir, "EFI/boot"), 0755) + c.Assert(err, IsNil) + // SHA3-384: 39efae6545f16e39633fbfbef0d5e9fdd45a25d7df8764978ce4d81f255b038046a38d9855e42e5c7c4024e153fd2e37 + err = ioutil.WriteFile(filepath.Join(boot.InitramfsUbuntuSeedDir, "EFI/boot/bootx64.efi"), + []byte("recovery shim content"), 0644) + c.Assert(err, IsNil) + // SHA3-384: aa3c1a83e74bf6dd40dd64e5c5bd1971d75cdf55515b23b9eb379f66bf43d4661d22c4b8cf7d7a982d2013ab65c1c4c5 + err = ioutil.WriteFile(filepath.Join(boot.InitramfsUbuntuSeedDir, "EFI/boot/grubx64.efi"), + []byte("recovery grub content"), 0644) c.Assert(err, IsNil) // grub on ubuntu-boot @@ -355,6 +319,25 @@ err = ioutil.WriteFile(mockBootGrubCfg, nil, 0644) c.Assert(err, IsNil) + unpackedGadgetDir := c.MkDir() + grubRecoveryCfg := []byte("#grub-recovery cfg") + grubRecoveryCfgAsset := []byte("#grub-recovery cfg from assets") + err = ioutil.WriteFile(filepath.Join(unpackedGadgetDir, "grub-recovery.conf"), grubRecoveryCfg, 0644) + c.Assert(err, IsNil) + restore := assets.MockInternal("grub-recovery.cfg", grubRecoveryCfgAsset) + defer restore() + grubCfg := []byte("#grub cfg") + err = ioutil.WriteFile(filepath.Join(unpackedGadgetDir, "grub.conf"), grubCfg, 0644) + c.Assert(err, IsNil) + grubCfgAsset := []byte("# Snapd-Boot-Config-Edition: 1\n#grub cfg from assets") + restore = assets.MockInternal("grub.cfg", grubCfgAsset) + defer restore() + + err = ioutil.WriteFile(filepath.Join(unpackedGadgetDir, "bootx64.efi"), []byte("shim content"), 0644) + c.Assert(err, IsNil) + err = ioutil.WriteFile(filepath.Join(unpackedGadgetDir, "grubx64.efi"), []byte("grub content"), 0644) + c.Assert(err, IsNil) + // make the snaps symlinks so that we can ensure that makebootable follows // the symlinks and copies the files and not the symlinks baseFn, baseInfo := makeSnap(c, "core20", `name: core20 @@ -383,11 +366,115 @@ KernelPath: kernelInSeed, Kernel: kernelInfo, Recovery: false, + UnpackedGadgetDir: unpackedGadgetDir, } - err = boot.MakeBootable(model, rootdir, bootWith) + // set up observer state + useEncryption := true + obs, err := boot.TrustedAssetsInstallObserverForModel(model, unpackedGadgetDir, useEncryption) + c.Assert(obs, NotNil) + c.Assert(err, IsNil) + runBootStruct := &gadget.LaidOutStructure{ + VolumeStructure: &gadget.VolumeStructure{ + Role: gadget.SystemBoot, + }, + } + + // only grubx64.efi gets installed to system-boot + _, err = obs.Observe(gadget.ContentWrite, runBootStruct, boot.InitramfsUbuntuBootDir, "EFI/boot/grubx64.efi", + &gadget.ContentChange{After: filepath.Join(unpackedGadgetDir, "grubx64.efi")}) + c.Assert(err, IsNil) + + // observe recovery assets + err = obs.ObserveExistingTrustedRecoveryAssets(boot.InitramfsUbuntuSeedDir) + c.Assert(err, IsNil) + + // set encryption key + myKey := secboot.EncryptionKey{} + myKey2 := secboot.EncryptionKey{} + for i := range myKey { + myKey[i] = byte(i) + myKey2[i] = byte(128 + i) + } + obs.ChosenEncryptionKeys(myKey, myKey2) + + // set a mock recovery kernel + readSystemEssentialCalls := 0 + restore = boot.MockSeedReadSystemEssential(func(seedDir, label string, essentialTypes []snap.Type, tm timings.Measurer) (*asserts.Model, []*seed.Snap, error) { + readSystemEssentialCalls++ + kernelSnap := &seed.Snap{ + Path: "/var/lib/snapd/seed/snaps/pc-kernel_1.snap", + SideInfo: &snap.SideInfo{ + Revision: snap.Revision{N: 1}, + RealName: "pc-kernel", + }, + } + return model, []*seed.Snap{kernelSnap}, nil + }) + defer restore() + + // set mock key sealing + sealKeysCalls := 0 + restore = boot.MockSecbootSealKeys(func(keys []secboot.SealKeyRequest, params *secboot.SealKeysParams) error { + sealKeysCalls++ + switch sealKeysCalls { + case 1: + c.Check(keys, HasLen, 1) + c.Check(keys[0].Key, DeepEquals, myKey) + case 2: + c.Check(keys, HasLen, 2) + c.Check(keys[0].Key, DeepEquals, myKey) + c.Check(keys[1].Key, DeepEquals, myKey2) + default: + c.Errorf("unexpected additional call to secboot.SealKeys (call # %d)", sealKeysCalls) + } + c.Assert(params.ModelParams, HasLen, 1) + + shim := bootloader.NewBootFile("", filepath.Join(s.rootdir, + "var/lib/snapd/boot-assets/grub/bootx64.efi-39efae6545f16e39633fbfbef0d5e9fdd45a25d7df8764978ce4d81f255b038046a38d9855e42e5c7c4024e153fd2e37"), + bootloader.RoleRecovery) + grub := bootloader.NewBootFile("", filepath.Join(s.rootdir, + "var/lib/snapd/boot-assets/grub/grubx64.efi-aa3c1a83e74bf6dd40dd64e5c5bd1971d75cdf55515b23b9eb379f66bf43d4661d22c4b8cf7d7a982d2013ab65c1c4c5"), + bootloader.RoleRecovery) + runGrub := bootloader.NewBootFile("", filepath.Join(s.rootdir, + "var/lib/snapd/boot-assets/grub/grubx64.efi-5ee042c15e104b825d6bc15c41cdb026589f1ec57ed966dd3f29f961d4d6924efc54b187743fa3a583b62722882d405d"), + bootloader.RoleRunMode) + kernel := bootloader.NewBootFile("/var/lib/snapd/seed/snaps/pc-kernel_1.snap", "kernel.efi", bootloader.RoleRecovery) + runKernel := bootloader.NewBootFile(filepath.Join(s.rootdir, "var/lib/snapd/snaps/pc-kernel_5.snap"), "kernel.efi", bootloader.RoleRunMode) + + switch sealKeysCalls { + case 1: + c.Assert(params.ModelParams[0].EFILoadChains, DeepEquals, []*secboot.LoadChain{ + secboot.NewLoadChain(shim, secboot.NewLoadChain(grub, secboot.NewLoadChain(kernel))), + secboot.NewLoadChain(shim, secboot.NewLoadChain(grub, secboot.NewLoadChain(runGrub, secboot.NewLoadChain(runKernel)))), + }) + c.Assert(params.ModelParams[0].KernelCmdlines, DeepEquals, []string{ + "snapd_recovery_mode=recover snapd_recovery_system=20191216 console=ttyS0 console=tty1 panic=-1", + "snapd_recovery_mode=run console=ttyS0 console=tty1 panic=-1", + }) + case 2: + c.Assert(params.ModelParams[0].EFILoadChains, DeepEquals, []*secboot.LoadChain{ + secboot.NewLoadChain(shim, secboot.NewLoadChain(grub, secboot.NewLoadChain(kernel))), + }) + c.Assert(params.ModelParams[0].KernelCmdlines, DeepEquals, []string{ + "snapd_recovery_mode=recover snapd_recovery_system=20191216 console=ttyS0 console=tty1 panic=-1", + }) + default: + c.Errorf("unexpected additional call to secboot.SealKeys (call # %d)", sealKeysCalls) + } + + c.Assert(params.ModelParams[0].Model.DisplayName(), Equals, "My Model") + + return nil + }) + defer restore() + + err = boot.MakeBootable(model, s.rootdir, bootWith, obs) c.Assert(err, IsNil) + // 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 core20Snap := filepath.Join(dirs.SnapBlobDirUnder(boot.InstallHostWritableDir), "core20_3.snap") pcKernelSnap := filepath.Join(dirs.SnapBlobDirUnder(boot.InstallHostWritableDir), "pc-kernel_5.snap") @@ -419,29 +506,313 @@ c.Check(extractedKernelSymlink, testutil.FilePresent) // ensure modeenv looks correct - ubuntuDataModeEnvPath := filepath.Join(rootdir, "/run/mnt/ubuntu-data/system-data/var/lib/snapd/modeenv") + ubuntuDataModeEnvPath := filepath.Join(s.rootdir, "/run/mnt/ubuntu-data/system-data/var/lib/snapd/modeenv") c.Check(ubuntuDataModeEnvPath, testutil.FileEquals, `mode=run recovery_system=20191216 +current_recovery_systems=20191216 base=core20_3.snap current_kernels=pc-kernel_5.snap model=my-brand/my-model-uc20 grade=dangerous +current_trusted_boot_assets={"grubx64.efi":["5ee042c15e104b825d6bc15c41cdb026589f1ec57ed966dd3f29f961d4d6924efc54b187743fa3a583b62722882d405d"]} +current_trusted_recovery_boot_assets={"bootx64.efi":["39efae6545f16e39633fbfbef0d5e9fdd45a25d7df8764978ce4d81f255b038046a38d9855e42e5c7c4024e153fd2e37"],"grubx64.efi":["aa3c1a83e74bf6dd40dd64e5c5bd1971d75cdf55515b23b9eb379f66bf43d4661d22c4b8cf7d7a982d2013ab65c1c4c5"]} `) + copiedGrubBin := filepath.Join( + dirs.SnapBootAssetsDirUnder(boot.InstallHostWritableDir), + "grub", + "grubx64.efi-5ee042c15e104b825d6bc15c41cdb026589f1ec57ed966dd3f29f961d4d6924efc54b187743fa3a583b62722882d405d", + ) + copiedRecoveryGrubBin := filepath.Join( + dirs.SnapBootAssetsDirUnder(boot.InstallHostWritableDir), + "grub", + "grubx64.efi-aa3c1a83e74bf6dd40dd64e5c5bd1971d75cdf55515b23b9eb379f66bf43d4661d22c4b8cf7d7a982d2013ab65c1c4c5", + ) + copiedRecoveryShimBin := filepath.Join( + dirs.SnapBootAssetsDirUnder(boot.InstallHostWritableDir), + "grub", + "bootx64.efi-39efae6545f16e39633fbfbef0d5e9fdd45a25d7df8764978ce4d81f255b038046a38d9855e42e5c7c4024e153fd2e37", + ) + + // only one file in the cache under new root + checkContentGlob(c, filepath.Join(dirs.SnapBootAssetsDirUnder(boot.InstallHostWritableDir), "grub", "*"), []string{ + copiedRecoveryShimBin, + copiedGrubBin, + copiedRecoveryGrubBin, + }) + // with the right content + c.Check(copiedGrubBin, testutil.FileEquals, "grub content") + c.Check(copiedRecoveryGrubBin, testutil.FileEquals, "recovery grub content") + c.Check(copiedRecoveryShimBin, testutil.FileEquals, "recovery shim content") + + // make sure SealKey was called for the run object and the fallback object + c.Check(sealKeysCalls, Equals, 2) + + // make sure the marker file for sealed key was created + c.Check(filepath.Join(dirs.SnapFDEDirUnder(boot.InstallHostWritableDir), "sealed-keys"), testutil.FilePresent) + + // make sure we wrote the boot chains data file + c.Check(filepath.Join(dirs.SnapFDEDirUnder(boot.InstallHostWritableDir), "boot-chains"), testutil.FilePresent) } -func (s *makeBootable20UbootSuite) TestUbootMakeBootable20TraditionalUbootenvFails(c *C) { - dirs.SetRootDir("") +func (s *makeBootable20Suite) TestMakeBootable20RunModeInstallBootConfigErr(c *C) { + bootloader.Force(nil) + + model := boottest.MakeMockUC20Model() + seedSnapsDirs := filepath.Join(s.rootdir, "/snaps") + err := os.MkdirAll(seedSnapsDirs, 0755) + c.Assert(err, IsNil) + + // grub on ubuntu-seed + mockSeedGrubDir := filepath.Join(boot.InitramfsUbuntuSeedDir, "EFI", "ubuntu") + err = os.MkdirAll(mockSeedGrubDir, 0755) + c.Assert(err, IsNil) + // no recovery grub.cfg so that test fails if it ever reaches that point + + // grub on ubuntu-boot + mockBootGrubDir := filepath.Join(boot.InitramfsUbuntuBootDir, "EFI", "ubuntu") + mockBootGrubCfg := filepath.Join(mockBootGrubDir, "grub.cfg") + err = os.MkdirAll(filepath.Dir(mockBootGrubCfg), 0755) + c.Assert(err, IsNil) + err = ioutil.WriteFile(mockBootGrubCfg, nil, 0644) + c.Assert(err, IsNil) + + unpackedGadgetDir := c.MkDir() + + // make the snaps symlinks so that we can ensure that makebootable follows + // the symlinks and copies the files and not the symlinks + baseFn, baseInfo := makeSnap(c, "core20", `name: core20 +type: base +version: 5.0 +`, snap.R(3)) + baseInSeed := filepath.Join(seedSnapsDirs, baseInfo.Filename()) + err = os.Symlink(baseFn, baseInSeed) + c.Assert(err, IsNil) + kernelFn, kernelInfo := makeSnapWithFiles(c, "pc-kernel", `name: pc-kernel +type: kernel +version: 5.0 +`, snap.R(5), + [][]string{ + {"kernel.efi", "I'm a kernel.efi"}, + }, + ) + kernelInSeed := filepath.Join(seedSnapsDirs, kernelInfo.Filename()) + err = os.Symlink(kernelFn, kernelInSeed) + c.Assert(err, IsNil) + + bootWith := &boot.BootableSet{ + RecoverySystemDir: "20191216", + BasePath: baseInSeed, + Base: baseInfo, + KernelPath: kernelInSeed, + Kernel: kernelInfo, + Recovery: false, + UnpackedGadgetDir: unpackedGadgetDir, + } + + // no grub marker in gadget directory raises an error + err = boot.MakeBootable(model, s.rootdir, bootWith, nil) + c.Assert(err, ErrorMatches, "internal error: cannot identify run system bootloader: cannot determine bootloader") + + // set up grub.cfg in gadget + grubCfg := []byte("#grub cfg") + err = ioutil.WriteFile(filepath.Join(unpackedGadgetDir, "grub.conf"), grubCfg, 0644) + c.Assert(err, IsNil) + + // no write access to destination directory + restore := assets.MockInternal("grub.cfg", nil) + defer restore() + err = boot.MakeBootable(model, s.rootdir, bootWith, nil) + c.Assert(err, ErrorMatches, `cannot install managed bootloader assets: internal error: no boot asset for "grub.cfg"`) +} + +func (s *makeBootable20Suite) TestMakeBootable20RunModeSealKeyErr(c *C) { + bootloader.Force(nil) + + model := boottest.MakeMockUC20Model() + seedSnapsDirs := filepath.Join(s.rootdir, "/snaps") + err := os.MkdirAll(seedSnapsDirs, 0755) + c.Assert(err, IsNil) + + // grub on ubuntu-seed + mockSeedGrubDir := filepath.Join(boot.InitramfsUbuntuSeedDir, "EFI", "ubuntu") + mockSeedGrubCfg := filepath.Join(mockSeedGrubDir, "grub.cfg") + err = os.MkdirAll(filepath.Dir(mockSeedGrubCfg), 0755) + c.Assert(err, IsNil) + err = ioutil.WriteFile(mockSeedGrubCfg, []byte("# Snapd-Boot-Config-Edition: 1\n"), 0644) + c.Assert(err, IsNil) + + // setup recovery boot assets + err = os.MkdirAll(filepath.Join(boot.InitramfsUbuntuSeedDir, "EFI/boot"), 0755) + c.Assert(err, IsNil) + // SHA3-384: 39efae6545f16e39633fbfbef0d5e9fdd45a25d7df8764978ce4d81f255b038046a38d9855e42e5c7c4024e153fd2e37 + err = ioutil.WriteFile(filepath.Join(boot.InitramfsUbuntuSeedDir, "EFI/boot/bootx64.efi"), + []byte("recovery shim content"), 0644) + c.Assert(err, IsNil) + // SHA3-384: aa3c1a83e74bf6dd40dd64e5c5bd1971d75cdf55515b23b9eb379f66bf43d4661d22c4b8cf7d7a982d2013ab65c1c4c5 + err = ioutil.WriteFile(filepath.Join(boot.InitramfsUbuntuSeedDir, "EFI/boot/grubx64.efi"), + []byte("recovery grub content"), 0644) + c.Assert(err, IsNil) + + // grub on ubuntu-boot + mockBootGrubDir := filepath.Join(boot.InitramfsUbuntuBootDir, "EFI", "ubuntu") + mockBootGrubCfg := filepath.Join(mockBootGrubDir, "grub.cfg") + err = os.MkdirAll(filepath.Dir(mockBootGrubCfg), 0755) + c.Assert(err, IsNil) + err = ioutil.WriteFile(mockBootGrubCfg, nil, 0644) + c.Assert(err, IsNil) + + unpackedGadgetDir := c.MkDir() + grubRecoveryCfg := []byte("#grub-recovery cfg") + grubRecoveryCfgAsset := []byte("#grub-recovery cfg from assets") + err = ioutil.WriteFile(filepath.Join(unpackedGadgetDir, "grub-recovery.conf"), grubRecoveryCfg, 0644) + c.Assert(err, IsNil) + restore := assets.MockInternal("grub-recovery.cfg", grubRecoveryCfgAsset) + defer restore() + grubCfg := []byte("#grub cfg") + err = ioutil.WriteFile(filepath.Join(unpackedGadgetDir, "grub.conf"), grubCfg, 0644) + c.Assert(err, IsNil) + grubCfgAsset := []byte("# Snapd-Boot-Config-Edition: 1\n#grub cfg from assets") + restore = assets.MockInternal("grub.cfg", grubCfgAsset) + defer restore() + + err = ioutil.WriteFile(filepath.Join(unpackedGadgetDir, "bootx64.efi"), []byte("shim content"), 0644) + c.Assert(err, IsNil) + err = ioutil.WriteFile(filepath.Join(unpackedGadgetDir, "grubx64.efi"), []byte("grub content"), 0644) + c.Assert(err, IsNil) - model := makeMockUC20Model() + // make the snaps symlinks so that we can ensure that makebootable follows + // the symlinks and copies the files and not the symlinks + baseFn, baseInfo := makeSnap(c, "core20", `name: core20 +type: base +version: 5.0 +`, snap.R(3)) + baseInSeed := filepath.Join(seedSnapsDirs, baseInfo.Filename()) + err = os.Symlink(baseFn, baseInSeed) + c.Assert(err, IsNil) + kernelFn, kernelInfo := makeSnapWithFiles(c, "pc-kernel", `name: pc-kernel +type: kernel +version: 5.0 +`, snap.R(5), + [][]string{ + {"kernel.efi", "I'm a kernel.efi"}, + }, + ) + kernelInSeed := filepath.Join(seedSnapsDirs, kernelInfo.Filename()) + err = os.Symlink(kernelFn, kernelInSeed) + c.Assert(err, IsNil) + + bootWith := &boot.BootableSet{ + RecoverySystemDir: "20191216", + BasePath: baseInSeed, + Base: baseInfo, + KernelPath: kernelInSeed, + Kernel: kernelInfo, + Recovery: false, + UnpackedGadgetDir: unpackedGadgetDir, + } + + // set up observer state + useEncryption := true + obs, err := boot.TrustedAssetsInstallObserverForModel(model, unpackedGadgetDir, useEncryption) + c.Assert(obs, NotNil) + c.Assert(err, IsNil) + runBootStruct := &gadget.LaidOutStructure{ + VolumeStructure: &gadget.VolumeStructure{ + Role: gadget.SystemBoot, + }, + } + + // only grubx64.efi gets installed to system-boot + _, err = obs.Observe(gadget.ContentWrite, runBootStruct, boot.InitramfsUbuntuBootDir, "EFI/boot/grubx64.efi", + &gadget.ContentChange{After: filepath.Join(unpackedGadgetDir, "grubx64.efi")}) + c.Assert(err, IsNil) + + // observe recovery assets + err = obs.ObserveExistingTrustedRecoveryAssets(boot.InitramfsUbuntuSeedDir) + c.Assert(err, IsNil) + + // set encryption key + myKey := secboot.EncryptionKey{} + myKey2 := secboot.EncryptionKey{} + for i := range myKey { + myKey[i] = byte(i) + myKey2[i] = byte(128 + i) + } + obs.ChosenEncryptionKeys(myKey, myKey2) + + // set a mock recovery kernel + readSystemEssentialCalls := 0 + restore = boot.MockSeedReadSystemEssential(func(seedDir, label string, essentialTypes []snap.Type, tm timings.Measurer) (*asserts.Model, []*seed.Snap, error) { + readSystemEssentialCalls++ + kernelSnap := &seed.Snap{ + Path: "/var/lib/snapd/seed/snaps/pc-kernel_1.snap", + SideInfo: &snap.SideInfo{ + Revision: snap.Revision{N: 0}, + RealName: "pc-kernel", + }, + } + return model, []*seed.Snap{kernelSnap}, nil + }) + defer restore() + + // set mock key sealing + sealKeysCalls := 0 + restore = boot.MockSecbootSealKeys(func(keys []secboot.SealKeyRequest, params *secboot.SealKeysParams) error { + sealKeysCalls++ + switch sealKeysCalls { + case 1: + c.Check(keys, HasLen, 1) + c.Check(keys[0].Key, DeepEquals, myKey) + case 2: + c.Check(keys, HasLen, 2) + c.Check(keys[0].Key, DeepEquals, myKey) + c.Check(keys[1].Key, DeepEquals, myKey2) + default: + c.Errorf("unexpected additional call to secboot.SealKeys (call # %d)", sealKeysCalls) + } + c.Assert(params.ModelParams, HasLen, 1) + + shim := bootloader.NewBootFile("", filepath.Join(s.rootdir, + "var/lib/snapd/boot-assets/grub/bootx64.efi-39efae6545f16e39633fbfbef0d5e9fdd45a25d7df8764978ce4d81f255b038046a38d9855e42e5c7c4024e153fd2e37"), + bootloader.RoleRecovery) + grub := bootloader.NewBootFile("", filepath.Join(s.rootdir, + "var/lib/snapd/boot-assets/grub/grubx64.efi-aa3c1a83e74bf6dd40dd64e5c5bd1971d75cdf55515b23b9eb379f66bf43d4661d22c4b8cf7d7a982d2013ab65c1c4c5"), + bootloader.RoleRecovery) + runGrub := bootloader.NewBootFile("", filepath.Join(s.rootdir, + "var/lib/snapd/boot-assets/grub/grubx64.efi-5ee042c15e104b825d6bc15c41cdb026589f1ec57ed966dd3f29f961d4d6924efc54b187743fa3a583b62722882d405d"), + bootloader.RoleRunMode) + kernel := bootloader.NewBootFile("/var/lib/snapd/seed/snaps/pc-kernel_1.snap", "kernel.efi", bootloader.RoleRecovery) + runKernel := bootloader.NewBootFile(filepath.Join(s.rootdir, "var/lib/snapd/snaps/pc-kernel_5.snap"), "kernel.efi", bootloader.RoleRunMode) + + c.Assert(params.ModelParams[0].EFILoadChains, DeepEquals, []*secboot.LoadChain{ + secboot.NewLoadChain(shim, secboot.NewLoadChain(grub, secboot.NewLoadChain(kernel))), + secboot.NewLoadChain(shim, secboot.NewLoadChain(grub, secboot.NewLoadChain(runGrub, secboot.NewLoadChain(runKernel)))), + }) + c.Assert(params.ModelParams[0].KernelCmdlines, DeepEquals, []string{ + "snapd_recovery_mode=recover snapd_recovery_system=20191216 console=ttyS0 console=tty1 panic=-1", + "snapd_recovery_mode=run console=ttyS0 console=tty1 panic=-1", + }) + c.Assert(params.ModelParams[0].Model.DisplayName(), Equals, "My Model") + + return fmt.Errorf("seal error") + }) + defer restore() + + err = boot.MakeBootable(model, s.rootdir, bootWith, obs) + c.Assert(err, ErrorMatches, "cannot seal the encryption keys: seal error") +} + +func (s *makeBootable20UbootSuite) TestUbootMakeBootable20TraditionalUbootenvFails(c *C) { + bootloader.Force(nil) + model := boottest.MakeMockUC20Model() unpackedGadgetDir := c.MkDir() ubootEnv := []byte("#uboot env") err := ioutil.WriteFile(filepath.Join(unpackedGadgetDir, "uboot.conf"), ubootEnv, 0644) c.Assert(err, IsNil) - rootdir := c.MkDir() // on uc20 the seed layout if different - seedSnapsDirs := filepath.Join(rootdir, "/snaps") + seedSnapsDirs := filepath.Join(s.rootdir, "/snaps") err = os.MkdirAll(seedSnapsDirs, 0755) c.Assert(err, IsNil) @@ -479,23 +850,20 @@ } // TODO:UC20: enable this use case - err = boot.MakeBootable(model, rootdir, bootWith) - c.Assert(err, ErrorMatches, fmt.Sprintf("cannot find boot config in %q", unpackedGadgetDir)) + err = boot.MakeBootable(model, s.rootdir, bootWith, nil) + c.Assert(err, ErrorMatches, "non-empty uboot.env not supported on UC20 yet") } func (s *makeBootable20UbootSuite) TestUbootMakeBootable20BootScr(c *C) { - dirs.SetRootDir("") - - model := makeMockUC20Model() + model := boottest.MakeMockUC20Model() unpackedGadgetDir := c.MkDir() // the uboot.conf must be empty for this to work/do the right thing err := ioutil.WriteFile(filepath.Join(unpackedGadgetDir, "uboot.conf"), nil, 0644) c.Assert(err, IsNil) - rootdir := c.MkDir() // on uc20 the seed layout if different - seedSnapsDirs := filepath.Join(rootdir, "/snaps") + seedSnapsDirs := filepath.Join(s.rootdir, "/snaps") err = os.MkdirAll(seedSnapsDirs, 0755) c.Assert(err, IsNil) @@ -532,16 +900,17 @@ Recovery: true, } - err = boot.MakeBootable(model, rootdir, bootWith) + err = boot.MakeBootable(model, s.rootdir, bootWith, nil) c.Assert(err, IsNil) // since uboot.conf was absent, we won't have installed the uboot.env, as // it is expected that the gadget assets would have installed boot.scr // instead - c.Check(filepath.Join(rootdir, "uboot.env"), testutil.FileAbsent) + c.Check(filepath.Join(s.rootdir, "uboot.env"), testutil.FileAbsent) c.Check(s.bootloader.BootVars, DeepEquals, map[string]string{ "snapd_recovery_system": label, + "snapd_recovery_mode": "install", }) // ensure the correct recovery system configuration was set @@ -555,14 +924,11 @@ ) } -func (s *makeBootable20UbootSuite) TestUbootMakeBootable20RunModeBootScr(c *C) { - dirs.SetRootDir("") +func (s *makeBootable20UbootSuite) TestUbootMakeBootable20RunModeBootSel(c *C) { bootloader.Force(nil) - model := makeMockUC20Model() - rootdir := c.MkDir() - dirs.SetRootDir(rootdir) - seedSnapsDirs := filepath.Join(rootdir, "/snaps") + model := boottest.MakeMockUC20Model() + seedSnapsDirs := filepath.Join(s.rootdir, "/snaps") err := os.MkdirAll(seedSnapsDirs, 0755) c.Assert(err, IsNil) @@ -574,7 +940,7 @@ c.Assert(err, IsNil) c.Assert(env.Save(), IsNil) - // uboot on ubuntu-boot + // uboot on ubuntu-boot (as if it was installed when creating the partition) mockBootUbootBootSel := filepath.Join(boot.InitramfsUbuntuBootDir, "uboot/ubuntu/boot.sel") err = os.MkdirAll(filepath.Dir(mockBootUbootBootSel), 0755) c.Assert(err, IsNil) @@ -582,6 +948,9 @@ c.Assert(err, IsNil) c.Assert(env.Save(), IsNil) + unpackedGadgetDir := c.MkDir() + c.Assert(ioutil.WriteFile(filepath.Join(unpackedGadgetDir, "uboot.conf"), nil, 0644), IsNil) + baseFn, baseInfo := makeSnap(c, "core20", `name: core20 type: base version: 5.0 @@ -610,9 +979,9 @@ KernelPath: kernelInSeed, Kernel: kernelInfo, Recovery: false, + UnpackedGadgetDir: unpackedGadgetDir, } - - err = boot.MakeBootable(model, rootdir, bootWith) + err = boot.MakeBootable(model, s.rootdir, bootWith, nil) c.Assert(err, IsNil) // ensure base/kernel got copied to /var/lib/snapd/snaps @@ -641,9 +1010,10 @@ } // ensure modeenv looks correct - ubuntuDataModeEnvPath := filepath.Join(rootdir, "/run/mnt/ubuntu-data/system-data/var/lib/snapd/modeenv") + ubuntuDataModeEnvPath := filepath.Join(s.rootdir, "/run/mnt/ubuntu-data/system-data/var/lib/snapd/modeenv") c.Check(ubuntuDataModeEnvPath, testutil.FileEquals, `mode=run recovery_system=20191216 +current_recovery_systems=20191216 base=core20_3.snap current_kernels=arm-kernel_5.snap model=my-brand/my-model-uc20 diff -Nru snapd-2.45.1+20.04.2/boot/modeenv.go snapd-2.48.3+20.04/boot/modeenv.go --- snapd-2.45.1+20.04.2/boot/modeenv.go 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/boot/modeenv.go 2021-02-02 08:21:12.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2019 Canonical Ltd + * Copyright (C) 2019-2020 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as @@ -21,9 +21,13 @@ import ( "bytes" + "encoding/json" "fmt" + "io" "os" "path/filepath" + "reflect" + "sort" "strings" "github.com/mvo5/goconfigparser" @@ -32,18 +36,32 @@ "github.com/snapcore/snapd/osutil" ) +type bootAssetsMap map[string][]string + // Modeenv is a file on UC20 that provides additional information // about the current mode (run,recover,install) type Modeenv struct { - Mode string - RecoverySystem string - Base string - TryBase string - BaseStatus string - CurrentKernels []string - Model string - BrandID string - Grade string + Mode string `key:"mode"` + RecoverySystem string `key:"recovery_system"` + CurrentRecoverySystems []string `key:"current_recovery_systems"` + Base string `key:"base"` + TryBase string `key:"try_base"` + BaseStatus string `key:"base_status"` + CurrentKernels []string `key:"current_kernels"` + Model string `key:"model"` + BrandID string `key:"model,secondary"` + Grade string `key:"grade"` + // CurrentTrustedBootAssets is a map of a run bootloader's asset names to + // a list of hashes of the asset contents. Typically the first entry in + // the list is a hash of an asset the system currently boots with (or is + // expected to have booted with). The second entry, if present, is the + // hash of an entry added when an update of the asset was being applied + // and will become the sole entry after a successful boot. + CurrentTrustedBootAssets bootAssetsMap `key:"current_trusted_boot_assets"` + // CurrentTrustedRecoveryBootAssetsMap is a map of a recovery bootloader's + // asset names to a list of hashes of the asset contents. Used similarly + // to CurrentTrustedBootAssets. + CurrentTrustedRecoveryBootAssets bootAssetsMap `key:"current_trusted_recovery_boot_assets"` // read is set to true when a modenv was read successfully read bool @@ -51,6 +69,43 @@ // originRootdir is set to the root whence the modeenv was // read from, and where it will be written back to originRootdir string + + // extrakeys is all the keys in the modeenv we read from the file but don't + // understand, we keep track of this so that if we read a new modeenv with + // extra keys and need to rewrite it, we will write those new keys as well + extrakeys map[string]string +} + +var modeenvKnownKeys = make(map[string]bool) + +func init() { + st := reflect.TypeOf(Modeenv{}) + num := st.NumField() + for i := 0; i < num; i++ { + f := st.Field(i) + if f.PkgPath != "" { + // unexported + continue + } + key := f.Tag.Get("key") + if key == "" { + panic(fmt.Sprintf("modeenv %s field has no key tag", f.Name)) + } + const secondaryModifier = ",secondary" + if strings.HasSuffix(key, secondaryModifier) { + // secondary field in a group fields + // corresponding to one file key + key := key[:len(key)-len(secondaryModifier)] + if !modeenvKnownKeys[key] { + panic(fmt.Sprintf("modeenv %s field marked as secondary for not yet defined key %q", f.Name, key)) + } + continue + } + if modeenvKnownKeys[key] { + panic(fmt.Sprintf("modeenv key %q repeated on %s", key, f.Name)) + } + modeenvKnownKeys[key] = true + } } func modeenvFile(rootdir string) string { @@ -69,52 +124,87 @@ if err := cfg.ReadFile(modeenvPath); err != nil { return nil, err } + // TODO:UC20: should we check these errors and try to do something? - recoverySystem, _ := cfg.Get("", "recovery_system") - mode, _ := cfg.Get("", "mode") - base, _ := cfg.Get("", "base") - baseStatus, _ := cfg.Get("", "base_status") - tryBase, _ := cfg.Get("", "try_base") + m := Modeenv{ + read: true, + originRootdir: rootdir, + extrakeys: make(map[string]string), + } + unmarshalModeenvValueFromCfg(cfg, "recovery_system", &m.RecoverySystem) + unmarshalModeenvValueFromCfg(cfg, "current_recovery_systems", &m.CurrentRecoverySystems) + unmarshalModeenvValueFromCfg(cfg, "mode", &m.Mode) + if m.Mode == "" { + return nil, fmt.Errorf("internal error: mode is unset") + } + unmarshalModeenvValueFromCfg(cfg, "base", &m.Base) + unmarshalModeenvValueFromCfg(cfg, "base_status", &m.BaseStatus) + unmarshalModeenvValueFromCfg(cfg, "try_base", &m.TryBase) // current_kernels is a comma-delimited list in a string - kernelsString, _ := cfg.Get("", "current_kernels") - var kernels []string - if kernelsString != "" { - kernels = strings.Split(kernelsString, ",") - // drop empty strings - nonEmptyKernels := make([]string, 0, len(kernels)) - for _, kernel := range kernels { - if kernel != "" { - nonEmptyKernels = append(nonEmptyKernels, kernel) - } - } - kernels = nonEmptyKernels - } - brand := "" - model := "" - brandSlashModel, _ := cfg.Get("", "model") - if bsmSplit := strings.SplitN(brandSlashModel, "/", 2); len(bsmSplit) == 2 { - if bsmSplit[0] != "" && bsmSplit[1] != "" { - brand = bsmSplit[0] - model = bsmSplit[1] + unmarshalModeenvValueFromCfg(cfg, "current_kernels", &m.CurrentKernels) + var bm modeenvModel + unmarshalModeenvValueFromCfg(cfg, "model", &bm) + m.BrandID = bm.brandID + m.Model = bm.model + // expect the caller to validate the grade + unmarshalModeenvValueFromCfg(cfg, "grade", &m.Grade) + unmarshalModeenvValueFromCfg(cfg, "current_trusted_boot_assets", &m.CurrentTrustedBootAssets) + unmarshalModeenvValueFromCfg(cfg, "current_trusted_recovery_boot_assets", &m.CurrentTrustedRecoveryBootAssets) + + // save all the rest of the keys we don't understand + keys, err := cfg.Options("") + if err != nil { + return nil, err + } + for _, k := range keys { + if !modeenvKnownKeys[k] { + val, err := cfg.Get("", k) + if err != nil { + return nil, err + } + m.extrakeys[k] = val } } - // expect the caller to validate the grade - grade, _ := cfg.Get("", "grade") - return &Modeenv{ - Mode: mode, - RecoverySystem: recoverySystem, - Base: base, - TryBase: tryBase, - BaseStatus: baseStatus, - CurrentKernels: kernels, - BrandID: brand, - Grade: grade, - Model: model, - read: true, - originRootdir: rootdir, - }, nil + return &m, nil +} + +// deepEqual compares two modeenvs to ensure they are textually the same. It +// does not consider whether the modeenvs were read from disk or created purely +// in memory. It also does not sort or otherwise mutate any sub-objects, +// performing simple strict verification of sub-objects. +func (m *Modeenv) deepEqual(m2 *Modeenv) bool { + b, err := json.Marshal(m) + if err != nil { + return false + } + b2, err := json.Marshal(m2) + if err != nil { + return false + } + return bytes.Equal(b, b2) +} + +// Copy will make a deep copy of a Modeenv. +func (m *Modeenv) Copy() (*Modeenv, error) { + // to avoid hard-coding all fields here and manually copying everything, we + // take the easy way out and serialize to json then re-import into a + // empty Modeenv + b, err := json.Marshal(m) + if err != nil { + return nil, err + } + m2 := &Modeenv{} + err = json.Unmarshal(b, m2) + if err != nil { + return nil, err + } + + // manually copy the unexported fields as they won't be in the JSON + m2.read = m.read + m2.originRootdir = m.originRootdir + return m2, nil } // Write outputs the modeenv to the file where it was read, only valid on @@ -134,24 +224,16 @@ return err } buf := bytes.NewBuffer(nil) - if m.Mode != "" { - fmt.Fprintf(buf, "mode=%s\n", m.Mode) - } - if m.RecoverySystem != "" { - fmt.Fprintf(buf, "recovery_system=%s\n", m.RecoverySystem) - } - if m.Base != "" { - fmt.Fprintf(buf, "base=%s\n", m.Base) - } - if m.TryBase != "" { - fmt.Fprintf(buf, "try_base=%s\n", m.TryBase) - } - if m.BaseStatus != "" { - fmt.Fprintf(buf, "base_status=%s\n", m.BaseStatus) - } - if len(m.CurrentKernels) != 0 { - fmt.Fprintf(buf, "current_kernels=%s\n", strings.Join(m.CurrentKernels, ",")) + if m.Mode == "" { + return fmt.Errorf("internal error: mode is unset") } + marshalModeenvEntryTo(buf, "mode", m.Mode) + marshalModeenvEntryTo(buf, "recovery_system", m.RecoverySystem) + marshalModeenvEntryTo(buf, "current_recovery_systems", m.CurrentRecoverySystems) + marshalModeenvEntryTo(buf, "base", m.Base) + marshalModeenvEntryTo(buf, "try_base", m.TryBase) + marshalModeenvEntryTo(buf, "base_status", m.BaseStatus) + marshalModeenvEntryTo(buf, "current_kernels", strings.Join(m.CurrentKernels, ",")) if m.Model != "" || m.Grade != "" { if m.Model == "" { return fmt.Errorf("internal error: model is unset") @@ -159,10 +241,21 @@ if m.BrandID == "" { return fmt.Errorf("internal error: brand is unset") } - fmt.Fprintf(buf, "model=%s/%s\n", m.BrandID, m.Model) + marshalModeenvEntryTo(buf, "model", &modeenvModel{brandID: m.BrandID, model: m.Model}) } - if m.Grade != "" { - fmt.Fprintf(buf, "grade=%s\n", m.Grade) + marshalModeenvEntryTo(buf, "grade", m.Grade) + marshalModeenvEntryTo(buf, "current_trusted_boot_assets", m.CurrentTrustedBootAssets) + marshalModeenvEntryTo(buf, "current_trusted_recovery_boot_assets", m.CurrentTrustedRecoveryBootAssets) + + // write all the extra keys at the end + // sort them for test convenience + extraKeys := make([]string, 0, len(m.extrakeys)) + for k := range m.extrakeys { + extraKeys = append(extraKeys, k) + } + sort.Strings(extraKeys) + for _, k := range extraKeys { + marshalModeenvEntryTo(buf, k, m.extrakeys[k]) } if err := osutil.AtomicWriteFile(modeenvPath, buf.Bytes(), 0644, 0); err != nil { @@ -170,3 +263,140 @@ } return nil } + +type modeenvValueMarshaller interface { + MarshalModeenvValue() (string, error) +} + +type modeenvValueUnmarshaller interface { + UnmarshalModeenvValue(value string) error +} + +// marshalModeenvEntryTo marshals to out what as value for an entry +// with the given key. If what is empty this is a no-op. +func marshalModeenvEntryTo(out io.Writer, key string, what interface{}) error { + var asString string + switch v := what.(type) { + case string: + if v == "" { + return nil + } + asString = v + case []string: + if len(v) == 0 { + return nil + } + asString = asModeenvStringList(v) + default: + if vm, ok := what.(modeenvValueMarshaller); ok { + marshalled, err := vm.MarshalModeenvValue() + if err != nil { + return fmt.Errorf("cannot marshal value for key %q: %v", key, err) + } + asString = marshalled + } else if jm, ok := what.(json.Marshaler); ok { + marshalled, err := jm.MarshalJSON() + if err != nil { + return fmt.Errorf("cannot marshal value for key %q as JSON: %v", key, err) + } + asString = string(marshalled) + if asString == "null" { + // no need to keep nulls in the modeenv + return nil + } + } else { + return fmt.Errorf("internal error: cannot marshal unsupported type %T value %v for key %q", what, what, key) + } + } + _, err := fmt.Fprintf(out, "%s=%s\n", key, asString) + return err +} + +// unmarshalModeenvValueFromCfg unmarshals the value of the entry with +// th 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 { + return fmt.Errorf("internal error: cannot unmarshal to nil") + } + kv, _ := cfg.Get("", key) + + switch v := dest.(type) { + case *string: + *v = kv + case *[]string: + *v = splitModeenvStringList(kv) + default: + if vm, ok := v.(modeenvValueUnmarshaller); ok { + if err := vm.UnmarshalModeenvValue(kv); err != nil { + return fmt.Errorf("cannot unmarshal modeenv value %q to %T: %v", kv, dest, err) + } + return nil + } else if jm, ok := v.(json.Unmarshaler); ok { + if len(kv) == 0 { + // leave jm empty + return nil + } + if err := jm.UnmarshalJSON([]byte(kv)); err != nil { + return fmt.Errorf("cannot unmarshal modeenv value %q as JSON to %T: %v", kv, dest, err) + } + return nil + } + return fmt.Errorf("internal error: cannot unmarshal value %q for unsupported type %T", kv, dest) + } + return nil +} + +func splitModeenvStringList(v string) []string { + if v == "" { + return nil + } + split := strings.Split(v, ",") + // drop empty strings + nonEmpty := make([]string, 0, len(split)) + for _, one := range split { + if one != "" { + nonEmpty = append(nonEmpty, one) + } + } + if len(nonEmpty) == 0 { + return nil + } + return nonEmpty +} + +func asModeenvStringList(v []string) string { + return strings.Join(v, ",") +} + +type modeenvModel struct { + brandID, model string +} + +func (m *modeenvModel) MarshalModeenvValue() (string, error) { + return fmt.Sprintf("%s/%s", m.brandID, m.model), nil +} + +func (m *modeenvModel) UnmarshalModeenvValue(brandSlashModel string) error { + if bsmSplit := strings.SplitN(brandSlashModel, "/", 2); len(bsmSplit) == 2 { + if bsmSplit[0] != "" && bsmSplit[1] != "" { + m.brandID = bsmSplit[0] + m.model = bsmSplit[1] + } + } + return nil +} + +func (b bootAssetsMap) MarshalJSON() ([]byte, error) { + asMap := map[string][]string(b) + return json.Marshal(asMap) +} + +func (b *bootAssetsMap) UnmarshalJSON(data []byte) error { + var asMap map[string][]string + if err := json.Unmarshal(data, &asMap); err != nil { + return err + } + *b = bootAssetsMap(asMap) + return nil +} diff -Nru snapd-2.45.1+20.04.2/boot/modeenv_test.go snapd-2.48.3+20.04/boot/modeenv_test.go --- snapd-2.45.1+20.04.2/boot/modeenv_test.go 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/boot/modeenv_test.go 2021-02-02 08:21:12.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2019 Canonical Ltd + * Copyright (C) 2019-2020 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,10 +20,15 @@ package boot_test import ( + "bytes" + "encoding/json" + "fmt" "io/ioutil" "os" "path/filepath" + "strings" + "github.com/mvo5/goconfigparser" . "gopkg.in/check.v1" "github.com/snapcore/snapd/boot" @@ -46,6 +51,23 @@ s.mockModeenvPath = filepath.Join(s.tmpdir, dirs.SnapModeenvFile) } +func (s *modeenvSuite) TestKnownKnown(c *C) { + // double check keys as found with reflect + c.Check(boot.ModeenvKnownKeys, DeepEquals, map[string]bool{ + "mode": true, + "recovery_system": true, + "current_recovery_systems": true, + "base": true, + "try_base": true, + "base_status": true, + "current_kernels": true, + "model": true, + "grade": true, + "current_trusted_boot_assets": true, + "current_trusted_recovery_boot_assets": true, + }) +} + func (s *modeenvSuite) TestReadEmptyErrors(c *C) { modeenv, err := boot.ReadModeenv("/no/such/file") c.Assert(os.IsNotExist(err), Equals, true) @@ -68,11 +90,8 @@ s.makeMockModeenvFile(c, "") modeenv, err := boot.ReadModeenv(s.tmpdir) - c.Assert(err, IsNil) - c.Check(modeenv.Mode, Equals, "") - c.Check(modeenv.RecoverySystem, Equals, "") - // an empty modeenv still means the modeenv was set - c.Check(modeenv.WasRead(), Equals, true) + c.Assert(err, ErrorMatches, "internal error: mode is unset") + c.Assert(modeenv, IsNil) } func (s *modeenvSuite) TestReadMode(c *C) { @@ -85,6 +104,178 @@ c.Check(modeenv.Base, Equals, "") } +func (s *modeenvSuite) TestDeepEqualDiskVsMemoryInvariant(c *C) { + s.makeMockModeenvFile(c, `mode=recovery +recovery_system=20191126 +base=core20_123.snap +try_base=core20_124.snap +base_status=try +`) + + diskModeenv, err := boot.ReadModeenv(s.tmpdir) + c.Assert(err, IsNil) + inMemoryModeenv := &boot.Modeenv{ + Mode: "recovery", + RecoverySystem: "20191126", + Base: "core20_123.snap", + TryBase: "core20_124.snap", + BaseStatus: "try", + } + c.Assert(inMemoryModeenv.DeepEqual(diskModeenv), Equals, true) + c.Assert(diskModeenv.DeepEqual(inMemoryModeenv), Equals, true) +} + +func (s *modeenvSuite) TestCopyDeepEquals(c *C) { + s.makeMockModeenvFile(c, `mode=recovery +recovery_system=20191126 +base=core20_123.snap +try_base=core20_124.snap +base_status=try +current_trusted_boot_assets={"thing1":["hash1","hash2"],"thing2":["hash3"]} +`) + + diskModeenv, err := boot.ReadModeenv(s.tmpdir) + c.Assert(err, IsNil) + inMemoryModeenv := &boot.Modeenv{ + Mode: "recovery", + RecoverySystem: "20191126", + Base: "core20_123.snap", + TryBase: "core20_124.snap", + BaseStatus: "try", + CurrentTrustedBootAssets: boot.BootAssetsMap{ + "thing1": {"hash1", "hash2"}, + "thing2": {"hash3"}, + }, + } + + c.Assert(inMemoryModeenv.DeepEqual(diskModeenv), Equals, true) + c.Assert(diskModeenv.DeepEqual(inMemoryModeenv), Equals, true) + + diskModeenv2, err := diskModeenv.Copy() + c.Assert(err, IsNil) + c.Assert(diskModeenv.DeepEqual(diskModeenv2), Equals, true) + c.Assert(diskModeenv2.DeepEqual(diskModeenv), Equals, true) + c.Assert(inMemoryModeenv.DeepEqual(diskModeenv2), Equals, true) + c.Assert(diskModeenv2.DeepEqual(inMemoryModeenv), Equals, true) + + inMemoryModeenv2, err := inMemoryModeenv.Copy() + c.Assert(err, IsNil) + c.Assert(inMemoryModeenv.DeepEqual(inMemoryModeenv2), Equals, true) + c.Assert(inMemoryModeenv2.DeepEqual(inMemoryModeenv), Equals, true) + c.Assert(inMemoryModeenv2.DeepEqual(diskModeenv), Equals, true) + c.Assert(diskModeenv.DeepEqual(inMemoryModeenv2), Equals, true) +} + +func (s *modeenvSuite) TestCopyDiskWriteWorks(c *C) { + s.makeMockModeenvFile(c, `mode=recovery +recovery_system=20191126 +base=core20_123.snap +try_base=core20_124.snap +base_status=try +`) + + diskModeenv, err := boot.ReadModeenv(s.tmpdir) + c.Assert(err, IsNil) + dupDiskModeenv, err := diskModeenv.Copy() + c.Assert(err, IsNil) + + // move the original file out of the way + err = os.Rename(dirs.SnapModeenvFileUnder(s.tmpdir), dirs.SnapModeenvFileUnder(s.tmpdir)+".orig") + c.Assert(err, IsNil) + c.Assert(dirs.SnapModeenvFileUnder(s.tmpdir), testutil.FileAbsent) + + // write the duplicate, it should write to the same original location and it + // should be the same content + err = dupDiskModeenv.Write() + c.Assert(err, IsNil) + c.Assert(dirs.SnapModeenvFileUnder(s.tmpdir), testutil.FilePresent) + origBytes, err := ioutil.ReadFile(dirs.SnapModeenvFileUnder(s.tmpdir) + ".orig") + c.Assert(err, IsNil) + // the files should be the same + c.Assert(dirs.SnapModeenvFileUnder(s.tmpdir), testutil.FileEquals, string(origBytes)) +} + +func (s *modeenvSuite) TestCopyMemoryWriteFails(c *C) { + inMemoryModeenv := &boot.Modeenv{ + Mode: "recovery", + RecoverySystem: "20191126", + Base: "core20_123.snap", + TryBase: "core20_124.snap", + BaseStatus: "try", + } + dupInMemoryModeenv, err := inMemoryModeenv.Copy() + c.Assert(err, IsNil) + + // write the duplicate, it should fail + err = dupInMemoryModeenv.Write() + c.Assert(err, ErrorMatches, "internal error: must use WriteTo with modeenv not read from disk") +} + +func (s *modeenvSuite) TestDeepEquals(c *C) { + // start with two identical modeenvs + modeenv1 := &boot.Modeenv{ + Mode: "recovery", + RecoverySystem: "20191126", + CurrentRecoverySystems: []string{"1", "2"}, + + Base: "core20_123.snap", + TryBase: "core20_124.snap", + BaseStatus: "try", + CurrentKernels: []string{"k1", "k2"}, + + Model: "model", + BrandID: "brand", + Grade: "secured", + + CurrentTrustedBootAssets: boot.BootAssetsMap{ + "thing1": {"hash1", "hash2"}, + "thing2": {"hash3"}, + }, + } + + modeenv2 := &boot.Modeenv{ + Mode: "recovery", + RecoverySystem: "20191126", + CurrentRecoverySystems: []string{"1", "2"}, + + Base: "core20_123.snap", + TryBase: "core20_124.snap", + BaseStatus: "try", + CurrentKernels: []string{"k1", "k2"}, + + Model: "model", + BrandID: "brand", + Grade: "secured", + + CurrentTrustedBootAssets: boot.BootAssetsMap{ + "thing1": {"hash1", "hash2"}, + "thing2": {"hash3"}, + }, + } + + // same object should be the same + c.Assert(modeenv1.DeepEqual(modeenv1), Equals, true) + + // no difference should be the same at the start + c.Assert(modeenv1.DeepEqual(modeenv2), Equals, true) + c.Assert(modeenv2.DeepEqual(modeenv1), Equals, true) + + // invert CurrentKernels + modeenv2.CurrentKernels = []string{"k2", "k1"} + c.Assert(modeenv1.DeepEqual(modeenv2), Equals, false) + c.Assert(modeenv2.DeepEqual(modeenv1), Equals, false) + + // make CurrentKernels capitalized + modeenv2.CurrentKernels = []string{"K1", "k2"} + c.Assert(modeenv1.DeepEqual(modeenv2), Equals, false) + c.Assert(modeenv2.DeepEqual(modeenv1), Equals, false) + + // make CurrentKernels disappear + modeenv2.CurrentKernels = nil + c.Assert(modeenv1.DeepEqual(modeenv2), Equals, false) + c.Assert(modeenv2.DeepEqual(modeenv1), Equals, false) +} + func (s *modeenvSuite) TestReadModeWithRecoverySystem(c *C) { s.makeMockModeenvFile(c, `mode=recovery recovery_system=20191126 @@ -96,6 +287,62 @@ c.Check(modeenv.RecoverySystem, Equals, "20191126") } +func (s *modeenvSuite) TestReadModeenvWithUnknownKeysKeepsWrites(c *C) { + s.makeMockModeenvFile(c, `first_unknown=thing +mode=recovery +recovery_system=20191126 +unknown_key=some unknown value +a_key=other +`) + + modeenv, err := boot.ReadModeenv(s.tmpdir) + c.Assert(err, IsNil) + c.Check(modeenv.Mode, Equals, "recovery") + c.Check(modeenv.RecoverySystem, Equals, "20191126") + + c.Assert(modeenv.Write(), IsNil) + + c.Assert(s.mockModeenvPath, testutil.FileEquals, `mode=recovery +recovery_system=20191126 +a_key=other +first_unknown=thing +unknown_key=some unknown value +`) +} + +func (s *modeenvSuite) TestReadModeenvWithUnknownKeysDeepEqualsSameWithoutUnknownKeys(c *C) { + s.makeMockModeenvFile(c, `first_unknown=thing +mode=recovery +recovery_system=20191126 +try_base=core20_124.snap +base_status=try +unknown_key=some unknown value +current_trusted_boot_assets={"grubx64.efi":["hash1","hash2"]} +current_trusted_recovery_boot_assets={"bootx64.efi":["shimhash1","shimhash2"],"grubx64.efi":["recovery-hash1"]} +a_key=other +`) + + modeenvWithExtraKeys, err := boot.ReadModeenv(s.tmpdir) + c.Assert(err, IsNil) + c.Check(modeenvWithExtraKeys.Mode, Equals, "recovery") + c.Check(modeenvWithExtraKeys.RecoverySystem, Equals, "20191126") + + // should be the same as one that with just those keys in memory + c.Assert(modeenvWithExtraKeys.DeepEqual(&boot.Modeenv{ + Mode: "recovery", + RecoverySystem: "20191126", + TryBase: "core20_124.snap", + BaseStatus: boot.TryStatus, + CurrentTrustedBootAssets: boot.BootAssetsMap{ + "grubx64.efi": []string{"hash1", "hash2"}, + }, + CurrentTrustedRecoveryBootAssets: boot.BootAssetsMap{ + "bootx64.efi": []string{"shimhash1", "shimhash2"}, + "grubx64.efi": []string{"recovery-hash1"}, + }, + }), Equals, true) +} + func (s *modeenvSuite) TestReadModeWithBase(c *C) { s.makeMockModeenvFile(c, `mode=recovery recovery_system=20191126 @@ -254,8 +501,10 @@ c.Assert(s.mockModeenvPath, testutil.FileAbsent) modeenv := &boot.Modeenv{ - Mode: "run", - RecoverySystem: "20191128", + Mode: "run", + RecoverySystem: "20191128", + CurrentRecoverySystems: []string{"20191128", "2020-02-03", "20240101-FOO"}, + // keep this comment to make gofmt 1.9 happy Base: "core20_321.snap", TryBase: "core20_322.snap", BaseStatus: boot.TryStatus, @@ -266,9 +515,153 @@ c.Assert(s.mockModeenvPath, testutil.FileEquals, `mode=run recovery_system=20191128 +current_recovery_systems=20191128,2020-02-03,20240101-FOO base=core20_321.snap try_base=core20_322.snap base_status=try current_kernels=pc-kernel_1.snap,pc-kernel_2.snap `) } + +func (s *modeenvSuite) TestReadRecoverySystems(c *C) { + tt := []struct { + systemsString string + expectedSystems []string + }{ + { + "20191126", + []string{"20191126"}, + }, { + "20191128,2020-02-03,20240101-FOO", + []string{"20191128", "2020-02-03", "20240101-FOO"}, + }, + {",,,", nil}, + {"", nil}, + } + + for _, t := range tt { + c.Logf("tc: %q", t.systemsString) + s.makeMockModeenvFile(c, `mode=recovery +recovery_system=20191126 +current_recovery_systems=`+t.systemsString+"\n") + + modeenv, err := boot.ReadModeenv(s.tmpdir) + c.Assert(err, IsNil) + c.Check(modeenv.Mode, Equals, "recovery") + c.Check(modeenv.RecoverySystem, Equals, "20191126") + c.Check(modeenv.CurrentRecoverySystems, DeepEquals, t.expectedSystems) + } +} + +type fancyDataBothMarshallers struct { + Foo []string +} + +func (f *fancyDataBothMarshallers) MarshalModeenvValue() (string, error) { + return strings.Join(f.Foo, "#"), nil +} + +func (f *fancyDataBothMarshallers) UnmarshalModeenvValue(v string) error { + f.Foo = strings.Split(v, "#") + return nil +} + +func (f *fancyDataBothMarshallers) MarshalJSON() ([]byte, error) { + return nil, fmt.Errorf("unexpected call to JSON marshaller") +} + +func (f *fancyDataBothMarshallers) UnmarshalJSON(data []byte) error { + return fmt.Errorf("unexpected call to JSON unmarshaller") +} + +type fancyDataJSONOnly struct { + Foo []string +} + +func (f *fancyDataJSONOnly) MarshalJSON() ([]byte, error) { + return json.Marshal(f.Foo) +} + +func (f *fancyDataJSONOnly) UnmarshalJSON(data []byte) error { + return json.Unmarshal(data, &f.Foo) +} + +func (s *modeenvSuite) TestFancyMarshalUnmarshal(c *C) { + var buf bytes.Buffer + + dboth := fancyDataBothMarshallers{Foo: []string{"1", "two"}} + err := boot.MarshalModeenvEntryTo(&buf, "fancy", &dboth) + c.Assert(err, IsNil) + c.Check(buf.String(), Equals, `fancy=1#two +`) + + djson := fancyDataJSONOnly{Foo: []string{"1", "two", "with\nnewline"}} + err = boot.MarshalModeenvEntryTo(&buf, "fancy_json", &djson) + c.Assert(err, IsNil) + c.Check(buf.String(), Equals, `fancy=1#two +fancy_json=["1","two","with\nnewline"] +`) + + cfg := goconfigparser.New() + cfg.AllowNoSectionHeader = true + err = cfg.Read(&buf) + c.Assert(err, IsNil) + + var dbothRev fancyDataBothMarshallers + err = boot.UnmarshalModeenvValueFromCfg(cfg, "fancy", &dbothRev) + c.Assert(err, IsNil) + c.Check(dbothRev, DeepEquals, dboth) + + var djsonRev fancyDataJSONOnly + err = boot.UnmarshalModeenvValueFromCfg(cfg, "fancy_json", &djsonRev) + c.Assert(err, IsNil) + c.Check(djsonRev, DeepEquals, djson) +} + +func (s *modeenvSuite) TestFancyUnmarshalJSONEmpty(c *C) { + var buf bytes.Buffer + + cfg := goconfigparser.New() + cfg.AllowNoSectionHeader = true + err := cfg.Read(&buf) + c.Assert(err, IsNil) + + var djsonRev fancyDataJSONOnly + err = boot.UnmarshalModeenvValueFromCfg(cfg, "fancy_json", &djsonRev) + c.Assert(err, IsNil) + c.Check(djsonRev.Foo, IsNil) +} + +func (s *modeenvSuite) TestMarshalCurrentTrustedBootAssets(c *C) { + c.Assert(s.mockModeenvPath, testutil.FileAbsent) + + modeenv := &boot.Modeenv{ + Mode: "run", + RecoverySystem: "20191128", + CurrentTrustedBootAssets: boot.BootAssetsMap{ + "grubx64.efi": []string{"hash1", "hash2"}, + }, + CurrentTrustedRecoveryBootAssets: boot.BootAssetsMap{ + "grubx64.efi": []string{"recovery-hash1"}, + "bootx64.efi": []string{"shimhash1", "shimhash2"}, + }, + } + err := modeenv.WriteTo(s.tmpdir) + c.Assert(err, IsNil) + + c.Assert(s.mockModeenvPath, testutil.FileEquals, `mode=run +recovery_system=20191128 +current_trusted_boot_assets={"grubx64.efi":["hash1","hash2"]} +current_trusted_recovery_boot_assets={"bootx64.efi":["shimhash1","shimhash2"],"grubx64.efi":["recovery-hash1"]} +`) + + modeenvRead, err := boot.ReadModeenv(s.tmpdir) + c.Assert(err, IsNil) + c.Assert(modeenvRead.CurrentTrustedBootAssets, DeepEquals, boot.BootAssetsMap{ + "grubx64.efi": []string{"hash1", "hash2"}, + }) + c.Assert(modeenvRead.CurrentTrustedRecoveryBootAssets, DeepEquals, boot.BootAssetsMap{ + "grubx64.efi": []string{"recovery-hash1"}, + "bootx64.efi": []string{"shimhash1", "shimhash2"}, + }) +} diff -Nru snapd-2.45.1+20.04.2/boot/seal.go snapd-2.48.3+20.04/boot/seal.go --- snapd-2.45.1+20.04.2/boot/seal.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48.3+20.04/boot/seal.go 2021-02-02 08:21:12.000000000 +0000 @@ -0,0 +1,671 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2020 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 boot + +import ( + "crypto/ecdsa" + "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/logger" + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/secboot" + "github.com/snapcore/snapd/seed" + "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/strutil" + "github.com/snapcore/snapd/timings" +) + +var ( + secbootSealKeys = secboot.SealKeys + secbootResealKeys = secboot.ResealKeys + + seedReadSystemEssential = seed.ReadSystemEssential +) + +// Hook functions setup by devicestate to support device-specific full +// disk encryption implementations. The state must be locked when these +// functions are called. +var ( + HasFDESetupHook = func() (bool, error) { + return false, nil + } + RunFDESetupHook = func(op string, params *FDESetupHookParams) ([]byte, error) { + return nil, fmt.Errorf("internal error: RunFDESetupHook not set yet") + } +) + +type sealingMethod string + +const ( + sealingMethodLegacyTPM = sealingMethod("") + sealingMethodTPM = sealingMethod("tpm") + sealingMethodFDESetupHook = sealingMethod("fde-setup-hook") +) + +// FDESetupHookParams contains the inputs for the fde-setup hook +type FDESetupHookParams struct { + Key secboot.EncryptionKey + KeyName string + + Models []*asserts.Model + + //TODO:UC20: provide bootchains and a way to track measured + //boot-assets +} + +func bootChainsFileUnder(rootdir string) string { + return filepath.Join(dirs.SnapFDEDirUnder(rootdir), "boot-chains") +} + +func recoveryBootChainsFileUnder(rootdir string) string { + return filepath.Join(dirs.SnapFDEDirUnder(rootdir), "recovery-boot-chains") +} + +// 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 { + // make sure relevant locations exist + for _, p := range []string{ + InitramfsSeedEncryptionKeyDir, + InitramfsBootEncryptionKeyDir, + InstallHostFDEDataDir, + InstallHostFDESaveDir, + } { + // XXX: should that be 0700 ? + if err := os.MkdirAll(p, 0755); err != nil { + return err + } + } + + hasHook, err := HasFDESetupHook() + if err != nil { + return fmt.Errorf("cannot check for fde-setup hook %v", err) + } + if hasHook { + return sealKeyToModeenvUsingFDESetupHook(key, saveKey, model, modeenv) + } + + return sealKeyToModeenvUsingSecboot(key, saveKey, model, modeenv) +} + +func runKeySealRequests(key secboot.EncryptionKey) []secboot.SealKeyRequest { + return []secboot.SealKeyRequest{ + { + Key: key, + KeyFile: filepath.Join(InitramfsBootEncryptionKeyDir, "ubuntu-data.sealed-key"), + }, + } +} + +func fallbackKeySealRequests(key, saveKey secboot.EncryptionKey) []secboot.SealKeyRequest { + return []secboot.SealKeyRequest{ + { + Key: key, + KeyFile: filepath.Join(InitramfsSeedEncryptionKeyDir, "ubuntu-data.recovery.sealed-key"), + }, + { + Key: saveKey, + KeyFile: filepath.Join(InitramfsSeedEncryptionKeyDir, "ubuntu-save.recovery.sealed-key"), + }, + } +} + +func sealKeyToModeenvUsingFDESetupHook(key, saveKey secboot.EncryptionKey, model *asserts.Model, modeenv *Modeenv) error { + // TODO: support full boot chains + + for _, skr := range append(runKeySealRequests(key), fallbackKeySealRequests(key, saveKey)...) { + params := &FDESetupHookParams{ + Key: skr.Key, + // TODO: decide what the right KeyName is + // KeyName: filepath.Base(skr.KeyFile), + Models: []*asserts.Model{model}, + } + sealedKey, err := RunFDESetupHook("initial-setup", params) + if err != nil { + return err + } + if err := osutil.AtomicWriteFile(filepath.Join(skr.KeyFile), sealedKey, 0600, 0); err != nil { + return fmt.Errorf("cannot store key: %v", err) + } + } + + if err := stampSealedKeys(InstallHostWritableDir, "fde-setup-hook"); err != nil { + return err + } + + return nil +} + +func sealKeyToModeenvUsingSecboot(key, saveKey secboot.EncryptionKey, model *asserts.Model, modeenv *Modeenv) error { + // build the recovery mode boot chain + rbl, err := bootloader.Find(InitramfsUbuntuSeedDir, &bootloader.Options{ + Role: bootloader.RoleRecovery, + }) + if err != nil { + return fmt.Errorf("cannot find the recovery bootloader: %v", err) + } + tbl, ok := rbl.(bootloader.TrustedAssetsBootloader) + if !ok { + // TODO:UC20: later the exact kind of bootloaders we expect here might change + return fmt.Errorf("internal error: cannot seal keys without a trusted assets bootloader") + } + + recoveryBootChains, err := recoveryBootChainsForSystems([]string{modeenv.RecoverySystem}, tbl, model, modeenv) + if err != nil { + return fmt.Errorf("cannot compose recovery boot chains: %v", err) + } + + // build the run mode boot chains + bl, err := bootloader.Find(InitramfsUbuntuBootDir, &bootloader.Options{ + Role: bootloader.RoleRunMode, + NoSlashBoot: true, + }) + if err != nil { + return fmt.Errorf("cannot find the bootloader: %v", err) + } + cmdline, err := ComposeCandidateCommandLine(model) + if err != nil { + return fmt.Errorf("cannot compose the candidate command line: %v", err) + } + + runModeBootChains, err := runModeBootChains(rbl, bl, model, modeenv, cmdline) + if err != nil { + return fmt.Errorf("cannot compose run mode boot chains: %v", err) + } + + pbc := toPredictableBootChains(append(runModeBootChains, recoveryBootChains...)) + + roleToBlName := map[bootloader.Role]string{ + bootloader.RoleRecovery: rbl.Name(), + bootloader.RoleRunMode: bl.Name(), + } + + // the boot chains we seal the fallback object to + rpbc := toPredictableBootChains(recoveryBootChains) + + // gets written to a file by sealRunObjectKeys() + authKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return fmt.Errorf("cannot generate key for signing dynamic authorization policies: %v", err) + } + + if err := sealRunObjectKeys(key, pbc, authKey, roleToBlName); err != nil { + return err + } + + if err := sealFallbackObjectKeys(key, saveKey, rpbc, authKey, roleToBlName); err != nil { + return err + } + + if err := stampSealedKeys(InstallHostWritableDir, sealingMethodTPM); err != nil { + return err + } + + installBootChainsPath := bootChainsFileUnder(InstallHostWritableDir) + if err := writeBootChains(pbc, installBootChainsPath, 0); err != nil { + return err + } + + installRecoveryBootChainsPath := recoveryBootChainsFileUnder(InstallHostWritableDir) + if err := writeBootChains(rpbc, installRecoveryBootChainsPath, 0); err != nil { + return err + } + + return nil +} + +func sealRunObjectKeys(key secboot.EncryptionKey, pbc predictableBootChains, authKey *ecdsa.PrivateKey, roleToBlName map[bootloader.Role]string) error { + modelParams, err := sealKeyModelParams(pbc, roleToBlName) + if err != nil { + return fmt.Errorf("cannot prepare for key sealing: %v", err) + } + + sealKeyParams := &secboot.SealKeysParams{ + ModelParams: modelParams, + TPMPolicyAuthKey: authKey, + TPMPolicyAuthKeyFile: filepath.Join(InstallHostFDESaveDir, "tpm-policy-auth-key"), + TPMLockoutAuthFile: filepath.Join(InstallHostFDESaveDir, "tpm-lockout-auth"), + TPMProvision: true, + PCRPolicyCounterHandle: secboot.RunObjectPCRPolicyCounterHandle, + } + // 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. + // Furthermore, the run object key is stored on ubuntu-boot so that we do not + // need to continually write/read keys from ubuntu-seed. + if err := secbootSealKeys(runKeySealRequests(key), sealKeyParams); err != nil { + return fmt.Errorf("cannot seal the encryption keys: %v", err) + } + + return nil +} + +func sealFallbackObjectKeys(key, saveKey secboot.EncryptionKey, pbc predictableBootChains, authKey *ecdsa.PrivateKey, roleToBlName map[bootloader.Role]string) error { + // also seal the keys to the recovery bootchains as a fallback + modelParams, err := sealKeyModelParams(pbc, roleToBlName) + if err != nil { + return fmt.Errorf("cannot prepare for fallback key sealing: %v", err) + } + sealKeyParams := &secboot.SealKeysParams{ + ModelParams: modelParams, + TPMPolicyAuthKey: authKey, + PCRPolicyCounterHandle: secboot.FallbackObjectPCRPolicyCounterHandle, + } + // 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 := 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 +} + +// resealKeyToModeenv reseals the existing encryption key to the +// parameters specified in modeenv. +func resealKeyToModeenv(rootdir string, model *asserts.Model, modeenv *Modeenv, expectReseal bool) error { + method, err := sealedKeysMethod(rootdir) + if err == errNoSealedKeys { + // nothing to do + return nil + } + if err != nil { + return err + } + switch method { + case sealingMethodFDESetupHook: + return resealKeyToModeenvUsingFDESetupHook(rootdir, model, modeenv, expectReseal) + case sealingMethodTPM, sealingMethodLegacyTPM: + return resealKeyToModeenvSecboot(rootdir, model, modeenv, expectReseal) + default: + return fmt.Errorf("unknown key sealing method: %q", method) + } +} + +var resealKeyToModeenvUsingFDESetupHook = resealKeyToModeenvUsingFDESetupHookImpl + +func resealKeyToModeenvUsingFDESetupHookImpl(rootdir string, model *asserts.Model, modeenv *Modeenv, expectReseal bool) error { + // TODO: Implement reseal using the fde-setup hook. This will + // require a helper like "FDEShouldResealUsingSetupHook" + // that will be set by devicestate and returns (bool, + // error). It needs to return "false" during seeding + // because then there is no kernel available yet. It + // can though return true as soon as there's an active + // kernel if seeded is false + // + // It will also need to run HasFDESetupHook internally + // and return an error if the hook goes missing + // (e.g. because a kernel refresh losses the hook by + // accident). It could also run features directly and + // check for "reseal" in features. + return nil +} + +func resealKeyToModeenvSecboot(rootdir string, model *asserts.Model, modeenv *Modeenv, expectReseal bool) error { + // build the recovery mode boot chain + rbl, err := bootloader.Find(InitramfsUbuntuSeedDir, &bootloader.Options{ + Role: bootloader.RoleRecovery, + }) + if err != nil { + return fmt.Errorf("cannot find the recovery bootloader: %v", err) + } + tbl, ok := rbl.(bootloader.TrustedAssetsBootloader) + if !ok { + // TODO:UC20: later the exact kind of bootloaders we expect here might change + return fmt.Errorf("internal error: sealed keys but not a trusted assets bootloader") + } + recoveryBootChains, err := recoveryBootChainsForSystems(modeenv.CurrentRecoverySystems, tbl, model, modeenv) + if err != nil { + return fmt.Errorf("cannot compose recovery boot chains: %v", err) + } + + // build the run mode boot chains + bl, err := bootloader.Find(InitramfsUbuntuBootDir, &bootloader.Options{ + Role: bootloader.RoleRunMode, + NoSlashBoot: true, + }) + if err != nil { + return fmt.Errorf("cannot find the bootloader: %v", err) + } + cmdline, err := ComposeCommandLine(model) + if err != nil { + return fmt.Errorf("cannot compose the run mode command line: %v", err) + } + + runModeBootChains, err := runModeBootChains(rbl, bl, model, modeenv, cmdline) + if err != nil { + return fmt.Errorf("cannot compose run mode boot chains: %v", err) + } + + // reseal the run object + pbc := toPredictableBootChains(append(runModeBootChains, recoveryBootChains...)) + + needed, nextCount, err := isResealNeeded(pbc, bootChainsFileUnder(rootdir), expectReseal) + if err != nil { + return err + } + if !needed { + logger.Debugf("reseal not necessary") + return nil + } + pbcJSON, _ := json.Marshal(pbc) + logger.Debugf("resealing (%d) to boot chains: %s", nextCount, pbcJSON) + + roleToBlName := map[bootloader.Role]string{ + bootloader.RoleRecovery: rbl.Name(), + bootloader.RoleRunMode: bl.Name(), + } + + saveFDEDir := dirs.SnapFDEDirUnderSave(dirs.SnapSaveDirUnder(rootdir)) + authKeyFile := filepath.Join(saveFDEDir, "tpm-policy-auth-key") + if err := resealRunObjectKeys(pbc, authKeyFile, roleToBlName); err != nil { + return err + } + logger.Debugf("resealing (%d) succeeded", nextCount) + + bootChainsPath := bootChainsFileUnder(rootdir) + if err := writeBootChains(pbc, bootChainsPath, nextCount); err != nil { + return err + } + + // reseal the fallback object + rpbc := toPredictableBootChains(recoveryBootChains) + + var nextFallbackCount int + needed, nextFallbackCount, err = isResealNeeded(rpbc, recoveryBootChainsFileUnder(rootdir), expectReseal) + if err != nil { + return err + } + if !needed { + logger.Debugf("fallback reseal not necessary") + return nil + } + + rpbcJSON, _ := json.Marshal(rpbc) + logger.Debugf("resealing (%d) to recovery boot chains: %s", nextCount, rpbcJSON) + + if err := resealFallbackObjectKeys(rpbc, authKeyFile, roleToBlName); err != nil { + return err + } + logger.Debugf("fallback resealing (%d) succeeded", nextFallbackCount) + + recoveryBootChainsPath := recoveryBootChainsFileUnder(rootdir) + return writeBootChains(rpbc, recoveryBootChainsPath, nextFallbackCount) +} + +func resealRunObjectKeys(pbc predictableBootChains, authKeyFile string, roleToBlName map[bootloader.Role]string) error { + // get model parameters from bootchains + modelParams, err := sealKeyModelParams(pbc, roleToBlName) + if err != nil { + return fmt.Errorf("cannot prepare for key resealing: %v", err) + } + + // list all the key files to reseal + keyFiles := []string{ + filepath.Join(InitramfsBootEncryptionKeyDir, "ubuntu-data.sealed-key"), + } + + resealKeyParams := &secboot.ResealKeysParams{ + ModelParams: modelParams, + KeyFiles: keyFiles, + TPMPolicyAuthKeyFile: authKeyFile, + } + if err := secbootResealKeys(resealKeyParams); err != nil { + return fmt.Errorf("cannot reseal the encryption key: %v", err) + } + + return nil +} + +func resealFallbackObjectKeys(pbc predictableBootChains, authKeyFile string, roleToBlName map[bootloader.Role]string) error { + // get model parameters from bootchains + modelParams, err := sealKeyModelParams(pbc, roleToBlName) + if err != nil { + return fmt.Errorf("cannot prepare for fallback key resealing: %v", err) + } + + // 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"), + } + + resealKeyParams := &secboot.ResealKeysParams{ + ModelParams: modelParams, + KeyFiles: keyFiles, + TPMPolicyAuthKeyFile: authKeyFile, + } + if err := secbootResealKeys(resealKeyParams); err != nil { + return fmt.Errorf("cannot reseal the fallback encryption keys: %v", err) + } + + return nil +} + +func recoveryBootChainsForSystems(systems []string, trbl bootloader.TrustedAssetsBootloader, model *asserts.Model, modeenv *Modeenv) (chains []bootChain, err error) { + for _, system := range systems { + // get the command line + cmdline, err := ComposeRecoveryCommandLine(model, system) + if err != nil { + return nil, fmt.Errorf("cannot obtain recovery kernel command line: %v", err) + } + + // get kernel information from seed + perf := timings.New(nil) + _, snaps, err := seedReadSystemEssential(dirs.SnapSeedDir, system, []snap.Type{snap.TypeKernel}, perf) + if err != nil { + return nil, err + } + if len(snaps) != 1 { + return nil, fmt.Errorf("cannot obtain recovery kernel snap") + } + seedKernel := snaps[0] + + var kernelRev string + if seedKernel.SideInfo.Revision.Store() { + kernelRev = seedKernel.SideInfo.Revision.String() + } + + recoveryBootChain, err := trbl.RecoveryBootChain(seedKernel.Path) + if err != nil { + return nil, err + } + + // get asset chains + assetChain, kbf, err := buildBootAssets(recoveryBootChain, modeenv) + if err != nil { + return nil, err + } + + chains = append(chains, bootChain{ + BrandID: model.BrandID(), + Model: model.Model(), + Grade: model.Grade(), + ModelSignKeyID: model.SignKeyID(), + AssetChain: assetChain, + Kernel: seedKernel.SnapName(), + KernelRevision: kernelRev, + KernelCmdlines: []string{cmdline}, + model: model, + kernelBootFile: kbf, + }) + } + return chains, nil +} + +func runModeBootChains(rbl, bl bootloader.Bootloader, model *asserts.Model, modeenv *Modeenv, cmdline string) ([]bootChain, error) { + tbl, ok := rbl.(bootloader.TrustedAssetsBootloader) + if !ok { + return nil, fmt.Errorf("recovery bootloader doesn't support trusted assets") + } + + chains := make([]bootChain, 0, len(modeenv.CurrentKernels)) + for _, k := range modeenv.CurrentKernels { + info, err := snap.ParsePlaceInfoFromSnapFileName(k) + if err != nil { + return nil, err + } + runModeBootChain, err := tbl.BootChain(bl, info.MountFile()) + if err != nil { + return nil, err + } + + // get asset chains + assetChain, kbf, err := buildBootAssets(runModeBootChain, modeenv) + if err != nil { + return nil, err + } + var kernelRev string + if info.SnapRevision().Store() { + kernelRev = info.SnapRevision().String() + } + chains = append(chains, bootChain{ + BrandID: model.BrandID(), + Model: model.Model(), + Grade: model.Grade(), + ModelSignKeyID: model.SignKeyID(), + AssetChain: assetChain, + Kernel: info.SnapName(), + KernelRevision: kernelRev, + KernelCmdlines: []string{cmdline}, + model: model, + kernelBootFile: kbf, + }) + } + return chains, nil +} + +// buildBootAssets takes the BootFiles of a bootloader boot chain and +// produces corresponding bootAssets with the matching current asset +// hashes from modeenv plus it returns separately the last BootFile +// which is for the kernel. +func buildBootAssets(bootFiles []bootloader.BootFile, modeenv *Modeenv) (assets []bootAsset, kernel bootloader.BootFile, err error) { + assets = make([]bootAsset, len(bootFiles)-1) + + // the last element is the kernel which is not a boot asset + for i, bf := range bootFiles[:len(bootFiles)-1] { + name := filepath.Base(bf.Path) + var hashes []string + var ok bool + if bf.Role == bootloader.RoleRecovery { + hashes, ok = modeenv.CurrentTrustedRecoveryBootAssets[name] + } else { + hashes, ok = modeenv.CurrentTrustedBootAssets[name] + } + if !ok { + return nil, kernel, fmt.Errorf("cannot find expected boot asset %s in modeenv", name) + } + assets[i] = bootAsset{ + Role: bf.Role, + Name: name, + Hashes: hashes, + } + } + + return assets, bootFiles[len(bootFiles)-1], nil +} + +func sealKeyModelParams(pbc predictableBootChains, roleToBlName map[bootloader.Role]string) ([]*secboot.SealKeyModelParams, error) { + modelToParams := map[*asserts.Model]*secboot.SealKeyModelParams{} + modelParams := make([]*secboot.SealKeyModelParams, 0, len(pbc)) + + for _, bc := range pbc { + loadChains, err := bootAssetsToLoadChains(bc.AssetChain, bc.kernelBootFile, roleToBlName) + if err != nil { + return nil, fmt.Errorf("cannot build load chains with current boot assets: %s", err) + } + + // group parameters by model, reuse an existing SealKeyModelParams + // if the model is the same. + if params, ok := modelToParams[bc.model]; ok { + params.KernelCmdlines = strutil.SortedListsUniqueMerge(params.KernelCmdlines, bc.KernelCmdlines) + params.EFILoadChains = append(params.EFILoadChains, loadChains...) + } else { + param := &secboot.SealKeyModelParams{ + Model: bc.model, + KernelCmdlines: bc.KernelCmdlines, + EFILoadChains: loadChains, + } + modelParams = append(modelParams, param) + modelToParams[bc.model] = param + } + } + + return modelParams, nil +} + +// isResealNeeded returns true when the predictable boot chains provided as +// input do not match the cached boot chains on disk under rootdir. +// It also returns the next value for the reasel count that is saved +// together with the boot chains. +// A hint expectReseal can be provided, it is used when the matching +// is ambigous because the boot chains contain unrevisioned kernels. +func isResealNeeded(pbc predictableBootChains, bootChainsFile string, expectReseal bool) (ok bool, nextCount int, err error) { + previousPbc, c, err := readBootChains(bootChainsFile) + if err != nil { + return false, 0, err + } + + switch predictableBootChainsEqualForReseal(pbc, previousPbc) { + case bootChainEquivalent: + return false, c + 1, nil + case bootChainUnrevisioned: + return expectReseal, c + 1, nil + case bootChainDifferent: + } + return true, c + 1, nil +} diff -Nru snapd-2.45.1+20.04.2/boot/seal_test.go snapd-2.48.3+20.04/boot/seal_test.go --- snapd-2.45.1+20.04.2/boot/seal_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48.3+20.04/boot/seal_test.go 2021-02-02 08:21:12.000000000 +0000 @@ -0,0 +1,1044 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2020 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 boot_test + +import ( + "errors" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "strconv" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/boot" + "github.com/snapcore/snapd/boot/boottest" + "github.com/snapcore/snapd/bootloader" + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/secboot" + "github.com/snapcore/snapd/seed" + "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/testutil" + "github.com/snapcore/snapd/timings" +) + +type sealSuite struct { + testutil.BaseTest +} + +var _ = Suite(&sealSuite{}) + +func (s *sealSuite) SetUpTest(c *C) { + s.BaseTest.SetUpTest(c) + + rootdir := c.MkDir() + dirs.SetRootDir(rootdir) + s.AddCleanup(func() { dirs.SetRootDir("/") }) +} + +func (s *sealSuite) TestSealKeyToModeenv(c *C) { + for _, tc := range []struct { + sealErr error + err string + }{ + {sealErr: nil, err: ""}, + {sealErr: errors.New("seal error"), err: "cannot seal the encryption keys: seal error"}, + } { + rootdir := c.MkDir() + dirs.SetRootDir(rootdir) + defer dirs.SetRootDir("") + + err := createMockGrubCfg(filepath.Join(rootdir, "run/mnt/ubuntu-seed")) + c.Assert(err, IsNil) + + err = createMockGrubCfg(filepath.Join(rootdir, "run/mnt/ubuntu-boot")) + c.Assert(err, IsNil) + + modeenv := &boot.Modeenv{ + RecoverySystem: "20200825", + CurrentTrustedRecoveryBootAssets: boot.BootAssetsMap{ + "grubx64.efi": []string{"grub-hash-1"}, + "bootx64.efi": []string{"shim-hash-1"}, + }, + + CurrentTrustedBootAssets: boot.BootAssetsMap{ + "grubx64.efi": []string{"run-grub-hash-1"}, + }, + + CurrentKernels: []string{"pc-kernel_500.snap"}, + } + + // mock asset cache + p := filepath.Join(rootdir, "var/lib/snapd/boot-assets/grub/bootx64.efi-shim-hash-1") + err = os.MkdirAll(filepath.Dir(p), 0755) + c.Assert(err, IsNil) + err = ioutil.WriteFile(p, nil, 0644) + c.Assert(err, IsNil) + err = ioutil.WriteFile(filepath.Join(rootdir, "var/lib/snapd/boot-assets/grub/grubx64.efi-grub-hash-1"), nil, 0644) + c.Assert(err, IsNil) + err = ioutil.WriteFile(filepath.Join(rootdir, "var/lib/snapd/boot-assets/grub/grubx64.efi-run-grub-hash-1"), nil, 0644) + c.Assert(err, IsNil) + + // set encryption key + myKey := secboot.EncryptionKey{} + myKey2 := secboot.EncryptionKey{} + for i := range myKey { + myKey[i] = byte(i) + myKey2[i] = byte(128 + i) + } + + model := boottest.MakeMockUC20Model() + + // set a mock recovery kernel + readSystemEssentialCalls := 0 + restore := boot.MockSeedReadSystemEssential(func(seedDir, label string, essentialTypes []snap.Type, tm timings.Measurer) (*asserts.Model, []*seed.Snap, error) { + readSystemEssentialCalls++ + kernelSnap := &seed.Snap{ + Path: "/var/lib/snapd/seed/snaps/pc-kernel_1.snap", + SideInfo: &snap.SideInfo{ + RealName: "pc-kernel", + Revision: snap.Revision{N: 1}, + }, + } + return model, []*seed.Snap{kernelSnap}, nil + }) + defer restore() + + // set mock key sealing + sealKeysCalls := 0 + restore = boot.MockSecbootSealKeys(func(keys []secboot.SealKeyRequest, params *secboot.SealKeysParams) error { + 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, KeyFile: dataKeyFile}}) + 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") + c.Check(keys, DeepEquals, []secboot.SealKeyRequest{{Key: myKey, KeyFile: dataKeyFile}, {Key: myKey2, KeyFile: saveKeyFile}}) + default: + c.Errorf("unexpected additional call to secboot.SealKeys (call # %d)", sealKeysCalls) + } + c.Assert(params.ModelParams, HasLen, 1) + for _, d := range []string{boot.InitramfsSeedEncryptionKeyDir, boot.InstallHostFDEDataDir} { + ex, isdir, _ := osutil.DirExists(d) + c.Check(ex && isdir, Equals, true, Commentf("location %q does not exist or is not a directory", d)) + } + + shim := bootloader.NewBootFile("", filepath.Join(rootdir, "var/lib/snapd/boot-assets/grub/bootx64.efi-shim-hash-1"), bootloader.RoleRecovery) + grub := bootloader.NewBootFile("", filepath.Join(rootdir, "var/lib/snapd/boot-assets/grub/grubx64.efi-grub-hash-1"), bootloader.RoleRecovery) + runGrub := bootloader.NewBootFile("", filepath.Join(rootdir, "var/lib/snapd/boot-assets/grub/grubx64.efi-run-grub-hash-1"), bootloader.RoleRunMode) + kernel := bootloader.NewBootFile("/var/lib/snapd/seed/snaps/pc-kernel_1.snap", "kernel.efi", bootloader.RoleRecovery) + runKernel := bootloader.NewBootFile(filepath.Join(rootdir, "var/lib/snapd/snaps/pc-kernel_500.snap"), "kernel.efi", bootloader.RoleRunMode) + + switch sealKeysCalls { + case 1: + c.Assert(params.ModelParams[0].EFILoadChains, DeepEquals, []*secboot.LoadChain{ + secboot.NewLoadChain(shim, + secboot.NewLoadChain(grub, + secboot.NewLoadChain(kernel))), + secboot.NewLoadChain(shim, + secboot.NewLoadChain(grub, + secboot.NewLoadChain(runGrub, + secboot.NewLoadChain(runKernel)))), + }) + c.Assert(params.ModelParams[0].KernelCmdlines, DeepEquals, []string{ + "snapd_recovery_mode=recover snapd_recovery_system=20200825 console=ttyS0 console=tty1 panic=-1", + "snapd_recovery_mode=run console=ttyS0 console=tty1 panic=-1", + }) + case 2: + c.Assert(params.ModelParams[0].EFILoadChains, DeepEquals, []*secboot.LoadChain{ + secboot.NewLoadChain(shim, + secboot.NewLoadChain(grub, + secboot.NewLoadChain(kernel))), + }) + c.Assert(params.ModelParams[0].KernelCmdlines, DeepEquals, []string{ + "snapd_recovery_mode=recover snapd_recovery_system=20200825 console=ttyS0 console=tty1 panic=-1", + }) + default: + c.Errorf("unexpected additional call to secboot.SealKeys (call # %d)", sealKeysCalls) + } + c.Assert(params.ModelParams[0].Model.DisplayName(), Equals, "My Model") + + return tc.sealErr + }) + 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 == "" { + c.Assert(err, IsNil) + } else { + c.Assert(err, ErrorMatches, tc.err) + continue + } + + // verify the boot chains data file + pbc, cnt, err := boot.ReadBootChains(filepath.Join(dirs.SnapFDEDirUnder(boot.InstallHostWritableDir), "boot-chains")) + c.Assert(err, IsNil) + c.Check(cnt, Equals, 0) + c.Check(pbc, DeepEquals, boot.PredictableBootChains{ + boot.BootChain{ + BrandID: "my-brand", + Model: "my-model-uc20", + Grade: "dangerous", + ModelSignKeyID: "Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij", + AssetChain: []boot.BootAsset{ + { + Role: "recovery", + Name: "bootx64.efi", + Hashes: []string{"shim-hash-1"}, + }, + { + Role: "recovery", + Name: "grubx64.efi", + Hashes: []string{"grub-hash-1"}, + }, + }, + Kernel: "pc-kernel", + KernelRevision: "1", + KernelCmdlines: []string{ + "snapd_recovery_mode=recover snapd_recovery_system=20200825 console=ttyS0 console=tty1 panic=-1", + }, + }, + boot.BootChain{ + BrandID: "my-brand", + Model: "my-model-uc20", + Grade: "dangerous", + ModelSignKeyID: "Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij", + AssetChain: []boot.BootAsset{ + { + Role: "recovery", + Name: "bootx64.efi", + Hashes: []string{"shim-hash-1"}, + }, + { + Role: "recovery", + Name: "grubx64.efi", + Hashes: []string{"grub-hash-1"}, + }, + { + Role: "run-mode", + Name: "grubx64.efi", + Hashes: []string{"run-grub-hash-1"}, + }, + }, + Kernel: "pc-kernel", + KernelRevision: "500", + KernelCmdlines: []string{ + "snapd_recovery_mode=run console=ttyS0 console=tty1 panic=-1", + }, + }, + }) + + // verify the recovery boot chains + pbc, cnt, err = boot.ReadBootChains(filepath.Join(dirs.SnapFDEDirUnder(boot.InstallHostWritableDir), "recovery-boot-chains")) + c.Assert(err, IsNil) + c.Check(cnt, Equals, 0) + c.Check(pbc, DeepEquals, boot.PredictableBootChains{ + boot.BootChain{ + BrandID: "my-brand", + Model: "my-model-uc20", + Grade: "dangerous", + ModelSignKeyID: "Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij", + AssetChain: []boot.BootAsset{ + { + Role: "recovery", + Name: "bootx64.efi", + Hashes: []string{"shim-hash-1"}, + }, + { + Role: "recovery", + Name: "grubx64.efi", + Hashes: []string{"grub-hash-1"}, + }, + }, + Kernel: "pc-kernel", + KernelRevision: "1", + KernelCmdlines: []string{ + "snapd_recovery_mode=recover snapd_recovery_system=20200825 console=ttyS0 console=tty1 panic=-1", + }, + }, + }) + + // marker + marker := filepath.Join(dirs.SnapFDEDirUnder(boot.InstallHostWritableDir), "sealed-keys") + c.Check(marker, testutil.FileEquals, "tpm") + } +} + +// TODO:UC20: also test fallback reseal +func (s *sealSuite) TestResealKeyToModeenv(c *C) { + var prevPbc boot.PredictableBootChains + + for _, tc := range []struct { + sealedKeys bool + prevPbc bool + resealErr error + err string + }{ + {sealedKeys: false, resealErr: nil, err: ""}, + {sealedKeys: true, resealErr: nil, err: ""}, + {sealedKeys: true, resealErr: errors.New("reseal error"), err: "cannot reseal the encryption key: reseal error"}, + {prevPbc: true, sealedKeys: true, resealErr: nil, err: ""}, + } { + rootdir := c.MkDir() + dirs.SetRootDir(rootdir) + defer dirs.SetRootDir("") + + if tc.sealedKeys { + c.Assert(os.MkdirAll(dirs.SnapFDEDir, 0755), IsNil) + err := ioutil.WriteFile(filepath.Join(dirs.SnapFDEDir, "sealed-keys"), nil, 0644) + c.Assert(err, IsNil) + + } + + err := createMockGrubCfg(filepath.Join(rootdir, "run/mnt/ubuntu-seed")) + c.Assert(err, IsNil) + + err = createMockGrubCfg(filepath.Join(rootdir, "run/mnt/ubuntu-boot")) + c.Assert(err, IsNil) + + modeenv := &boot.Modeenv{ + CurrentRecoverySystems: []string{"20200825"}, + CurrentTrustedRecoveryBootAssets: boot.BootAssetsMap{ + "grubx64.efi": []string{"grub-hash-1"}, + "bootx64.efi": []string{"shim-hash-1", "shim-hash-2"}, + }, + + CurrentTrustedBootAssets: boot.BootAssetsMap{ + "grubx64.efi": []string{"run-grub-hash-1", "run-grub-hash-2"}, + }, + + CurrentKernels: []string{"pc-kernel_500.snap", "pc-kernel_600.snap"}, + } + + if tc.prevPbc { + err := boot.WriteBootChains(prevPbc, filepath.Join(dirs.SnapFDEDir, "boot-chains"), 9) + c.Assert(err, IsNil) + } + + // mock asset cache + p := filepath.Join(rootdir, "var/lib/snapd/boot-assets/grub/bootx64.efi-shim-hash-1") + err = os.MkdirAll(filepath.Dir(p), 0755) + c.Assert(err, IsNil) + err = ioutil.WriteFile(p, nil, 0644) + c.Assert(err, IsNil) + err = ioutil.WriteFile(filepath.Join(rootdir, "var/lib/snapd/boot-assets/grub/bootx64.efi-shim-hash-2"), nil, 0644) + c.Assert(err, IsNil) + err = ioutil.WriteFile(filepath.Join(rootdir, "var/lib/snapd/boot-assets/grub/grubx64.efi-grub-hash-1"), nil, 0644) + c.Assert(err, IsNil) + err = ioutil.WriteFile(filepath.Join(rootdir, "var/lib/snapd/boot-assets/grub/grubx64.efi-run-grub-hash-1"), nil, 0644) + c.Assert(err, IsNil) + err = ioutil.WriteFile(filepath.Join(rootdir, "var/lib/snapd/boot-assets/grub/grubx64.efi-run-grub-hash-2"), nil, 0644) + c.Assert(err, IsNil) + + model := boottest.MakeMockUC20Model() + + // set a mock recovery kernel + readSystemEssentialCalls := 0 + restore := boot.MockSeedReadSystemEssential(func(seedDir, label string, essentialTypes []snap.Type, tm timings.Measurer) (*asserts.Model, []*seed.Snap, error) { + readSystemEssentialCalls++ + kernelSnap := &seed.Snap{ + Path: "/var/lib/snapd/seed/snaps/pc-kernel_1.snap", + SideInfo: &snap.SideInfo{ + RealName: "pc-kernel", + Revision: snap.Revision{N: 1}, + }, + } + return model, []*seed.Snap{kernelSnap}, nil + }) + defer restore() + + // set mock key resealing + resealKeysCalls := 0 + restore = boot.MockSecbootResealKeys(func(params *secboot.ResealKeysParams) error { + c.Check(params.TPMPolicyAuthKeyFile, Equals, filepath.Join(dirs.SnapSaveDir, "device/fde", "tpm-policy-auth-key")) + + resealKeysCalls++ + c.Assert(params.ModelParams, HasLen, 1) + + // shared parameters + c.Assert(params.ModelParams[0].Model.DisplayName(), Equals, "My Model") + switch resealKeysCalls { + case 1: + c.Assert(params.ModelParams[0].KernelCmdlines, DeepEquals, []string{ + "snapd_recovery_mode=recover snapd_recovery_system=20200825 console=ttyS0 console=tty1 panic=-1", + "snapd_recovery_mode=run console=ttyS0 console=tty1 panic=-1", + }) + // load chains + c.Assert(params.ModelParams[0].EFILoadChains, HasLen, 6) + case 2: + c.Assert(params.ModelParams[0].KernelCmdlines, DeepEquals, []string{ + "snapd_recovery_mode=recover snapd_recovery_system=20200825 console=ttyS0 console=tty1 panic=-1", + }) + // load chains + c.Assert(params.ModelParams[0].EFILoadChains, HasLen, 2) + default: + c.Errorf("unexpected additional call to secboot.ResealKeys (call # %d)", resealKeysCalls) + } + + // recovery parameters + shim := bootloader.NewBootFile("", filepath.Join(rootdir, "var/lib/snapd/boot-assets/grub/bootx64.efi-shim-hash-1"), bootloader.RoleRecovery) + shim2 := bootloader.NewBootFile("", filepath.Join(rootdir, "var/lib/snapd/boot-assets/grub/bootx64.efi-shim-hash-2"), bootloader.RoleRecovery) + grub := bootloader.NewBootFile("", filepath.Join(rootdir, "var/lib/snapd/boot-assets/grub/grubx64.efi-grub-hash-1"), bootloader.RoleRecovery) + kernel := bootloader.NewBootFile("/var/lib/snapd/seed/snaps/pc-kernel_1.snap", "kernel.efi", bootloader.RoleRecovery) + + c.Assert(params.ModelParams[0].EFILoadChains[:2], DeepEquals, []*secboot.LoadChain{ + secboot.NewLoadChain(shim, + secboot.NewLoadChain(grub, + secboot.NewLoadChain(kernel))), + secboot.NewLoadChain(shim2, + secboot.NewLoadChain(grub, + secboot.NewLoadChain(kernel))), + }) + + // run mode parameters + runGrub := bootloader.NewBootFile("", filepath.Join(rootdir, "var/lib/snapd/boot-assets/grub/grubx64.efi-run-grub-hash-1"), bootloader.RoleRunMode) + runGrub2 := bootloader.NewBootFile("", filepath.Join(rootdir, "var/lib/snapd/boot-assets/grub/grubx64.efi-run-grub-hash-2"), bootloader.RoleRunMode) + runKernel := bootloader.NewBootFile(filepath.Join(rootdir, "var/lib/snapd/snaps/pc-kernel_500.snap"), "kernel.efi", bootloader.RoleRunMode) + runKernel2 := bootloader.NewBootFile(filepath.Join(rootdir, "var/lib/snapd/snaps/pc-kernel_600.snap"), "kernel.efi", bootloader.RoleRunMode) + + switch resealKeysCalls { + case 1: + c.Assert(params.ModelParams[0].EFILoadChains[2:4], DeepEquals, []*secboot.LoadChain{ + secboot.NewLoadChain(shim, + secboot.NewLoadChain(grub, + secboot.NewLoadChain(runGrub, + secboot.NewLoadChain(runKernel)), + secboot.NewLoadChain(runGrub2, + secboot.NewLoadChain(runKernel)), + )), + secboot.NewLoadChain(shim2, + secboot.NewLoadChain(grub, + secboot.NewLoadChain(runGrub, + secboot.NewLoadChain(runKernel)), + secboot.NewLoadChain(runGrub2, + secboot.NewLoadChain(runKernel)), + )), + }) + + c.Assert(params.ModelParams[0].EFILoadChains[4:], DeepEquals, []*secboot.LoadChain{ + secboot.NewLoadChain(shim, + secboot.NewLoadChain(grub, + secboot.NewLoadChain(runGrub, + secboot.NewLoadChain(runKernel2)), + secboot.NewLoadChain(runGrub2, + secboot.NewLoadChain(runKernel2)), + )), + secboot.NewLoadChain(shim2, + secboot.NewLoadChain(grub, + secboot.NewLoadChain(runGrub, + secboot.NewLoadChain(runKernel2)), + secboot.NewLoadChain(runGrub2, + secboot.NewLoadChain(runKernel2)), + )), + }) + } + + return tc.resealErr + }) + defer restore() + + // here we don't have unasserted kernels so just set + // expectReseal to false as it doesn't matter; + // the behavior with unasserted kernel is tested in + // boot_test.go specific tests + const expectReseal = false + err = boot.ResealKeyToModeenv(rootdir, model, modeenv, expectReseal) + if !tc.sealedKeys || tc.prevPbc { + // did nothing + c.Assert(err, IsNil) + c.Assert(resealKeysCalls, Equals, 0) + continue + } + if tc.resealErr != nil { + c.Assert(resealKeysCalls, Equals, 1) + } else { + c.Assert(resealKeysCalls, Equals, 2) + } + if tc.err == "" { + c.Assert(err, IsNil) + } else { + c.Assert(err, ErrorMatches, tc.err) + continue + } + + // verify the boot chains data file + pbc, cnt, err := boot.ReadBootChains(filepath.Join(dirs.SnapFDEDir, "boot-chains")) + c.Assert(err, IsNil) + if tc.prevPbc { + c.Assert(cnt, Equals, 10) + } else { + c.Assert(cnt, Equals, 1) + } + c.Check(pbc, DeepEquals, boot.PredictableBootChains{ + boot.BootChain{ + BrandID: "my-brand", + Model: "my-model-uc20", + Grade: "dangerous", + ModelSignKeyID: "Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij", + AssetChain: []boot.BootAsset{ + { + Role: "recovery", + Name: "bootx64.efi", + Hashes: []string{"shim-hash-1", "shim-hash-2"}, + }, + { + Role: "recovery", + Name: "grubx64.efi", + Hashes: []string{"grub-hash-1"}, + }, + }, + Kernel: "pc-kernel", + KernelRevision: "1", + KernelCmdlines: []string{ + "snapd_recovery_mode=recover snapd_recovery_system=20200825 console=ttyS0 console=tty1 panic=-1", + }, + }, + boot.BootChain{ + BrandID: "my-brand", + Model: "my-model-uc20", + Grade: "dangerous", + ModelSignKeyID: "Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij", + AssetChain: []boot.BootAsset{ + { + Role: "recovery", + Name: "bootx64.efi", + Hashes: []string{"shim-hash-1", "shim-hash-2"}, + }, + { + Role: "recovery", + Name: "grubx64.efi", + Hashes: []string{"grub-hash-1"}, + }, + { + Role: "run-mode", + Name: "grubx64.efi", + Hashes: []string{"run-grub-hash-1", "run-grub-hash-2"}, + }, + }, + Kernel: "pc-kernel", + KernelRevision: "500", + KernelCmdlines: []string{ + "snapd_recovery_mode=run console=ttyS0 console=tty1 panic=-1", + }, + }, + boot.BootChain{ + BrandID: "my-brand", + Model: "my-model-uc20", + Grade: "dangerous", + ModelSignKeyID: "Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij", + AssetChain: []boot.BootAsset{ + { + Role: "recovery", + Name: "bootx64.efi", + Hashes: []string{"shim-hash-1", "shim-hash-2"}, + }, + { + Role: "recovery", + Name: "grubx64.efi", + Hashes: []string{"grub-hash-1"}, + }, + { + Role: "run-mode", + Name: "grubx64.efi", + Hashes: []string{"run-grub-hash-1", "run-grub-hash-2"}, + }, + }, + Kernel: "pc-kernel", + KernelRevision: "600", + KernelCmdlines: []string{ + "snapd_recovery_mode=run console=ttyS0 console=tty1 panic=-1", + }, + }, + }) + prevPbc = pbc + } +} + +func (s *sealSuite) TestRecoveryBootChainsForSystems(c *C) { + for _, tc := range []struct { + assetsMap boot.BootAssetsMap + recoverySystems []string + undefinedKernel bool + expectedAssets []boot.BootAsset + expectedKernelRevs []int + err string + }{ + { + // transition sequences + recoverySystems: []string{"20200825"}, + assetsMap: boot.BootAssetsMap{ + "grubx64.efi": []string{"grub-hash-1", "grub-hash-2"}, + "bootx64.efi": []string{"shim-hash-1"}, + }, + expectedAssets: []boot.BootAsset{ + {Role: bootloader.RoleRecovery, Name: "bootx64.efi", Hashes: []string{"shim-hash-1"}}, + {Role: bootloader.RoleRecovery, Name: "grubx64.efi", Hashes: []string{"grub-hash-1", "grub-hash-2"}}, + }, + expectedKernelRevs: []int{1}, + }, + { + // two systems + recoverySystems: []string{"20200825", "20200831"}, + assetsMap: boot.BootAssetsMap{ + "grubx64.efi": []string{"grub-hash-1", "grub-hash-2"}, + "bootx64.efi": []string{"shim-hash-1"}, + }, + expectedAssets: []boot.BootAsset{ + {Role: bootloader.RoleRecovery, Name: "bootx64.efi", Hashes: []string{"shim-hash-1"}}, + {Role: bootloader.RoleRecovery, Name: "grubx64.efi", Hashes: []string{"grub-hash-1", "grub-hash-2"}}, + }, + expectedKernelRevs: []int{1, 3}, + }, + { + // non-transition sequence + recoverySystems: []string{"20200825"}, + assetsMap: boot.BootAssetsMap{ + "grubx64.efi": []string{"grub-hash-1"}, + "bootx64.efi": []string{"shim-hash-1"}, + }, + expectedAssets: []boot.BootAsset{ + {Role: bootloader.RoleRecovery, Name: "bootx64.efi", Hashes: []string{"shim-hash-1"}}, + {Role: bootloader.RoleRecovery, Name: "grubx64.efi", Hashes: []string{"grub-hash-1"}}, + }, + expectedKernelRevs: []int{1}, + }, + { + // invalid recovery system label + recoverySystems: []string{"0"}, + err: `invalid system seed label: "0"`, + }, + } { + rootdir := c.MkDir() + dirs.SetRootDir(rootdir) + defer dirs.SetRootDir("") + + // set recovery kernel + restore := boot.MockSeedReadSystemEssential(func(seedDir, label string, essentialTypes []snap.Type, tm timings.Measurer) (*asserts.Model, []*seed.Snap, error) { + if label != "20200825" && label != "20200831" { + return nil, nil, fmt.Errorf("invalid system seed label: %q", label) + } + kernelRev := 1 + if label == "20200831" { + kernelRev = 3 + } + kernelSnap := &seed.Snap{ + Path: fmt.Sprintf("/var/lib/snapd/seed/snaps/pc-kernel_%d.snap", kernelRev), + SideInfo: &snap.SideInfo{ + RealName: "pc-kernel", + Revision: snap.R(kernelRev), + }, + } + return nil, []*seed.Snap{kernelSnap}, nil + }) + defer restore() + + grubDir := filepath.Join(rootdir, "run/mnt/ubuntu-seed") + err := createMockGrubCfg(grubDir) + c.Assert(err, IsNil) + + bl, err := bootloader.Find(grubDir, &bootloader.Options{Role: bootloader.RoleRecovery}) + c.Assert(err, IsNil) + tbl, ok := bl.(bootloader.TrustedAssetsBootloader) + c.Assert(ok, Equals, true) + + model := boottest.MakeMockUC20Model() + + modeenv := &boot.Modeenv{ + CurrentTrustedRecoveryBootAssets: tc.assetsMap, + } + + bc, err := boot.RecoveryBootChainsForSystems(tc.recoverySystems, tbl, model, modeenv) + if tc.err == "" { + c.Assert(err, IsNil) + c.Assert(bc, HasLen, len(tc.recoverySystems)) + for i, chain := range bc { + c.Assert(chain.AssetChain, DeepEquals, tc.expectedAssets) + c.Check(chain.Kernel, Equals, "pc-kernel") + expectedKernelRev := tc.expectedKernelRevs[i] + c.Check(chain.KernelRevision, Equals, fmt.Sprintf("%d", expectedKernelRev)) + c.Check(chain.KernelBootFile(), DeepEquals, bootloader.BootFile{Snap: fmt.Sprintf("/var/lib/snapd/seed/snaps/pc-kernel_%d.snap", expectedKernelRev), Path: "kernel.efi", Role: bootloader.RoleRecovery}) + } + } else { + c.Assert(err, ErrorMatches, tc.err) + } + + } + +} + +func createMockGrubCfg(baseDir string) error { + cfg := filepath.Join(baseDir, "EFI/ubuntu/grub.cfg") + if err := os.MkdirAll(filepath.Dir(cfg), 0755); err != nil { + return err + } + return ioutil.WriteFile(cfg, []byte("# Snapd-Boot-Config-Edition: 1\n"), 0644) +} + +func (s *sealSuite) TestSealKeyModelParams(c *C) { + rootdir := c.MkDir() + dirs.SetRootDir(rootdir) + defer dirs.SetRootDir("") + + model := boottest.MakeMockUC20Model() + + roleToBlName := map[bootloader.Role]string{ + bootloader.RoleRecovery: "grub", + bootloader.RoleRunMode: "grub", + } + // mock asset cache + p := filepath.Join(rootdir, "var/lib/snapd/boot-assets/grub/shim-shim-hash") + err := os.MkdirAll(filepath.Dir(p), 0755) + c.Assert(err, IsNil) + err = ioutil.WriteFile(p, nil, 0644) + c.Assert(err, IsNil) + err = ioutil.WriteFile(filepath.Join(rootdir, "var/lib/snapd/boot-assets/grub/loader-loader-hash1"), nil, 0644) + c.Assert(err, IsNil) + err = ioutil.WriteFile(filepath.Join(rootdir, "var/lib/snapd/boot-assets/grub/loader-loader-hash2"), nil, 0644) + c.Assert(err, IsNil) + + oldmodel := boottest.MakeMockUC20Model(map[string]interface{}{ + "model": "old-model-uc20", + "timestamp": "2019-10-01T08:00:00+00:00", + }) + + // old recovery + oldrc := boot.BootChain{ + BrandID: oldmodel.BrandID(), + Model: oldmodel.Model(), + AssetChain: []boot.BootAsset{ + {Name: "shim", Role: bootloader.RoleRecovery, Hashes: []string{"shim-hash"}}, + {Name: "loader", Role: bootloader.RoleRecovery, Hashes: []string{"loader-hash1"}}, + }, + KernelCmdlines: []string{"panic=1", "oldrc"}, + } + oldrc.SetModelAssertion(oldmodel) + oldkbf := bootloader.BootFile{Snap: "pc-kernel_1.snap"} + oldrc.SetKernelBootFile(oldkbf) + + // recovery + rc1 := boot.BootChain{ + BrandID: model.BrandID(), + Model: model.Model(), + AssetChain: []boot.BootAsset{ + {Name: "shim", Role: bootloader.RoleRecovery, Hashes: []string{"shim-hash"}}, + {Name: "loader", Role: bootloader.RoleRecovery, Hashes: []string{"loader-hash1"}}, + }, + KernelCmdlines: []string{"panic=1", "rc1"}, + } + rc1.SetModelAssertion(model) + rc1kbf := bootloader.BootFile{Snap: "pc-kernel_10.snap"} + rc1.SetKernelBootFile(rc1kbf) + + // run system + runc1 := boot.BootChain{ + BrandID: model.BrandID(), + Model: model.Model(), + AssetChain: []boot.BootAsset{ + {Name: "shim", Role: bootloader.RoleRecovery, Hashes: []string{"shim-hash"}}, + {Name: "loader", Role: bootloader.RoleRecovery, Hashes: []string{"loader-hash1"}}, + {Name: "loader", Role: bootloader.RoleRunMode, Hashes: []string{"loader-hash2"}}, + }, + KernelCmdlines: []string{"panic=1", "runc1"}, + } + runc1.SetModelAssertion(model) + runc1kbf := bootloader.BootFile{Snap: "pc-kernel_50.snap"} + runc1.SetKernelBootFile(runc1kbf) + + pbc := boot.ToPredictableBootChains([]boot.BootChain{rc1, runc1, oldrc}) + + shim := bootloader.NewBootFile("", filepath.Join(rootdir, "var/lib/snapd/boot-assets/grub/shim-shim-hash"), bootloader.RoleRecovery) + loader1 := bootloader.NewBootFile("", filepath.Join(rootdir, "var/lib/snapd/boot-assets/grub/loader-loader-hash1"), bootloader.RoleRecovery) + loader2 := bootloader.NewBootFile("", filepath.Join(rootdir, "var/lib/snapd/boot-assets/grub/loader-loader-hash2"), bootloader.RoleRunMode) + + params, err := boot.SealKeyModelParams(pbc, roleToBlName) + c.Assert(err, IsNil) + c.Check(params, HasLen, 2) + c.Check(params[0].Model, Equals, model) + // NB: merging of lists makes panic=1 appear once + c.Check(params[0].KernelCmdlines, DeepEquals, []string{"panic=1", "rc1", "runc1"}) + + c.Check(params[0].EFILoadChains, DeepEquals, []*secboot.LoadChain{ + secboot.NewLoadChain(shim, + secboot.NewLoadChain(loader1, + secboot.NewLoadChain(rc1kbf))), + secboot.NewLoadChain(shim, + secboot.NewLoadChain(loader1, + secboot.NewLoadChain(loader2, + secboot.NewLoadChain(runc1kbf)))), + }) + + c.Check(params[1].Model, Equals, oldmodel) + c.Check(params[1].KernelCmdlines, DeepEquals, []string{"oldrc", "panic=1"}) + c.Check(params[1].EFILoadChains, DeepEquals, []*secboot.LoadChain{ + secboot.NewLoadChain(shim, + secboot.NewLoadChain(loader1, + secboot.NewLoadChain(oldkbf))), + }) +} + +func (s *sealSuite) TestIsResealNeeded(c *C) { + if os.Geteuid() == 0 { + c.Skip("the test cannot be run by the root user") + } + + chains := []boot.BootChain{ + { + BrandID: "mybrand", + Model: "foo", + Grade: "signed", + ModelSignKeyID: "my-key-id", + AssetChain: []boot.BootAsset{ + // hashes will be sorted + {Role: bootloader.RoleRecovery, Name: "shim", Hashes: []string{"x", "y"}}, + {Role: bootloader.RoleRecovery, Name: "loader", Hashes: []string{"c", "d"}}, + {Role: bootloader.RoleRunMode, Name: "loader", Hashes: []string{"z", "x"}}, + }, + Kernel: "pc-kernel-other", + KernelRevision: "2345", + KernelCmdlines: []string{`snapd_recovery_mode=run foo`}, + }, { + BrandID: "mybrand", + Model: "foo", + Grade: "dangerous", + ModelSignKeyID: "my-key-id", + AssetChain: []boot.BootAsset{ + // hashes will be sorted + {Role: bootloader.RoleRecovery, Name: "shim", Hashes: []string{"y", "x"}}, + {Role: bootloader.RoleRecovery, Name: "loader", Hashes: []string{"c", "d"}}, + }, + Kernel: "pc-kernel-recovery", + KernelRevision: "1234", + KernelCmdlines: []string{`snapd_recovery_mode=recover foo`}, + }, + } + + pbc := boot.ToPredictableBootChains(chains) + + rootdir := c.MkDir() + err := boot.WriteBootChains(pbc, filepath.Join(dirs.SnapFDEDirUnder(rootdir), "boot-chains"), 2) + c.Assert(err, IsNil) + + needed, _, err := boot.IsResealNeeded(pbc, filepath.Join(dirs.SnapFDEDirUnder(rootdir), "boot-chains"), false) + c.Assert(err, IsNil) + c.Check(needed, Equals, false) + + otherchain := []boot.BootChain{pbc[0]} + needed, cnt, err := boot.IsResealNeeded(otherchain, filepath.Join(dirs.SnapFDEDirUnder(rootdir), "boot-chains"), false) + c.Assert(err, IsNil) + // chains are different + c.Check(needed, Equals, true) + c.Check(cnt, Equals, 3) + + // boot-chains does not exist, we cannot compare so advise to reseal + otherRootdir := c.MkDir() + needed, cnt, err = boot.IsResealNeeded(otherchain, filepath.Join(dirs.SnapFDEDirUnder(otherRootdir), "boot-chains"), false) + c.Assert(err, IsNil) + c.Check(needed, Equals, true) + c.Check(cnt, Equals, 1) + + // exists but cannot be read + c.Assert(os.Chmod(filepath.Join(dirs.SnapFDEDirUnder(rootdir), "boot-chains"), 0000), IsNil) + defer os.Chmod(filepath.Join(dirs.SnapFDEDirUnder(rootdir), "boot-chains"), 0755) + needed, _, err = boot.IsResealNeeded(otherchain, filepath.Join(dirs.SnapFDEDirUnder(rootdir), "boot-chains"), false) + c.Assert(err, ErrorMatches, "cannot open existing boot chains data file: open .*/boot-chains: permission denied") + c.Check(needed, Equals, false) + + // unrevisioned kernel chain + unrevchain := []boot.BootChain{pbc[0], pbc[1]} + unrevchain[1].KernelRevision = "" + // write on disk + bootChainsFile := filepath.Join(dirs.SnapFDEDirUnder(rootdir), "boot-chains") + err = boot.WriteBootChains(unrevchain, bootChainsFile, 2) + c.Assert(err, IsNil) + + needed, cnt, err = boot.IsResealNeeded(pbc, bootChainsFile, false) + c.Assert(err, IsNil) + c.Check(needed, Equals, true) + c.Check(cnt, Equals, 3) + + // cases falling back to expectReseal + needed, _, err = boot.IsResealNeeded(unrevchain, bootChainsFile, false) + c.Assert(err, IsNil) + c.Check(needed, Equals, false) + + needed, cnt, err = boot.IsResealNeeded(unrevchain, bootChainsFile, true) + c.Assert(err, IsNil) + c.Check(needed, Equals, true) + c.Check(cnt, Equals, 3) +} + +func (s *sealSuite) TestSealToModeenvWithFdeHookHappy(c *C) { + rootdir := c.MkDir() + dirs.SetRootDir(rootdir) + defer dirs.SetRootDir("") + + restore := boot.MockHasFDESetupHook(func() (bool, error) { + return true, nil + }) + defer restore() + + n := 0 + var runFDESetupHookParams []*boot.FDESetupHookParams + restore = boot.MockRunFDESetupHook(func(op string, params *boot.FDESetupHookParams) ([]byte, error) { + n++ + c.Assert(op, Equals, "initial-setup") + runFDESetupHookParams = append(runFDESetupHookParams, params) + return []byte("sealed-key: " + strconv.Itoa(n)), nil + }) + defer restore() + + modeenv := &boot.Modeenv{ + RecoverySystem: "20200825", + } + key := secboot.EncryptionKey{1, 2, 3, 4} + saveKey := secboot.EncryptionKey{5, 6, 7, 8} + + model := boottest.MakeMockUC20Model() + err := boot.SealKeyToModeenv(key, saveKey, model, modeenv) + c.Assert(err, IsNil) + // check that runFDESetupHook was called the expected way + c.Check(runFDESetupHookParams, DeepEquals, []*boot.FDESetupHookParams{ + {Key: secboot.EncryptionKey{1, 2, 3, 4}, Models: []*asserts.Model{model}}, + {Key: secboot.EncryptionKey{1, 2, 3, 4}, Models: []*asserts.Model{model}}, + {Key: secboot.EncryptionKey{5, 6, 7, 8}, Models: []*asserts.Model{model}}, + }) + // check that the sealed keys got written to the expected places + for i, p := range []string{ + filepath.Join(boot.InitramfsBootEncryptionKeyDir, "ubuntu-data.sealed-key"), + filepath.Join(boot.InitramfsSeedEncryptionKeyDir, "ubuntu-data.recovery.sealed-key"), + filepath.Join(boot.InitramfsSeedEncryptionKeyDir, "ubuntu-save.recovery.sealed-key"), + } { + c.Check(p, testutil.FileEquals, "sealed-key: "+strconv.Itoa(i+1)) + } + marker := filepath.Join(dirs.SnapFDEDirUnder(boot.InstallHostWritableDir), "sealed-keys") + c.Check(marker, testutil.FileEquals, "fde-setup-hook") +} + +func (s *sealSuite) TestSealToModeenvWithFdeHookSad(c *C) { + rootdir := c.MkDir() + dirs.SetRootDir(rootdir) + defer dirs.SetRootDir("") + + restore := boot.MockHasFDESetupHook(func() (bool, error) { + return true, nil + }) + defer restore() + + restore = boot.MockRunFDESetupHook(func(op string, params *boot.FDESetupHookParams) ([]byte, error) { + return nil, fmt.Errorf("hook failed") + }) + defer restore() + + modeenv := &boot.Modeenv{ + RecoverySystem: "20200825", + } + key := secboot.EncryptionKey{1, 2, 3, 4} + saveKey := secboot.EncryptionKey{5, 6, 7, 8} + + model := boottest.MakeMockUC20Model() + err := boot.SealKeyToModeenv(key, saveKey, model, modeenv) + c.Assert(err, ErrorMatches, "hook failed") + marker := filepath.Join(dirs.SnapFDEDirUnder(boot.InstallHostWritableDir), "sealed-keys") + c.Check(marker, testutil.FileAbsent) +} + +func (s *sealSuite) TestResealKeyToModeenvWithFdeHookCalled(c *C) { + rootdir := c.MkDir() + dirs.SetRootDir(rootdir) + defer dirs.SetRootDir("") + + resealKeyToModeenvUsingFDESetupHookCalled := 0 + restore := boot.MockResealKeyToModeenvUsingFDESetupHook(func(string, *asserts.Model, *boot.Modeenv, bool) error { + resealKeyToModeenvUsingFDESetupHookCalled++ + return nil + }) + defer restore() + + // TODO: this simulates that the hook is not available yet + // because of e.g. seeding. Longer term there will be + // more, see TODO in resealKeyToModeenvUsingFDESetupHookImpl + restore = boot.MockHasFDESetupHook(func() (bool, error) { + return false, fmt.Errorf("hook not available yet because e.g. seeding") + }) + defer restore() + + marker := filepath.Join(dirs.SnapFDEDirUnder(rootdir), "sealed-keys") + err := os.MkdirAll(filepath.Dir(marker), 0755) + c.Assert(err, IsNil) + err = ioutil.WriteFile(marker, []byte("fde-setup-hook"), 0644) + c.Assert(err, IsNil) + + modeenv := &boot.Modeenv{ + RecoverySystem: "20200825", + } + + model := boottest.MakeMockUC20Model() + expectReseal := false + err = boot.ResealKeyToModeenv(rootdir, model, modeenv, expectReseal) + c.Assert(err, IsNil) + c.Check(resealKeyToModeenvUsingFDESetupHookCalled, Equals, 1) +} + +func (s *sealSuite) TestResealKeyToModeenvWithFdeHookVerySad(c *C) { + rootdir := c.MkDir() + dirs.SetRootDir(rootdir) + defer dirs.SetRootDir("") + + resealKeyToModeenvUsingFDESetupHookCalled := 0 + restore := boot.MockResealKeyToModeenvUsingFDESetupHook(func(string, *asserts.Model, *boot.Modeenv, bool) error { + resealKeyToModeenvUsingFDESetupHookCalled++ + return fmt.Errorf("fde setup hook failed") + }) + defer restore() + + marker := filepath.Join(dirs.SnapFDEDirUnder(rootdir), "sealed-keys") + err := os.MkdirAll(filepath.Dir(marker), 0755) + c.Assert(err, IsNil) + err = ioutil.WriteFile(marker, []byte("fde-setup-hook"), 0644) + c.Assert(err, IsNil) + + modeenv := &boot.Modeenv{ + RecoverySystem: "20200825", + } + + model := boottest.MakeMockUC20Model() + expectReseal := false + err = boot.ResealKeyToModeenv(rootdir, model, modeenv, expectReseal) + c.Assert(err, ErrorMatches, "fde setup hook failed") + c.Check(resealKeyToModeenvUsingFDESetupHookCalled, Equals, 1) +} diff -Nru snapd-2.45.1+20.04.2/bootloader/androidboot.go snapd-2.48.3+20.04/bootloader/androidboot.go --- snapd-2.45.1+20.04.2/bootloader/androidboot.go 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/bootloader/androidboot.go 2021-02-02 08:21:12.000000000 +0000 @@ -24,7 +24,6 @@ "path/filepath" "github.com/snapcore/snapd/bootloader/androidbootenv" - "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/snap" ) @@ -33,11 +32,8 @@ } // newAndroidboot creates a new Androidboot bootloader object -func newAndroidBoot(rootdir string) Bootloader { +func newAndroidBoot(rootdir string, _ *Options) Bootloader { a := &androidboot{rootdir: rootdir} - if !osutil.FileExists(a.ConfigFile()) { - return nil - } return a } @@ -56,7 +52,7 @@ return filepath.Join(a.rootdir, "/boot/androidboot") } -func (a *androidboot) InstallBootConfig(gadgetDir string, opts *Options) (bool, error) { +func (a *androidboot) InstallBootConfig(gadgetDir string, opts *Options) error { gadgetFile := filepath.Join(gadgetDir, a.Name()+".conf") systemFile := a.ConfigFile() return genericInstallBootConfig(gadgetFile, systemFile) diff -Nru snapd-2.45.1+20.04.2/bootloader/androidboot_test.go snapd-2.48.3+20.04/bootloader/androidboot_test.go --- snapd-2.45.1+20.04.2/bootloader/androidboot_test.go 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/bootloader/androidboot_test.go 2021-02-02 08:21:12.000000000 +0000 @@ -45,11 +45,6 @@ bootloader.MockAndroidBootFile(c, s.rootdir, 0644) } -func (s *androidBootTestSuite) TestNewAndroidbootNoAndroidbootReturnsNil(c *C) { - a := bootloader.NewAndroidBoot("/something/not/there") - c.Assert(a, IsNil) -} - func (s *androidBootTestSuite) TestNewAndroidboot(c *C) { a := bootloader.NewAndroidBoot(s.rootdir) c.Assert(a, NotNil) diff -Nru snapd-2.45.1+20.04.2/bootloader/asset.go snapd-2.48.3+20.04/bootloader/asset.go --- snapd-2.45.1+20.04.2/bootloader/asset.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48.3+20.04/bootloader/asset.go 2021-02-02 08:21:12.000000000 +0000 @@ -0,0 +1,113 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2020 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 bootloader + +import ( + "bufio" + "bytes" + "errors" + "fmt" + "io" + "os" + "strconv" + "strings" + + "github.com/snapcore/snapd/bootloader/assets" +) + +var errNoEdition = errors.New("no edition") + +// editionFromDiskConfigAsset extracts the edition information from a boot +// config asset on disk. +func editionFromDiskConfigAsset(p string) (uint, error) { + f, err := os.Open(p) + if err != nil { + if os.IsNotExist(err) { + return 0, errNoEdition + } + return 0, fmt.Errorf("cannot load existing config asset: %v", err) + } + defer f.Close() + return editionFromConfigAsset(f) +} + +const editionHeader = "# Snapd-Boot-Config-Edition: " + +// editionFromConfigAsset extracts edition information from boot config asset. +func editionFromConfigAsset(asset io.Reader) (uint, error) { + scanner := bufio.NewScanner(asset) + if !scanner.Scan() { + err := fmt.Errorf("cannot read config asset: unexpected EOF") + if sErr := scanner.Err(); sErr != nil { + err = fmt.Errorf("cannot read config asset: %v", err) + } + return 0, err + } + + line := scanner.Text() + if !strings.HasPrefix(line, editionHeader) { + return 0, errNoEdition + } + + editionStr := line[len(editionHeader):] + editionStr = strings.TrimSpace(editionStr) + edition, err := strconv.ParseUint(editionStr, 10, 32) + if err != nil { + return 0, fmt.Errorf("cannot parse asset edition: %v", err) + } + return uint(edition), nil +} + +// editionFromInternalConfigAsset extracts edition information from a named +// internal boot config asset. +func editionFromInternalConfigAsset(assetName string) (uint, error) { + data := assets.Internal(assetName) + if data == nil { + return 0, fmt.Errorf("internal error: no boot asset for %q", assetName) + } + return editionFromConfigAsset(bytes.NewReader(data)) +} + +// configAsset is a boot config asset, such as text script, used by grub or +// u-boot. +type configAsset struct { + body []byte + parsedEdition uint +} + +func (g *configAsset) Edition() uint { + return g.parsedEdition +} + +func (g *configAsset) Raw() []byte { + return g.body +} + +func configAssetFrom(data []byte) (*configAsset, error) { + edition, err := editionFromConfigAsset(bytes.NewReader(data)) + if err != nil && err != errNoEdition { + return nil, err + } + gbs := &configAsset{ + body: data, + parsedEdition: edition, + } + return gbs, nil +} diff -Nru snapd-2.45.1+20.04.2/bootloader/assets/assets.go snapd-2.48.3+20.04/bootloader/assets/assets.go --- snapd-2.45.1+20.04.2/bootloader/assets/assets.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48.3+20.04/bootloader/assets/assets.go 2021-02-02 08:21:12.000000000 +0000 @@ -0,0 +1,137 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2020 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 assets + +import ( + "fmt" + "sort" + + "github.com/snapcore/snapd/osutil" +) + +var registeredAssets = map[string][]byte{} + +// ForEditions wraps a snippet that is used in editions starting with +// FirstEdition. +type ForEditions struct { + // First edition this snippet is used in + FirstEdition uint + // Snippet data + Snippet []byte +} + +var registeredEditionSnippets = map[string][]ForEditions{} + +// registerInternal registers an internal asset under the given name. +func registerInternal(name string, data []byte) { + if _, ok := registeredAssets[name]; ok { + panic(fmt.Sprintf("asset %q is already registered", name)) + } + registeredAssets[name] = data +} + +// Internal returns the content of an internal asset registered under the given +// name, or nil when none was found. +func Internal(name string) []byte { + return registeredAssets[name] +} + +type byFirstEdition []ForEditions + +func (b byFirstEdition) Len() int { return len(b) } +func (b byFirstEdition) Swap(i, j int) { b[i], b[j] = b[j], b[i] } +func (b byFirstEdition) Less(i, j int) bool { return b[i].FirstEdition < b[j].FirstEdition } + +// registerSnippetForEditions register a set of snippets, each carrying the +// first edition number it applies to, under a given key. +func registerSnippetForEditions(name string, snippets []ForEditions) { + if _, ok := registeredEditionSnippets[name]; ok { + panic(fmt.Sprintf("edition snippets %q are already registered", name)) + } + + if !sort.IsSorted(byFirstEdition(snippets)) { + panic(fmt.Sprintf("edition snippets %q must be sorted in ascending edition number order", name)) + } + for i := range snippets { + if i == 0 { + continue + } + if snippets[i-1].FirstEdition == snippets[i].FirstEdition { + panic(fmt.Sprintf(`first edition %v repeated in edition snippets %q`, + snippets[i].FirstEdition, name)) + } + } + registeredEditionSnippets[name] = snippets +} + +// SnippetForEdition returns a snippet registered under given name, +// applicable for the provided edition number. +func SnippetForEdition(name string, edition uint) []byte { + snippets := registeredEditionSnippets[name] + if snippets == nil { + return nil + } + var current []byte + // snippets are sorted by ascending edition number when adding + for _, snip := range snippets { + if edition >= snip.FirstEdition { + current = snip.Snippet + } else { + break + } + } + return current +} + +// MockInternal mocks the contents of an internal asset for use in testing. +func MockInternal(name string, data []byte) (restore func()) { + osutil.MustBeTestBinary("mocking can be done only in tests") + + old, ok := registeredAssets[name] + registeredAssets[name] = data + return func() { + if ok { + registeredAssets[name] = old + } else { + delete(registeredAssets, name) + } + } +} + +// MockSnippetsForEdition mocks the contents of per-edition snippets. +func MockSnippetsForEdition(name string, snippets []ForEditions) (restore func()) { + osutil.MustBeTestBinary("mocking can be done only in tests") + + old, ok := registeredEditionSnippets[name] + snippetsCopy := make([]ForEditions, len(snippets)) + copy(snippetsCopy, snippets) + if ok { + delete(registeredEditionSnippets, name) + } + registerSnippetForEditions(name, snippetsCopy) + + return func() { + if ok { + registeredEditionSnippets[name] = old + } else { + delete(registeredAssets, name) + } + } +} diff -Nru snapd-2.45.1+20.04.2/bootloader/assets/assets_test.go snapd-2.48.3+20.04/bootloader/assets/assets_test.go --- snapd-2.45.1+20.04.2/bootloader/assets/assets_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48.3+20.04/bootloader/assets/assets_test.go 2021-02-02 08:21:12.000000000 +0000 @@ -0,0 +1,156 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2020 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 assets_test + +import ( + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/bootloader/assets" + "github.com/snapcore/snapd/testutil" +) + +type assetsTestSuite struct { + testutil.BaseTest +} + +var _ = Suite(&assetsTestSuite{}) + +func (s *assetsTestSuite) SetUpTest(c *C) { + s.BaseTest.SetUpTest(c) + s.AddCleanup(assets.MockCleanState()) +} + +func (s *assetsTestSuite) TestRegisterInternalSimple(c *C) { + assets.RegisterInternal("foo", []byte("bar")) + data := assets.Internal("foo") + c.Check(data, DeepEquals, []byte("bar")) + + complexData := `this is "some +complex binary " data +` + assets.RegisterInternal("complex-data", []byte(complexData)) + complex := assets.Internal("complex-data") + c.Check(complex, DeepEquals, []byte(complexData)) + + nodata := assets.Internal("no data") + c.Check(nodata, IsNil) +} + +func (s *assetsTestSuite) TestRegisterDoublePanics(c *C) { + assets.RegisterInternal("foo", []byte("foo")) + // panics with the same key, no matter the data used + c.Assert(func() { assets.RegisterInternal("foo", []byte("bar")) }, + PanicMatches, `asset "foo" is already registered`) + c.Assert(func() { assets.RegisterInternal("foo", []byte("foo")) }, + PanicMatches, `asset "foo" is already registered`) +} + +func (s *assetsTestSuite) TestRegisterSnippetPanics(c *C) { + assets.RegisterSnippetForEditions("foo", []assets.ForEditions{ + {FirstEdition: 1, Snippet: []byte("foo")}, + }) + // panics with the same key + c.Assert(func() { + assets.RegisterSnippetForEditions("foo", []assets.ForEditions{ + {FirstEdition: 2, Snippet: []byte("bar")}, + }) + }, PanicMatches, `edition snippets "foo" are already registered`) + // panics when snippets aren't sorted + c.Assert(func() { + assets.RegisterSnippetForEditions("unsorted", []assets.ForEditions{ + {FirstEdition: 2, Snippet: []byte("two")}, + {FirstEdition: 1, Snippet: []byte("one")}, + }) + }, PanicMatches, `edition snippets "unsorted" must be sorted in ascending edition number order`) + // panics when edition is repeated + c.Assert(func() { + assets.RegisterSnippetForEditions("doubled edition", []assets.ForEditions{ + {FirstEdition: 1, Snippet: []byte("one")}, + {FirstEdition: 2, Snippet: []byte("two")}, + {FirstEdition: 3, Snippet: []byte("three")}, + {FirstEdition: 3, Snippet: []byte("more tree")}, + {FirstEdition: 4, Snippet: []byte("four")}, + }) + }, PanicMatches, `first edition 3 repeated in edition snippets "doubled edition"`) + // mix unsorted with duplicate edition + c.Assert(func() { + assets.RegisterSnippetForEditions("unsorted and doubled edition", []assets.ForEditions{ + {FirstEdition: 1, Snippet: []byte("one")}, + {FirstEdition: 2, Snippet: []byte("two")}, + {FirstEdition: 1, Snippet: []byte("one again")}, + {FirstEdition: 3, Snippet: []byte("more tree")}, + {FirstEdition: 4, Snippet: []byte("four")}, + }) + }, PanicMatches, `edition snippets "unsorted and doubled edition" must be sorted in ascending edition number order`) +} + +func (s *assetsTestSuite) TestEditionSnippets(c *C) { + assets.RegisterSnippetForEditions("foo", []assets.ForEditions{ + {FirstEdition: 1, Snippet: []byte("one")}, + {FirstEdition: 2, Snippet: []byte("two")}, + {FirstEdition: 3, Snippet: []byte("three")}, + {FirstEdition: 4, Snippet: []byte("four")}, + {FirstEdition: 10, Snippet: []byte("ten")}, + {FirstEdition: 20, Snippet: []byte("twenty")}, + }) + assets.RegisterSnippetForEditions("bar", []assets.ForEditions{ + {FirstEdition: 1, Snippet: []byte("bar one")}, + {FirstEdition: 3, Snippet: []byte("bar three")}, + // same as 3 + {FirstEdition: 5, Snippet: []byte("bar three")}, + }) + assets.RegisterSnippetForEditions("just-one", []assets.ForEditions{ + {FirstEdition: 2, Snippet: []byte("just one")}, + }) + + for _, tc := range []struct { + asset string + edition uint + exp []byte + }{ + {"foo", 1, []byte("one")}, + {"foo", 4, []byte("four")}, + {"foo", 10, []byte("ten")}, + // still using snipped from edition 4 + {"foo", 9, []byte("four")}, + // still using snipped from edition 10 + {"foo", 11, []byte("ten")}, + {"foo", 30, []byte("twenty")}, + // different asset + {"bar", 1, []byte("bar one")}, + {"bar", 2, []byte("bar one")}, + {"bar", 3, []byte("bar three")}, + {"bar", 4, []byte("bar three")}, + {"bar", 5, []byte("bar three")}, + {"bar", 6, []byte("bar three")}, + // nothing registered for edition 0 + {"bar", 0, nil}, + // a single snippet under this key + {"just-one", 2, []byte("just one")}, + {"just-one", 1, nil}, + // asset not registered + {"no asset", 1, nil}, + {"no asset", 100, nil}, + } { + c.Logf("%q edition %v", tc.asset, tc.edition) + snippet := assets.SnippetForEdition(tc.asset, tc.edition) + c.Check(snippet, DeepEquals, tc.exp) + } +} diff -Nru snapd-2.45.1+20.04.2/bootloader/assets/data/grub.cfg snapd-2.48.3+20.04/bootloader/assets/data/grub.cfg --- snapd-2.45.1+20.04.2/bootloader/assets/data/grub.cfg 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48.3+20.04/bootloader/assets/data/grub.cfg 2021-02-02 08:21:12.000000000 +0000 @@ -0,0 +1,44 @@ +# Snapd-Boot-Config-Edition: 1 + +set default=0 +set timeout=3 +set timeout_style=hidden + +# load only kernel_status from the bootenv +load_env --file /EFI/ubuntu/grubenv kernel_status snapd_extra_cmdline_args + +set snapd_static_cmdline_args='console=ttyS0 console=tty1 panic=-1' + +set kernel=kernel.efi + +if [ "$kernel_status" = "try" ]; then + # a new kernel got installed + set kernel_status="trying" + save_env kernel_status + + # use try-kernel.efi + set kernel=try-kernel.efi +elif [ "$kernel_status" = "trying" ]; then + # nothing cleared the "trying snap" so the boot failed + # we clear the mode and boot normally + set kernel_status="" + save_env kernel_status +elif [ -n "$kernel_status" ]; then + # ERROR invalid kernel_status state, reset to empty + echo "invalid kernel_status!!!" + echo "resetting to empty" + set kernel_status="" + 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 $snapd_static_cmdline_args $snapd_extra_cmdline_args +} +else + # nothing to boot :-/ + echo "missing kernel at $prefix/$kernel!" +fi diff -Nru snapd-2.45.1+20.04.2/bootloader/assets/data/grub-recovery.cfg snapd-2.48.3+20.04/bootloader/assets/data/grub-recovery.cfg --- snapd-2.45.1+20.04.2/bootloader/assets/data/grub-recovery.cfg 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48.3+20.04/bootloader/assets/data/grub-recovery.cfg 2021-02-02 08:21:12.000000000 +0000 @@ -0,0 +1,63 @@ +# Snapd-Boot-Config-Edition: 1 + +set default=0 +set timeout=3 +set timeout_style=hidden + +if [ -e /EFI/ubuntu/grubenv ]; then + load_env --file /EFI/ubuntu/grubenv snapd_recovery_mode snapd_recovery_system +fi + +# standard cmdline params +set snapd_static_cmdline_args='console=ttyS0 console=tty1 panic=-1' + +# if no default boot mode set, pick one +if [ -z "$snapd_recovery_mode" ]; then + set snapd_recovery_mode=install +fi + +if [ "$snapd_recovery_mode" = "run" ]; then + default="run" +elif [ -n "$snapd_recovery_system" ]; then + default=$snapd_recovery_mode-$snapd_recovery_system +fi + +search --no-floppy --set=boot_fs --label ubuntu-boot + +if [ -n "$boot_fs" ]; then + menuentry "Continue to run mode" --hotkey=n --id=run { + chainloader ($boot_fs)/EFI/boot/grubx64.efi + } +fi + +# globbing in grub does not sort +for label in /systems/*; do + regexp --set 1:label "/([0-9]*)\$" "$label" + if [ -z "$label" ]; then + continue + fi + # yes, you need to backslash that less-than + if [ -z "$best" -o "$label" \< "$best" ]; then + set best="$label" + fi + # if grubenv did not pick mode-system, use best one + if [ -z "$snapd_recovery_system" ]; then + default=$snapd_recovery_mode-$best + fi + set snapd_recovery_kernel= + load_env --file /systems/$label/grubenv snapd_recovery_kernel snapd_extra_cmdline_args + + # We could "source /systems/$snapd_recovery_system/grub.cfg" here as well + menuentry "Recover using $label" --hotkey=r --id=recover-$label $snapd_recovery_kernel recover $label { + loopback loop $2 + chainloader (loop)/kernel.efi snapd_recovery_mode=$3 snapd_recovery_system=$4 $snapd_static_cmdline_args $snapd_extra_cmdline_args + } + menuentry "Install using $label" --hotkey=i --id=install-$label $snapd_recovery_kernel install $label { + loopback loop $2 + chainloader (loop)/kernel.efi snapd_recovery_mode=$3 snapd_recovery_system=$4 $snapd_static_cmdline_args $snapd_extra_cmdline_args + } +done + +menuentry 'UEFI Firmware Settings' --hotkey=f 'uefi-firmware' { + fwsetup +} diff -Nru snapd-2.45.1+20.04.2/bootloader/assets/data/README.grub snapd-2.48.3+20.04/bootloader/assets/data/README.grub --- snapd-2.45.1+20.04.2/bootloader/assets/data/README.grub 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48.3+20.04/bootloader/assets/data/README.grub 2021-02-02 08:21:12.000000000 +0000 @@ -0,0 +1,10 @@ +Edition 1 of grub.cfg and grub-recovery.cfg imported from https://github.com/snapcore/pc-amd64-gadget, commit: + +commit e4d63119322691f14a3f9dfa36a3a075e941ec9d (HEAD -> 20, origin/HEAD, origin/20) +Merge: b70d2ae d113aca +Author: Dimitri John Ledkov +Date: Thu May 7 19:30:00 2020 +0100 + + Merge pull request #47 from xnox/production-keys + + gadget: bump edition to 2, using production signing keys for everything. diff -Nru snapd-2.45.1+20.04.2/bootloader/assets/export_test.go snapd-2.48.3+20.04/bootloader/assets/export_test.go --- snapd-2.45.1+20.04.2/bootloader/assets/export_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48.3+20.04/bootloader/assets/export_test.go 2021-02-02 08:21:12.000000000 +0000 @@ -0,0 +1,36 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2020 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 assets + +var ( + RegisterInternal = registerInternal + RegisterSnippetForEditions = registerSnippetForEditions +) + +func MockCleanState() (restore func()) { + oldRegisteredAssets := registeredAssets + oldRegisteredEditionAssets := registeredEditionSnippets + registeredAssets = map[string][]byte{} + registeredEditionSnippets = map[string][]ForEditions{} + return func() { + registeredAssets = oldRegisteredAssets + registeredEditionSnippets = oldRegisteredEditionAssets + } +} diff -Nru snapd-2.45.1+20.04.2/bootloader/assets/genasset/export_test.go snapd-2.48.3+20.04/bootloader/assets/genasset/export_test.go --- snapd-2.45.1+20.04.2/bootloader/assets/genasset/export_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48.3+20.04/bootloader/assets/genasset/export_test.go 2021-02-02 08:21:12.000000000 +0000 @@ -0,0 +1,32 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2020 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 ( + ParseArgs = parseArgs + Run = run + FormatLines = formatLines +) + +func ResetArgs() { + *inFile = "" + *outFile = "" + *assetName = "" +} diff -Nru snapd-2.45.1+20.04.2/bootloader/assets/genasset/main.go snapd-2.48.3+20.04/bootloader/assets/genasset/main.go --- snapd-2.45.1+20.04.2/bootloader/assets/genasset/main.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48.3+20.04/bootloader/assets/genasset/main.go 2021-02-02 08:21:12.000000000 +0000 @@ -0,0 +1,157 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2020 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 ( + "bytes" + "flag" + "fmt" + "io" + "os" + "strconv" + "text/template" + "time" + + "github.com/snapcore/snapd/osutil" +) + +var assetTemplateText = `// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) {{ .Year }} 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 assets + +// Code generated from {{ .InputFileName }} DO NOT EDIT + +func init() { + registerInternal("{{ .AssetName }}", []byte{ +{{ range .AssetDataLines }} {{ . }} +{{ end }} }) +} +` + +var inFile = flag.String("in", "", "asset input file") +var outFile = flag.String("out", "", "asset output file") +var assetName = flag.String("name", "", "asset name") +var assetTemplate = template.Must(template.New("asset").Parse(assetTemplateText)) + +// formatLines generates a list of strings, each carrying a line containing hex +// encoded data +func formatLines(data []byte) []string { + const lineBreak = 16 + + l := len(data) + lines := make([]string, 0, l/lineBreak+1) + for i := 0; i < l; i = i + lineBreak { + end := i + lineBreak + start := i + if end > l { + end = l + } + var line bytes.Buffer + forLine := data[start:end] + for idx, val := range forLine { + line.WriteString(fmt.Sprintf("0x%02x,", val)) + if idx != len(forLine)-1 { + line.WriteRune(' ') + } + } + lines = append(lines, line.String()) + } + return lines +} + +func run(assetName, inputFile, outputFile string) error { + inf, err := os.Open(inputFile) + if err != nil { + return fmt.Errorf("cannot open input file: %v", err) + } + defer inf.Close() + + var inData bytes.Buffer + if _, err := io.Copy(&inData, inf); err != nil { + return fmt.Errorf("cannot copy input data: %v", err) + } + + outf, err := osutil.NewAtomicFile(outputFile, 0644, 0, osutil.NoChown, osutil.NoChown) + if err != nil { + return fmt.Errorf("cannot open output file: %v", err) + } + defer outf.Cancel() + + templateData := struct { + Comment string + InputFileName string + AssetName string + AssetDataLines []string + Year string + }{ + InputFileName: inputFile, + // dealing with precise formatting in template is annoying thus + // we use a preformatted lines carrying asset data + AssetDataLines: formatLines(inData.Bytes()), + AssetName: assetName, + Year: strconv.Itoa(time.Now().Year()), + } + if err := assetTemplate.Execute(outf, &templateData); err != nil { + return fmt.Errorf("cannot generate content: %v", err) + } + return outf.Commit() +} + +func parseArgs() error { + flag.Parse() + if *inFile == "" { + return fmt.Errorf("input file not provided") + } + if *outFile == "" { + return fmt.Errorf("output file not provided") + } + if *assetName == "" { + return fmt.Errorf("asset name not provided") + } + return nil +} + +func main() { + if err := parseArgs(); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + if err := run(*assetName, *inFile, *outFile); err != nil { + fmt.Fprintf(os.Stderr, "error: %v\n", err) + os.Exit(1) + } +} diff -Nru snapd-2.45.1+20.04.2/bootloader/assets/genasset/main_test.go snapd-2.48.3+20.04/bootloader/assets/genasset/main_test.go --- snapd-2.45.1+20.04.2/bootloader/assets/genasset/main_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48.3+20.04/bootloader/assets/genasset/main_test.go 2021-02-02 08:21:12.000000000 +0000 @@ -0,0 +1,176 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2020 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" + "os/exec" + "path/filepath" + "testing" + "time" + + . "gopkg.in/check.v1" + + generate "github.com/snapcore/snapd/bootloader/assets/genasset" + "github.com/snapcore/snapd/testutil" +) + +// Hook up check.v1 into the "go test" runner +func Test(t *testing.T) { TestingT(t) } + +type generateAssetsTestSuite struct { + testutil.BaseTest +} + +var _ = Suite(&generateAssetsTestSuite{}) + +func (s *generateAssetsTestSuite) SetUpTest(c *C) { + s.BaseTest.SetUpTest(c) +} + +func mockArgs(args []string) (restore func()) { + old := os.Args + os.Args = args + return func() { + os.Args = old + } +} + +func (s *generateAssetsTestSuite) TestArgs(c *C) { + generate.ResetArgs() + restore := mockArgs([]string{"self", "-in", "ok", "-out", "ok", "-name", "assetname"}) + defer restore() + c.Assert(generate.ParseArgs(), IsNil) + // no input file + generate.ResetArgs() + restore = mockArgs([]string{"self", "-out", "ok", "-name", "assetname"}) + defer restore() + c.Assert(generate.ParseArgs(), ErrorMatches, "input file not provided") + // no output file + restore = mockArgs([]string{"self", "-in", "in", "-name", "assetname"}) + defer restore() + generate.ResetArgs() + c.Assert(generate.ParseArgs(), ErrorMatches, "output file not provided") + // no name + generate.ResetArgs() + restore = mockArgs([]string{"self", "-in", "in", "-out", "out"}) + defer restore() + c.Assert(generate.ParseArgs(), ErrorMatches, "asset name not provided") +} + +func (s *generateAssetsTestSuite) TestSimpleAsset(c *C) { + d := c.MkDir() + err := ioutil.WriteFile(filepath.Join(d, "in"), []byte("this is a\n"+ + "multiline asset \"'``\nwith chars\n"), 0644) + c.Assert(err, IsNil) + err = generate.Run("asset-name", filepath.Join(d, "in"), filepath.Join(d, "out")) + c.Assert(err, IsNil) + data, err := ioutil.ReadFile(filepath.Join(d, "out")) + c.Assert(err, IsNil) + + const exp = `// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) %d 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 assets + +// Code generated from %s DO NOT EDIT + +func init() { + registerInternal("asset-name", []byte{ + 0x74, 0x68, 0x69, 0x73, 0x20, 0x69, 0x73, 0x20, 0x61, 0x0a, 0x6d, 0x75, 0x6c, 0x74, 0x69, 0x6c, + 0x69, 0x6e, 0x65, 0x20, 0x61, 0x73, 0x73, 0x65, 0x74, 0x20, 0x22, 0x27, 0x60, 0x60, 0x0a, 0x77, + 0x69, 0x74, 0x68, 0x20, 0x63, 0x68, 0x61, 0x72, 0x73, 0x0a, + }) +} +` + c.Check(string(data), Equals, fmt.Sprintf(exp, time.Now().Year(), filepath.Join(d, "in"))) +} + +func (s *generateAssetsTestSuite) TestGoFmtClean(c *C) { + _, err := exec.LookPath("gofmt") + if err != nil { + c.Skip("gofmt is missing") + } + + d := c.MkDir() + err = ioutil.WriteFile(filepath.Join(d, "in"), []byte("this is a\n"+ + "multiline asset \"'``\nuneven chars\n"), 0644) + c.Assert(err, IsNil) + err = generate.Run("asset-name", filepath.Join(d, "in"), filepath.Join(d, "out")) + c.Assert(err, IsNil) + + cmd := exec.Command("gofmt", "-l", "-d", filepath.Join(d, "out")) + out, err := cmd.CombinedOutput() + c.Assert(err, IsNil) + c.Assert(out, HasLen, 0, Commentf("output file is not gofmt clean: %s", string(out))) +} + +func (s *generateAssetsTestSuite) TestRunErrors(c *C) { + d := c.MkDir() + err := generate.Run("asset-name", filepath.Join(d, "missing"), filepath.Join(d, "out")) + c.Assert(err, ErrorMatches, "cannot open input file: open .*/missing: no such file or directory") + + err = ioutil.WriteFile(filepath.Join(d, "in"), []byte("this is a\n"+ + "multiline asset \"'``\nuneven chars\n"), 0644) + c.Assert(err, IsNil) + + err = generate.Run("asset-name", filepath.Join(d, "in"), filepath.Join(d, "does-not-exist", "out")) + c.Assert(err, ErrorMatches, `cannot open output file: open .*/does-not-exist/out\..*: no such file or directory`) + +} + +func (s *generateAssetsTestSuite) TestFormatLines(c *C) { + out := generate.FormatLines(bytes.Repeat([]byte{1}, 12)) + c.Check(out, DeepEquals, []string{ + "0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,", + }) + out = generate.FormatLines(bytes.Repeat([]byte{1}, 16)) + c.Check(out, DeepEquals, []string{ + "0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,", + }) + out = generate.FormatLines(bytes.Repeat([]byte{1}, 17)) + c.Check(out, DeepEquals, []string{ + "0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,", + "0x01,", + }) + out = generate.FormatLines(bytes.Repeat([]byte{1}, 1)) + c.Check(out, DeepEquals, []string{ + "0x01,", + }) +} diff -Nru snapd-2.45.1+20.04.2/bootloader/assets/generate.go snapd-2.48.3+20.04/bootloader/assets/generate.go --- snapd-2.45.1+20.04.2/bootloader/assets/generate.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48.3+20.04/bootloader/assets/generate.go 2021-02-02 08:21:12.000000000 +0000 @@ -0,0 +1,23 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2020 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 assets + +//go:generate go run ./genasset/main.go -name grub.cfg -in ./data/grub.cfg -out ./grub_cfg_asset.go +//go:generate go run ./genasset/main.go -name grub-recovery.cfg -in ./data/grub-recovery.cfg -out ./grub_recovery_cfg_asset.go diff -Nru snapd-2.45.1+20.04.2/bootloader/assets/grub_cfg_asset.go snapd-2.48.3+20.04/bootloader/assets/grub_cfg_asset.go --- snapd-2.45.1+20.04.2/bootloader/assets/grub_cfg_asset.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48.3+20.04/bootloader/assets/grub_cfg_asset.go 2021-02-02 08:21:12.000000000 +0000 @@ -0,0 +1,110 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2020 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 assets + +// Code generated from ./data/grub.cfg DO NOT EDIT + +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, + 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, + 0x64, 0x64, 0x65, 0x6e, 0x0a, 0x0a, 0x23, 0x20, 0x6c, 0x6f, 0x61, 0x64, 0x20, 0x6f, 0x6e, 0x6c, + 0x79, 0x20, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x20, + 0x66, 0x72, 0x6f, 0x6d, 0x20, 0x74, 0x68, 0x65, 0x20, 0x62, 0x6f, 0x6f, 0x74, 0x65, 0x6e, 0x76, + 0x0a, 0x6c, 0x6f, 0x61, 0x64, 0x5f, 0x65, 0x6e, 0x76, 0x20, 0x2d, 0x2d, 0x66, 0x69, 0x6c, 0x65, + 0x20, 0x2f, 0x45, 0x46, 0x49, 0x2f, 0x75, 0x62, 0x75, 0x6e, 0x74, 0x75, 0x2f, 0x67, 0x72, 0x75, + 0x62, 0x65, 0x6e, 0x76, 0x20, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x5f, 0x73, 0x74, 0x61, 0x74, + 0x75, 0x73, 0x20, 0x73, 0x6e, 0x61, 0x70, 0x64, 0x5f, 0x65, 0x78, 0x74, 0x72, 0x61, 0x5f, 0x63, + 0x6d, 0x64, 0x6c, 0x69, 0x6e, 0x65, 0x5f, 0x61, 0x72, 0x67, 0x73, 0x0a, 0x0a, 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, 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, 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, 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, + }) +} diff -Nru snapd-2.45.1+20.04.2/bootloader/assets/grub.go snapd-2.48.3+20.04/bootloader/assets/grub.go --- snapd-2.45.1+20.04.2/bootloader/assets/grub.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48.3+20.04/bootloader/assets/grub.go 2021-02-02 08:21:12.000000000 +0000 @@ -0,0 +1,29 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2020 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 assets + +func init() { + registerSnippetForEditions("grub.cfg:static-cmdline", []ForEditions{ + {FirstEdition: 1, Snippet: []byte("console=ttyS0 console=tty1 panic=-1")}, + }) + registerSnippetForEditions("grub-recovery.cfg:static-cmdline", []ForEditions{ + {FirstEdition: 1, Snippet: []byte("console=ttyS0 console=tty1 panic=-1")}, + }) +} diff -Nru snapd-2.45.1+20.04.2/bootloader/assets/grub_recovery_cfg_asset.go snapd-2.48.3+20.04/bootloader/assets/grub_recovery_cfg_asset.go --- snapd-2.45.1+20.04.2/bootloader/assets/grub_recovery_cfg_asset.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48.3+20.04/bootloader/assets/grub_recovery_cfg_asset.go 2021-02-02 08:21:12.000000000 +0000 @@ -0,0 +1,157 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2020 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 assets + +// Code generated from ./data/grub-recovery.cfg DO NOT EDIT + +func init() { + registerInternal("grub-recovery.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, + 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, + 0x64, 0x64, 0x65, 0x6e, 0x0a, 0x0a, 0x69, 0x66, 0x20, 0x5b, 0x20, 0x2d, 0x65, 0x20, 0x2f, 0x45, + 0x46, 0x49, 0x2f, 0x75, 0x62, 0x75, 0x6e, 0x74, 0x75, 0x2f, 0x67, 0x72, 0x75, 0x62, 0x65, 0x6e, + 0x76, 0x20, 0x5d, 0x3b, 0x20, 0x74, 0x68, 0x65, 0x6e, 0x0a, 0x20, 0x20, 0x20, 0x6c, 0x6f, 0x61, + 0x64, 0x5f, 0x65, 0x6e, 0x76, 0x20, 0x2d, 0x2d, 0x66, 0x69, 0x6c, 0x65, 0x20, 0x2f, 0x45, 0x46, + 0x49, 0x2f, 0x75, 0x62, 0x75, 0x6e, 0x74, 0x75, 0x2f, 0x67, 0x72, 0x75, 0x62, 0x65, 0x6e, 0x76, + 0x20, 0x73, 0x6e, 0x61, 0x70, 0x64, 0x5f, 0x72, 0x65, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x79, 0x5f, + 0x6d, 0x6f, 0x64, 0x65, 0x20, 0x73, 0x6e, 0x61, 0x70, 0x64, 0x5f, 0x72, 0x65, 0x63, 0x6f, 0x76, + 0x65, 0x72, 0x79, 0x5f, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x0a, 0x66, 0x69, 0x0a, 0x0a, 0x23, + 0x20, 0x73, 0x74, 0x61, 0x6e, 0x64, 0x61, 0x72, 0x64, 0x20, 0x63, 0x6d, 0x64, 0x6c, 0x69, 0x6e, + 0x65, 0x20, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x73, 0x0a, 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, 0x0a, 0x23, 0x20, + 0x69, 0x66, 0x20, 0x6e, 0x6f, 0x20, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x20, 0x62, 0x6f, + 0x6f, 0x74, 0x20, 0x6d, 0x6f, 0x64, 0x65, 0x20, 0x73, 0x65, 0x74, 0x2c, 0x20, 0x70, 0x69, 0x63, + 0x6b, 0x20, 0x6f, 0x6e, 0x65, 0x0a, 0x69, 0x66, 0x20, 0x5b, 0x20, 0x2d, 0x7a, 0x20, 0x22, 0x24, + 0x73, 0x6e, 0x61, 0x70, 0x64, 0x5f, 0x72, 0x65, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x79, 0x5f, 0x6d, + 0x6f, 0x64, 0x65, 0x22, 0x20, 0x5d, 0x3b, 0x20, 0x74, 0x68, 0x65, 0x6e, 0x0a, 0x20, 0x20, 0x20, + 0x20, 0x73, 0x65, 0x74, 0x20, 0x73, 0x6e, 0x61, 0x70, 0x64, 0x5f, 0x72, 0x65, 0x63, 0x6f, 0x76, + 0x65, 0x72, 0x79, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x3d, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, + 0x0a, 0x66, 0x69, 0x0a, 0x0a, 0x69, 0x66, 0x20, 0x5b, 0x20, 0x22, 0x24, 0x73, 0x6e, 0x61, 0x70, + 0x64, 0x5f, 0x72, 0x65, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x79, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x22, + 0x20, 0x3d, 0x20, 0x22, 0x72, 0x75, 0x6e, 0x22, 0x20, 0x5d, 0x3b, 0x20, 0x74, 0x68, 0x65, 0x6e, + 0x0a, 0x20, 0x20, 0x20, 0x20, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x3d, 0x22, 0x72, 0x75, + 0x6e, 0x22, 0x0a, 0x65, 0x6c, 0x69, 0x66, 0x20, 0x5b, 0x20, 0x2d, 0x6e, 0x20, 0x22, 0x24, 0x73, + 0x6e, 0x61, 0x70, 0x64, 0x5f, 0x72, 0x65, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x79, 0x5f, 0x73, 0x79, + 0x73, 0x74, 0x65, 0x6d, 0x22, 0x20, 0x5d, 0x3b, 0x20, 0x74, 0x68, 0x65, 0x6e, 0x0a, 0x20, 0x20, + 0x20, 0x20, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x3d, 0x24, 0x73, 0x6e, 0x61, 0x70, 0x64, + 0x5f, 0x72, 0x65, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x79, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x2d, 0x24, + 0x73, 0x6e, 0x61, 0x70, 0x64, 0x5f, 0x72, 0x65, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x79, 0x5f, 0x73, + 0x79, 0x73, 0x74, 0x65, 0x6d, 0x0a, 0x66, 0x69, 0x0a, 0x0a, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, + 0x20, 0x2d, 0x2d, 0x6e, 0x6f, 0x2d, 0x66, 0x6c, 0x6f, 0x70, 0x70, 0x79, 0x20, 0x2d, 0x2d, 0x73, + 0x65, 0x74, 0x3d, 0x62, 0x6f, 0x6f, 0x74, 0x5f, 0x66, 0x73, 0x20, 0x2d, 0x2d, 0x6c, 0x61, 0x62, + 0x65, 0x6c, 0x20, 0x75, 0x62, 0x75, 0x6e, 0x74, 0x75, 0x2d, 0x62, 0x6f, 0x6f, 0x74, 0x0a, 0x0a, + 0x69, 0x66, 0x20, 0x5b, 0x20, 0x2d, 0x6e, 0x20, 0x22, 0x24, 0x62, 0x6f, 0x6f, 0x74, 0x5f, 0x66, + 0x73, 0x22, 0x20, 0x5d, 0x3b, 0x20, 0x74, 0x68, 0x65, 0x6e, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x6d, + 0x65, 0x6e, 0x75, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x20, 0x22, 0x43, 0x6f, 0x6e, 0x74, 0x69, 0x6e, + 0x75, 0x65, 0x20, 0x74, 0x6f, 0x20, 0x72, 0x75, 0x6e, 0x20, 0x6d, 0x6f, 0x64, 0x65, 0x22, 0x20, + 0x2d, 0x2d, 0x68, 0x6f, 0x74, 0x6b, 0x65, 0x79, 0x3d, 0x6e, 0x20, 0x2d, 0x2d, 0x69, 0x64, 0x3d, + 0x72, 0x75, 0x6e, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x63, 0x68, + 0x61, 0x69, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x65, 0x72, 0x20, 0x28, 0x24, 0x62, 0x6f, 0x6f, 0x74, + 0x5f, 0x66, 0x73, 0x29, 0x2f, 0x45, 0x46, 0x49, 0x2f, 0x62, 0x6f, 0x6f, 0x74, 0x2f, 0x67, 0x72, + 0x75, 0x62, 0x78, 0x36, 0x34, 0x2e, 0x65, 0x66, 0x69, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x0a, + 0x66, 0x69, 0x0a, 0x0a, 0x23, 0x20, 0x67, 0x6c, 0x6f, 0x62, 0x62, 0x69, 0x6e, 0x67, 0x20, 0x69, + 0x6e, 0x20, 0x67, 0x72, 0x75, 0x62, 0x20, 0x64, 0x6f, 0x65, 0x73, 0x20, 0x6e, 0x6f, 0x74, 0x20, + 0x73, 0x6f, 0x72, 0x74, 0x0a, 0x66, 0x6f, 0x72, 0x20, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x20, 0x69, + 0x6e, 0x20, 0x2f, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x73, 0x2f, 0x2a, 0x3b, 0x20, 0x64, 0x6f, + 0x0a, 0x20, 0x20, 0x20, 0x20, 0x72, 0x65, 0x67, 0x65, 0x78, 0x70, 0x20, 0x2d, 0x2d, 0x73, 0x65, + 0x74, 0x20, 0x31, 0x3a, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x20, 0x22, 0x2f, 0x28, 0x5b, 0x30, 0x2d, + 0x39, 0x5d, 0x2a, 0x29, 0x5c, 0x24, 0x22, 0x20, 0x22, 0x24, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x22, + 0x0a, 0x20, 0x20, 0x20, 0x20, 0x69, 0x66, 0x20, 0x5b, 0x20, 0x2d, 0x7a, 0x20, 0x22, 0x24, 0x6c, + 0x61, 0x62, 0x65, 0x6c, 0x22, 0x20, 0x5d, 0x3b, 0x20, 0x74, 0x68, 0x65, 0x6e, 0x0a, 0x20, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x63, 0x6f, 0x6e, 0x74, 0x69, 0x6e, 0x75, 0x65, 0x0a, 0x20, + 0x20, 0x20, 0x20, 0x66, 0x69, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x23, 0x20, 0x79, 0x65, 0x73, 0x2c, + 0x20, 0x79, 0x6f, 0x75, 0x20, 0x6e, 0x65, 0x65, 0x64, 0x20, 0x74, 0x6f, 0x20, 0x62, 0x61, 0x63, + 0x6b, 0x73, 0x6c, 0x61, 0x73, 0x68, 0x20, 0x74, 0x68, 0x61, 0x74, 0x20, 0x6c, 0x65, 0x73, 0x73, + 0x2d, 0x74, 0x68, 0x61, 0x6e, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x69, 0x66, 0x20, 0x5b, 0x20, 0x2d, + 0x7a, 0x20, 0x22, 0x24, 0x62, 0x65, 0x73, 0x74, 0x22, 0x20, 0x2d, 0x6f, 0x20, 0x22, 0x24, 0x6c, + 0x61, 0x62, 0x65, 0x6c, 0x22, 0x20, 0x5c, 0x3c, 0x20, 0x22, 0x24, 0x62, 0x65, 0x73, 0x74, 0x22, + 0x20, 0x5d, 0x3b, 0x20, 0x74, 0x68, 0x65, 0x6e, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, + 0x20, 0x73, 0x65, 0x74, 0x20, 0x62, 0x65, 0x73, 0x74, 0x3d, 0x22, 0x24, 0x6c, 0x61, 0x62, 0x65, + 0x6c, 0x22, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x66, 0x69, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x23, 0x20, + 0x69, 0x66, 0x20, 0x67, 0x72, 0x75, 0x62, 0x65, 0x6e, 0x76, 0x20, 0x64, 0x69, 0x64, 0x20, 0x6e, + 0x6f, 0x74, 0x20, 0x70, 0x69, 0x63, 0x6b, 0x20, 0x6d, 0x6f, 0x64, 0x65, 0x2d, 0x73, 0x79, 0x73, + 0x74, 0x65, 0x6d, 0x2c, 0x20, 0x75, 0x73, 0x65, 0x20, 0x62, 0x65, 0x73, 0x74, 0x20, 0x6f, 0x6e, + 0x65, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x69, 0x66, 0x20, 0x5b, 0x20, 0x2d, 0x7a, 0x20, 0x22, 0x24, + 0x73, 0x6e, 0x61, 0x70, 0x64, 0x5f, 0x72, 0x65, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x79, 0x5f, 0x73, + 0x79, 0x73, 0x74, 0x65, 0x6d, 0x22, 0x20, 0x5d, 0x3b, 0x20, 0x74, 0x68, 0x65, 0x6e, 0x0a, 0x20, + 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x3d, 0x24, + 0x73, 0x6e, 0x61, 0x70, 0x64, 0x5f, 0x72, 0x65, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x79, 0x5f, 0x6d, + 0x6f, 0x64, 0x65, 0x2d, 0x24, 0x62, 0x65, 0x73, 0x74, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x66, 0x69, + 0x0a, 0x20, 0x20, 0x20, 0x20, 0x73, 0x65, 0x74, 0x20, 0x73, 0x6e, 0x61, 0x70, 0x64, 0x5f, 0x72, + 0x65, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x79, 0x5f, 0x6b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x3d, 0x0a, + 0x20, 0x20, 0x20, 0x20, 0x6c, 0x6f, 0x61, 0x64, 0x5f, 0x65, 0x6e, 0x76, 0x20, 0x2d, 0x2d, 0x66, + 0x69, 0x6c, 0x65, 0x20, 0x2f, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x73, 0x2f, 0x24, 0x6c, 0x61, + 0x62, 0x65, 0x6c, 0x2f, 0x67, 0x72, 0x75, 0x62, 0x65, 0x6e, 0x76, 0x20, 0x73, 0x6e, 0x61, 0x70, + 0x64, 0x5f, 0x72, 0x65, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x79, 0x5f, 0x6b, 0x65, 0x72, 0x6e, 0x65, + 0x6c, 0x20, 0x73, 0x6e, 0x61, 0x70, 0x64, 0x5f, 0x65, 0x78, 0x74, 0x72, 0x61, 0x5f, 0x63, 0x6d, + 0x64, 0x6c, 0x69, 0x6e, 0x65, 0x5f, 0x61, 0x72, 0x67, 0x73, 0x0a, 0x0a, 0x20, 0x20, 0x20, 0x20, + 0x23, 0x20, 0x57, 0x65, 0x20, 0x63, 0x6f, 0x75, 0x6c, 0x64, 0x20, 0x22, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x20, 0x2f, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x73, 0x2f, 0x24, 0x73, 0x6e, 0x61, + 0x70, 0x64, 0x5f, 0x72, 0x65, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x79, 0x5f, 0x73, 0x79, 0x73, 0x74, + 0x65, 0x6d, 0x2f, 0x67, 0x72, 0x75, 0x62, 0x2e, 0x63, 0x66, 0x67, 0x22, 0x20, 0x68, 0x65, 0x72, + 0x65, 0x20, 0x61, 0x73, 0x20, 0x77, 0x65, 0x6c, 0x6c, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x6d, 0x65, + 0x6e, 0x75, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x20, 0x22, 0x52, 0x65, 0x63, 0x6f, 0x76, 0x65, 0x72, + 0x20, 0x75, 0x73, 0x69, 0x6e, 0x67, 0x20, 0x24, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x22, 0x20, 0x2d, + 0x2d, 0x68, 0x6f, 0x74, 0x6b, 0x65, 0x79, 0x3d, 0x72, 0x20, 0x2d, 0x2d, 0x69, 0x64, 0x3d, 0x72, + 0x65, 0x63, 0x6f, 0x76, 0x65, 0x72, 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, 0x72, 0x65, 0x63, 0x6f, 0x76, 0x65, 0x72, 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, 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, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x0a, + 0x20, 0x20, 0x20, 0x20, 0x6d, 0x65, 0x6e, 0x75, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x20, 0x22, 0x49, + 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 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, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 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, 0x69, 0x6e, 0x73, 0x74, 0x61, + 0x6c, 0x6c, 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, 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, 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.45.1+20.04.2/bootloader/assets/grub_test.go snapd-2.48.3+20.04/bootloader/assets/grub_test.go --- snapd-2.45.1+20.04.2/bootloader/assets/grub_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48.3+20.04/bootloader/assets/grub_test.go 2021-02-02 08:21:12.000000000 +0000 @@ -0,0 +1,128 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2020 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 assets_test + +import ( + "bytes" + "fmt" + "io/ioutil" + "testing" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/bootloader/assets" + "github.com/snapcore/snapd/testutil" +) + +// Hook up check.v1 into the "go test" runner +func Test(t *testing.T) { TestingT(t) } + +type grubAssetsTestSuite struct{} + +var _ = Suite(&grubAssetsTestSuite{}) + +func (s *grubAssetsTestSuite) testGrubConfigContains(c *C, name string, keys ...string) { + a := assets.Internal(name) + c.Assert(a, NotNil) + as := string(a) + for _, canary := range keys { + c.Assert(as, testutil.Contains, canary) + } + idx := bytes.IndexRune(a, '\n') + c.Assert(idx, Not(Equals), -1) + c.Assert(string(a[:idx]), Equals, "# Snapd-Boot-Config-Edition: 1") +} + +func (s *grubAssetsTestSuite) TestGrubConf(c *C) { + s.testGrubConfigContains(c, "grub.cfg", + "snapd_recovery_mode", + "set snapd_static_cmdline_args='console=ttyS0 console=tty1 panic=-1'", + ) +} + +func (s *grubAssetsTestSuite) TestGrubRecoveryConf(c *C) { + s.testGrubConfigContains(c, "grub-recovery.cfg", + "snapd_recovery_mode", + "snapd_recovery_system", + "set snapd_static_cmdline_args='console=ttyS0 console=tty1 panic=-1'", + ) +} + +func (s *grubAssetsTestSuite) TestGrubCmdlineSnippetEditions(c *C) { + for _, tc := range []struct { + asset string + edition uint + snip []byte + }{ + {"grub.cfg:static-cmdline", 1, []byte("console=ttyS0 console=tty1 panic=-1")}, + {"grub-recovery.cfg:static-cmdline", 1, []byte("console=ttyS0 console=tty1 panic=-1")}, + } { + snip := assets.SnippetForEdition(tc.asset, tc.edition) + c.Assert(snip, NotNil) + c.Check(snip, DeepEquals, tc.snip) + } +} + +func (s *grubAssetsTestSuite) TestGrubCmdlineSnippetCrossCheck(c *C) { + for _, tc := range []struct { + asset string + snippet string + edition uint + content []byte + pattern string + }{ + { + asset: "grub.cfg", snippet: "grub.cfg:static-cmdline", edition: 1, + content: []byte("console=ttyS0 console=tty1 panic=-1"), + pattern: "set snapd_static_cmdline_args='%s'\n", + }, + { + asset: "grub-recovery.cfg", snippet: "grub-recovery.cfg:static-cmdline", edition: 1, + content: []byte("console=ttyS0 console=tty1 panic=-1"), + pattern: "set snapd_static_cmdline_args='%s'\n", + }, + } { + grubCfg := assets.Internal(tc.asset) + c.Assert(grubCfg, NotNil) + prefix := fmt.Sprintf("# Snapd-Boot-Config-Edition: %d", tc.edition) + c.Assert(bytes.HasPrefix(grubCfg, []byte(prefix)), Equals, true) + // get a matching snippet + snip := assets.SnippetForEdition(tc.snippet, tc.edition) + c.Assert(snip, NotNil) + c.Assert(snip, DeepEquals, tc.content) + c.Assert(string(grubCfg), testutil.Contains, fmt.Sprintf(tc.pattern, string(snip))) + } +} + +func (s *grubAssetsTestSuite) TestGrubAssetsWereRegenerated(c *C) { + for _, tc := range []struct { + asset string + file string + }{ + {"grub.cfg", "data/grub.cfg"}, + {"grub-recovery.cfg", "data/grub-recovery.cfg"}, + } { + assetData := assets.Internal(tc.asset) + c.Assert(assetData, NotNil) + data, err := ioutil.ReadFile(tc.file) + c.Assert(err, IsNil) + c.Check(assetData, DeepEquals, data, Commentf("asset %q has not been updated", tc.asset)) + } +} diff -Nru snapd-2.45.1+20.04.2/bootloader/asset_test.go snapd-2.48.3+20.04/bootloader/asset_test.go --- snapd-2.45.1+20.04.2/bootloader/asset_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48.3+20.04/bootloader/asset_test.go 2021-02-02 08:21:12.000000000 +0000 @@ -0,0 +1,129 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2014-2015 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 bootloader_test + +import ( + "bytes" + "io/ioutil" + "os" + "path/filepath" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/bootloader" + "github.com/snapcore/snapd/bootloader/assets" +) + +type configAssetTestSuite struct { + baseBootenvTestSuite +} + +var _ = Suite(&configAssetTestSuite{}) + +func (s *configAssetTestSuite) TestTrivialFromConfigAssert(c *C) { + e, err := bootloader.EditionFromConfigAsset(bytes.NewBufferString(`# Snapd-Boot-Config-Edition: 321 +next line +one after that`)) + c.Assert(err, IsNil) + c.Assert(e, Equals, uint(321)) + + e, err = bootloader.EditionFromConfigAsset(bytes.NewBufferString(`# Snapd-Boot-Config-Edition: 932 +# Snapd-Boot-Config-Edition: 321 +one after that`)) + c.Assert(err, IsNil) + c.Assert(e, Equals, uint(932)) + + e, err = bootloader.EditionFromConfigAsset(bytes.NewBufferString(`# Snapd-Boot-Config-Edition: 1234 +one after that +# Snapd-Boot-Config-Edition: 321 +`)) + c.Assert(err, IsNil) + c.Assert(e, Equals, uint(1234)) +} + +func (s *configAssetTestSuite) TestTrivialFromFile(c *C) { + d := c.MkDir() + p := filepath.Join(d, "foo") + ioutil.WriteFile(p, []byte(`# Snapd-Boot-Config-Edition: 123 +this is some +this too`), 0644) + e, err := bootloader.EditionFromDiskConfigAsset(p) + c.Assert(err, IsNil) + c.Assert(e, Equals, uint(123)) +} + +func (s *configAssetTestSuite) TestRealConfig(c *C) { + grubConfig := assets.Internal("grub.cfg") + c.Assert(grubConfig, NotNil) + e, err := bootloader.EditionFromConfigAsset(bytes.NewReader(grubConfig)) + c.Assert(err, IsNil) + c.Assert(e, Equals, uint(1)) +} + +func (s *configAssetTestSuite) TestNoConfig(c *C) { + _, err := bootloader.EditionFromConfigAsset(bytes.NewReader(nil)) + c.Assert(err, ErrorMatches, "cannot read config asset: unexpected EOF") +} + +func (s *configAssetTestSuite) TestNoFile(c *C) { + d := c.MkDir() + p := filepath.Join(d, "foo") + // file does not exist + _, err := bootloader.EditionFromDiskConfigAsset(p) + c.Assert(err, ErrorMatches, "no edition") +} + +func (s *configAssetTestSuite) TestUnreadableFile(c *C) { + // root has DAC override + if os.Geteuid() == 0 { + c.Skip("test case cannot be correctly executed by root") + } + d := c.MkDir() + p := filepath.Join(d, "foo") + err := ioutil.WriteFile(p, []byte("foo"), 0000) + c.Assert(err, IsNil) + _, err = bootloader.EditionFromDiskConfigAsset(p) + c.Assert(err, ErrorMatches, "cannot load existing config asset: .*/foo: permission denied") +} + +func (s *configAssetTestSuite) TestNoEdition(c *C) { + _, err := bootloader.EditionFromConfigAsset(bytes.NewReader([]byte(`this is some script +without edition header +`))) + c.Assert(err, ErrorMatches, "no edition") +} + +func (s *configAssetTestSuite) TestBadEdition(c *C) { + _, err := bootloader.EditionFromConfigAsset(bytes.NewReader([]byte(`# Snapd-Boot-Config-Edition: random +data follows +`))) + c.Assert(err, ErrorMatches, `cannot parse asset edition: .* parsing "random": invalid syntax`) +} + +func (s *configAssetTestSuite) TestConfigAssetFrom(c *C) { + script := []byte(`# Snapd-Boot-Config-Edition: 123 +data follows +`) + bs, err := bootloader.ConfigAssetFrom(script) + c.Assert(err, IsNil) + c.Assert(bs, NotNil) + c.Assert(bs.Edition(), Equals, uint(123)) + c.Assert(bs.Raw(), DeepEquals, script) +} diff -Nru snapd-2.45.1+20.04.2/bootloader/bootloader.go snapd-2.48.3+20.04/bootloader/bootloader.go --- snapd-2.45.1+20.04.2/bootloader/bootloader.go 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/bootloader/bootloader.go 2021-02-02 08:21:12.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2014-2015 Canonical Ltd + * Copyright (C) 2014-2020 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,6 +25,7 @@ "os" "path/filepath" + "github.com/snapcore/snapd/bootloader/assets" "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/snap" @@ -39,6 +40,18 @@ ErrNoTryKernelRef = errors.New("no try-kernel referenced") ) +// Role indicates whether the bootloader is used for recovery or run mode. +type Role string + +const ( + // RoleSole applies to the sole bootloader used by UC16/18. + RoleSole Role = "" + // RoleRunMode applies to the run mode booloader. + RoleRunMode Role = "run-mode" + // RoleRecovery apllies to the recovery bootloader. + RoleRecovery Role = "recovery" +) + // Options carries bootloader options. type Options struct { // PrepareImageTime indicates whether the booloader is being @@ -46,16 +59,25 @@ // system. PrepareImageTime bool - // Recovery indicates to use the recovery bootloader. Note that - // UC16/18 do not have a recovery partition. - Recovery bool + // Role specifies to use the bootloader for the given role. + Role Role - // NoSlashBoot indicates to use the run mode bootloader but - // under the native layout and not the /boot mount. + // NoSlashBoot indicates to use the native layout of the + // bootloader partition and not the /boot mount. + // It applies only for RoleRunMode. + // It is implied and ignored for RoleRecovery. + // It is an error to set it for RoleSole. NoSlashBoot bool +} - // ExtractedRunKernelImage is whether to force kernel asset extraction. - ExtractedRunKernelImage bool +func (o *Options) validate() error { + if o == nil { + return nil + } + if o.NoSlashBoot && o.Role == RoleSole { + return fmt.Errorf("internal error: bootloader.RoleSole doesn't expect NoSlashBoot set") + } + return nil } // Bootloader provides an interface to interact with the system @@ -74,9 +96,8 @@ ConfigFile() string // InstallBootConfig will try to install the boot config in the - // given gadgetDir to rootdir. If no boot config for this bootloader - // is found ok is false. - InstallBootConfig(gadgetDir string, opts *Options) (ok bool, err error) + // given gadgetDir to rootdir. + InstallBootConfig(gadgetDir string, opts *Options) error // ExtractKernelAssets extracts kernel assets from the given kernel snap. ExtractKernelAssets(s snap.PlaceInfo, snapf snap.Container) error @@ -93,6 +114,7 @@ type RecoveryAwareBootloader interface { Bootloader SetRecoverySystemEnv(recoverySystemDir string, values map[string]string) error + GetRecoverySystemEnv(recoverySystemDir string, key string) (string, error) } type ExtractedRecoveryKernelImageBootloader interface { @@ -139,30 +161,110 @@ DisableTryKernel() error } -func genericInstallBootConfig(gadgetFile, systemFile string) (bool, error) { - if !osutil.FileExists(gadgetFile) { - return false, nil +// TrustedAssetsBootloader has boot assets that take part in the secure boot +// process and need to be tracked, while other boot assets (typically boot +// config) are managed by snapd. +type TrustedAssetsBootloader interface { + Bootloader + + // ManagedAssets returns a list of boot assets managed by the bootloader + // in the boot filesystem. Does not require rootdir to be set. + ManagedAssets() []string + // UpdateBootConfig updates the boot config assets used by the bootloader. + UpdateBootConfig(*Options) error + // CommandLine returns the kernel command line composed of mode and + // system arguments, built-in bootloader specific static arguments + // corresponding to the on-disk boot asset edition, followed by any + // extra arguments. The command line may be different when using a + // recovery bootloader. + CommandLine(modeArg, systemArg, extraArgs string) (string, error) + // CandidateCommandLine is similar to CommandLine, but uses the current + // edition of managed built-in boot assets as reference. + CandidateCommandLine(modeArg, systemArg, extraArgs string) (string, error) + + // TrustedAssets returns the list of relative paths to assets inside the + // bootloader's rootdir that are measured in the boot process in the + // order of loading during the boot. Does not require rootdir to be set. + TrustedAssets() ([]string, error) + + // RecoveryBootChain returns the load chain for recovery modes. + // It should be called on a RoleRecovery bootloader. + RecoveryBootChain(kernelPath string) ([]BootFile, error) + + // BootChain returns the load chain for run mode. + // It should be called on a RoleRecovery bootloader passing the + // RoleRunMode bootloader. + BootChain(runBl Bootloader, kernelPath string) ([]BootFile, error) +} + +func genericInstallBootConfig(gadgetFile, systemFile string) error { + if err := os.MkdirAll(filepath.Dir(systemFile), 0755); err != nil { + return err + } + return osutil.CopyFile(gadgetFile, systemFile, osutil.CopyFlagOverwrite) +} + +func genericSetBootConfigFromAsset(systemFile, assetName string) error { + bootConfig := assets.Internal(assetName) + if bootConfig == nil { + return fmt.Errorf("internal error: no boot asset for %q", assetName) } if err := os.MkdirAll(filepath.Dir(systemFile), 0755); err != nil { - return true, err + return err } - return true, osutil.CopyFile(gadgetFile, systemFile, osutil.CopyFlagOverwrite) + return osutil.AtomicWriteFile(systemFile, bootConfig, 0644, 0) +} + +func genericUpdateBootConfigFromAssets(systemFile string, assetName string) error { + currentBootConfigEdition, err := editionFromDiskConfigAsset(systemFile) + if err != nil && err != errNoEdition { + return err + } + if err == errNoEdition { + return nil + } + newBootConfig := assets.Internal(assetName) + if len(newBootConfig) == 0 { + return fmt.Errorf("no boot config asset with name %q", assetName) + } + bc, err := configAssetFrom(newBootConfig) + if err != nil { + return err + } + if bc.Edition() <= currentBootConfigEdition { + // edition of the candidate boot config is lower than or equal + // to one currently installed + return nil + } + return osutil.AtomicWriteFile(systemFile, bc.Raw(), 0644, 0) } // InstallBootConfig installs the bootloader config from the gadget // snap dir into the right place. func InstallBootConfig(gadgetDir, rootDir string, opts *Options) error { - for _, bl := range []installableBootloader{&grub{}, &uboot{}, &androidboot{}, &lk{}} { - bl.setRootDir(rootDir) - ok, err := bl.InstallBootConfig(gadgetDir, opts) - if ok { - return err - } + if err := opts.validate(); err != nil { + return err } - - return fmt.Errorf("cannot find boot config in %q", gadgetDir) + bl, err := ForGadget(gadgetDir, rootDir, opts) + if err != nil { + return fmt.Errorf("cannot find boot config in %q", gadgetDir) + } + return bl.InstallBootConfig(gadgetDir, opts) } +type bootloaderNewFunc func(rootdir string, opts *Options) Bootloader + +var ( + // bootloaders list all possible bootloaders by their constructor + // function. + bootloaders = []bootloaderNewFunc{ + newUboot, + newGrub, + newAndroidBoot, + newLk, + } +) + var ( forcedBootloader Bootloader forcedError error @@ -175,6 +277,9 @@ // can also be used to find the recovery bootloader, e.g. on uc20: // bootloader.Find("/run/mnt/ubuntu-seed") func Find(rootdir string, opts *Options) (Bootloader, error) { + if err := opts.validate(); err != nil { + return nil, err + } if forcedBootloader != nil || forcedError != nil { return forcedBootloader, forcedError } @@ -186,26 +291,12 @@ opts = &Options{} } - // try uboot - if uboot := newUboot(rootdir, opts); uboot != nil { - return uboot, nil - } - - // no, try grub - if grub := newGrub(rootdir, opts); grub != nil { - return grub, nil - } - - // no, try androidboot - if androidboot := newAndroidBoot(rootdir); androidboot != nil { - return androidboot, nil - } - - // no, try lk - if lk := newLk(rootdir, opts); lk != nil { - return lk, nil + for _, blNew := range bootloaders { + bl := blNew(rootdir, opts) + if osutil.FileExists(bl.ConfigFile()) { + return bl, nil + } } - // no, weeeee return nil, ErrBootloader } @@ -256,3 +347,52 @@ return nil } + +// ForGadget returns a bootloader matching a given gadget by inspecting the +// contents of gadget directory or an error if no matching bootloader is found. +func ForGadget(gadgetDir, rootDir string, opts *Options) (Bootloader, error) { + if err := opts.validate(); err != nil { + return nil, err + } + if forcedBootloader != nil || forcedError != nil { + return forcedBootloader, forcedError + } + for _, blNew := range bootloaders { + bl := blNew(rootDir, opts) + markerConf := filepath.Join(gadgetDir, bl.Name()+".conf") + // do we have a marker file? + if osutil.FileExists(markerConf) { + return bl, nil + } + } + return nil, ErrBootloader +} + +// BootFile represents each file in the chains of trusted assets and +// kernels used in the boot process. For example a boot file can be an +// EFI binary or a snap file containing an EFI binary. +type BootFile struct { + // Path is the path to the file in the filesystem or, if Snap + // is set, the relative path inside the snap file. + Path string + // Snap contains the path to the snap file if a snap file is used. + Snap string + // Role is set to the role of the bootloader this boot file + // originates from. + Role Role +} + +func NewBootFile(snap, path string, role Role) BootFile { + return BootFile{ + Snap: snap, + Path: path, + Role: role, + } +} + +// WithPath returns a copy of the BootFile with path updated to the +// specified value. +func (b BootFile) WithPath(path string) BootFile { + b.Path = path + return b +} diff -Nru snapd-2.45.1+20.04.2/bootloader/bootloadertest/bootloadertest.go snapd-2.48.3+20.04/bootloader/bootloadertest/bootloadertest.go --- snapd-2.45.1+20.04.2/bootloader/bootloadertest/bootloadertest.go 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/bootloader/bootloadertest/bootloadertest.go 2021-02-02 08:21:12.000000000 +0000 @@ -22,6 +22,7 @@ import ( "fmt" "path/filepath" + "strings" "github.com/snapcore/snapd/bootloader" "github.com/snapcore/snapd/snap" @@ -42,15 +43,18 @@ RemoveKernelAssetsCalls []snap.PlaceInfo InstallBootConfigCalled []string - InstallBootConfigResult bool InstallBootConfigErr error + enabledKernel snap.PlaceInfo + enabledTryKernel snap.PlaceInfo + panicMethods map[string]bool } -// ensure MockBootloader(s) implement the Bootloader interfaceces +// ensure MockBootloader(s) implement the Bootloader interface var _ bootloader.Bootloader = (*MockBootloader)(nil) var _ bootloader.RecoveryAwareBootloader = (*MockRecoveryAwareBootloader)(nil) +var _ bootloader.TrustedAssetsBootloader = (*MockTrustedAssetsBootloader)(nil) var _ bootloader.ExtractedRunKernelImageBootloader = (*MockExtractedRunKernelImageBootloader)(nil) var _ bootloader.ExtractedRecoveryKernelImageBootloader = (*MockExtractedRecoveryKernelImageBootloader)(nil) @@ -109,11 +113,33 @@ return nil } +func (b *MockBootloader) SetEnabledKernel(s snap.PlaceInfo) (restore func()) { + oldSn := b.enabledTryKernel + oldVar := b.BootVars["snap_kernel"] + b.enabledKernel = s + b.BootVars["snap_kernel"] = s.Filename() + return func() { + b.BootVars["snap_kernel"] = oldVar + b.enabledKernel = oldSn + } +} + +func (b *MockBootloader) SetEnabledTryKernel(s snap.PlaceInfo) (restore func()) { + oldSn := b.enabledTryKernel + oldVar := b.BootVars["snap_try_kernel"] + b.enabledTryKernel = s + b.BootVars["snap_try_kernel"] = s.Filename() + return func() { + b.BootVars["snap_try_kernel"] = oldVar + b.enabledTryKernel = oldSn + } +} + // InstallBootConfig installs the boot config in the gadget directory to the // mock bootloader's root directory. -func (b *MockBootloader) InstallBootConfig(gadgetDir string, opts *bootloader.Options) (bool, error) { +func (b *MockBootloader) InstallBootConfig(gadgetDir string, opts *bootloader.Options) error { b.InstallBootConfigCalled = append(b.InstallBootConfigCalled, gadgetDir) - return b.InstallBootConfigResult, b.InstallBootConfigErr + return b.InstallBootConfigErr } // MockRecoveryAwareBootloader mocks a bootloader implementing the @@ -178,6 +204,16 @@ return nil } +// GetRecoverySystemEnv gets the recovery system environment bootloader +// variables; part of RecoveryAwareBootloader. +func (b *MockRecoveryAwareBootloader) GetRecoverySystemEnv(recoverySystemDir, key string) (string, error) { + if recoverySystemDir == "" { + panic("MockBootloader.GetRecoverySystemEnv called without recoverySystemDir") + } + b.RecoverySystemDir = recoverySystemDir + return b.RecoverySystemBootVars[key], nil +} + // MockExtractedRunKernelImageBootloader mocks a bootloader // implementing the ExtractedRunKernelImageBootloader interface. type MockExtractedRunKernelImageBootloader struct { @@ -204,10 +240,10 @@ } } -// SetRunKernelImageEnabledKernel sets the current kernel "symlink" as returned +// SetEnabledKernel sets the current kernel "symlink" as returned // by Kernel(); returns' a restore function to set it back to what it was // before. -func (b *MockExtractedRunKernelImageBootloader) SetRunKernelImageEnabledKernel(kernel snap.PlaceInfo) (restore func()) { +func (b *MockExtractedRunKernelImageBootloader) SetEnabledKernel(kernel snap.PlaceInfo) (restore func()) { old := b.runKernelImageEnabledKernel b.runKernelImageEnabledKernel = kernel return func() { @@ -215,11 +251,11 @@ } } -// SetRunKernelImageEnabledTryKernel sets the current try-kernel "symlink" as +// SetEnabledTryKernel sets the current try-kernel "symlink" as // returned by TryKernel(). If set to nil, TryKernel()'s second return value // will be false; returns' a restore function to set it back to what it was // before. -func (b *MockExtractedRunKernelImageBootloader) SetRunKernelImageEnabledTryKernel(kernel snap.PlaceInfo) (restore func()) { +func (b *MockExtractedRunKernelImageBootloader) SetEnabledTryKernel(kernel snap.PlaceInfo) (restore func()) { old := b.runKernelImageEnabledTryKernel b.runKernelImageEnabledTryKernel = kernel return func() { @@ -335,3 +371,85 @@ b.runKernelImageEnabledTryKernel = nil return b.runKernelImageMockedErrs["DisableTryKernel"] } + +// MockTrustedAssetsBootloader mocks a bootloader implementing the +// bootloader.TrustedAssetsBootloader interface. +type MockTrustedAssetsBootloader struct { + *MockBootloader + + TrustedAssetsList []string + TrustedAssetsErr error + TrustedAssetsCalls int + + RecoveryBootChainList []bootloader.BootFile + RecoveryBootChainErr error + BootChainList []bootloader.BootFile + BootChainErr error + + RecoveryBootChainCalls []string + BootChainRunBl []bootloader.Bootloader + BootChainKernelPath []string + + UpdateErr error + UpdateCalls int + ManagedAssetsList []string + StaticCommandLine string + CandidateStaticCommandLine string + CommandLineErr error +} + +func (b *MockBootloader) WithTrustedAssets() *MockTrustedAssetsBootloader { + return &MockTrustedAssetsBootloader{ + MockBootloader: b, + } +} + +func (b *MockTrustedAssetsBootloader) ManagedAssets() []string { + return b.ManagedAssetsList +} + +func (b *MockTrustedAssetsBootloader) UpdateBootConfig(opts *bootloader.Options) error { + b.UpdateCalls++ + return b.UpdateErr +} + +func glueCommandLine(modeArg, systemArg, staticArgs, extraArgs string) string { + args := []string(nil) + for _, argSet := range []string{modeArg, systemArg, staticArgs, extraArgs} { + if argSet != "" { + args = append(args, argSet) + } + } + line := strings.Join(args, " ") + return strings.TrimSpace(line) +} + +func (b *MockTrustedAssetsBootloader) CommandLine(modeArg, systemArg, extraArgs string) (string, error) { + if b.CommandLineErr != nil { + return "", b.CommandLineErr + } + return glueCommandLine(modeArg, systemArg, b.StaticCommandLine, extraArgs), nil +} + +func (b *MockTrustedAssetsBootloader) CandidateCommandLine(modeArg, systemArg, extraArgs string) (string, error) { + if b.CommandLineErr != nil { + return "", b.CommandLineErr + } + return glueCommandLine(modeArg, systemArg, b.CandidateStaticCommandLine, extraArgs), nil +} + +func (b *MockTrustedAssetsBootloader) TrustedAssets() ([]string, error) { + b.TrustedAssetsCalls++ + return b.TrustedAssetsList, b.TrustedAssetsErr +} + +func (b *MockTrustedAssetsBootloader) RecoveryBootChain(kernelPath string) ([]bootloader.BootFile, error) { + b.RecoveryBootChainCalls = append(b.RecoveryBootChainCalls, kernelPath) + return b.RecoveryBootChainList, b.RecoveryBootChainErr +} + +func (b *MockTrustedAssetsBootloader) BootChain(runBl bootloader.Bootloader, kernelPath string) ([]bootloader.BootFile, error) { + b.BootChainRunBl = append(b.BootChainRunBl, runBl) + b.BootChainKernelPath = append(b.BootChainKernelPath, kernelPath) + return b.BootChainList, b.BootChainErr +} diff -Nru snapd-2.45.1+20.04.2/bootloader/bootloader_test.go snapd-2.48.3+20.04/bootloader/bootloader_test.go --- snapd-2.45.1+20.04.2/bootloader/bootloader_test.go 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/bootloader/bootloader_test.go 2021-02-02 08:21:12.000000000 +0000 @@ -22,12 +22,14 @@ import ( "errors" "io/ioutil" + "os" "path/filepath" "testing" . "gopkg.in/check.v1" "github.com/snapcore/snapd/bootloader" + "github.com/snapcore/snapd/bootloader/assets" "github.com/snapcore/snapd/bootloader/bootloadertest" "github.com/snapcore/snapd/snap" "github.com/snapcore/snapd/testutil" @@ -93,7 +95,7 @@ c.Assert(err, ErrorMatches, `cannot find boot config in.*`) } -func (s *bootenvTestSuite) TestInstallBootloaderConfig(c *C) { +func (s *bootenvTestSuite) TestInstallBootloaderConfigFromGadget(c *C) { for _, t := range []struct { name string gadgetFile, sysFile string @@ -108,18 +110,209 @@ name: "uboot boot.scr", gadgetFile: "uboot.conf", sysFile: "/uboot/ubuntu/boot.sel", - opts: &bootloader.Options{NoSlashBoot: true}, + opts: &bootloader.Options{Role: bootloader.RoleRecovery}, }, {name: "androidboot", gadgetFile: "androidboot.conf", sysFile: "/boot/androidboot/androidboot.env"}, {name: "lk", gadgetFile: "lk.conf", sysFile: "/boot/lk/snapbootsel.bin"}, - {name: "grub recovery", gadgetFile: "grub-recovery.conf", sysFile: "/EFI/ubuntu/grub.cfg", opts: &bootloader.Options{Recovery: true}}, } { mockGadgetDir := c.MkDir() + rootDir := c.MkDir() err := ioutil.WriteFile(filepath.Join(mockGadgetDir, t.gadgetFile), t.gadgetFileContent, 0644) c.Assert(err, IsNil) - err = bootloader.InstallBootConfig(mockGadgetDir, s.rootdir, t.opts) + err = bootloader.InstallBootConfig(mockGadgetDir, rootDir, t.opts) c.Assert(err, IsNil, Commentf("installing boot config for %s", t.name)) - fn := filepath.Join(s.rootdir, t.sysFile) + fn := filepath.Join(rootDir, t.sysFile) c.Assert(fn, testutil.FilePresent, Commentf("boot config missing for %s at %s", t.name, t.sysFile)) } } + +func (s *bootenvTestSuite) TestInstallBootloaderConfigFromAssets(c *C) { + recoveryOpts := &bootloader.Options{ + Role: bootloader.RoleRecovery, + } + systemBootOpts := &bootloader.Options{ + Role: bootloader.RoleRunMode, + } + defaultRecoveryGrubAsset := assets.Internal("grub-recovery.cfg") + c.Assert(defaultRecoveryGrubAsset, NotNil) + defaultGrubAsset := assets.Internal("grub.cfg") + c.Assert(defaultGrubAsset, NotNil) + + for _, t := range []struct { + name string + gadgetFile, sysFile string + gadgetFileContent []byte + sysFileContent []byte + assetContent []byte + assetName string + err string + opts *bootloader.Options + }{ + { + name: "recovery grub", + opts: recoveryOpts, + gadgetFile: "grub.conf", + // empty file in the gadget + gadgetFileContent: nil, + sysFile: "/EFI/ubuntu/grub.cfg", + assetName: "grub-recovery.cfg", + assetContent: []byte("hello assets"), + // boot config from assets + sysFileContent: []byte("hello assets"), + }, { + name: "recovery grub with non empty gadget file", + opts: recoveryOpts, + gadgetFile: "grub.conf", + gadgetFileContent: []byte("not so empty"), + sysFile: "/EFI/ubuntu/grub.cfg", + assetName: "grub-recovery.cfg", + assetContent: []byte("hello assets"), + // boot config from assets + sysFileContent: []byte("hello assets"), + }, { + name: "recovery grub with default asset", + opts: recoveryOpts, + gadgetFile: "grub.conf", + // empty file in the gadget + gadgetFileContent: nil, + sysFile: "/EFI/ubuntu/grub.cfg", + sysFileContent: defaultRecoveryGrubAsset, + }, { + name: "recovery grub missing asset", + opts: recoveryOpts, + gadgetFile: "grub.conf", + // empty file in the gadget + gadgetFileContent: nil, + sysFile: "/EFI/ubuntu/grub.cfg", + assetName: "grub-recovery.cfg", + // no asset content + err: `internal error: no boot asset for "grub-recovery.cfg"`, + }, { + name: "system-boot grub", + opts: systemBootOpts, + gadgetFile: "grub.conf", + // empty file in the gadget + gadgetFileContent: nil, + sysFile: "/EFI/ubuntu/grub.cfg", + assetName: "grub.cfg", + assetContent: []byte("hello assets"), + sysFileContent: []byte("hello assets"), + }, { + name: "system-boot grub with default asset", + opts: systemBootOpts, + gadgetFile: "grub.conf", + // empty file in the gadget + gadgetFileContent: nil, + sysFile: "/EFI/ubuntu/grub.cfg", + sysFileContent: defaultGrubAsset, + }, + } { + mockGadgetDir := c.MkDir() + rootDir := c.MkDir() + fn := filepath.Join(rootDir, t.sysFile) + err := ioutil.WriteFile(filepath.Join(mockGadgetDir, t.gadgetFile), t.gadgetFileContent, 0644) + c.Assert(err, IsNil) + var restoreAsset func() + if t.assetName != "" { + restoreAsset = assets.MockInternal(t.assetName, t.assetContent) + } + err = bootloader.InstallBootConfig(mockGadgetDir, rootDir, t.opts) + if t.err == "" { + c.Assert(err, IsNil, Commentf("installing boot config for %s", t.name)) + // mocked asset content + c.Assert(fn, testutil.FileEquals, string(t.sysFileContent)) + } else { + c.Assert(err, ErrorMatches, t.err) + c.Assert(fn, testutil.FileAbsent) + } + if restoreAsset != nil { + restoreAsset() + } + } +} + +func (s *bootenvTestSuite) TestBootloaderFind(c *C) { + for _, tc := range []struct { + name string + sysFile string + opts *bootloader.Options + expName string + }{ + {name: "grub", sysFile: "/boot/grub/grub.cfg", expName: "grub"}, + { + // native run partition layout + name: "grub", sysFile: "/EFI/ubuntu/grub.cfg", + opts: &bootloader.Options{Role: bootloader.RoleRunMode, NoSlashBoot: true}, + expName: "grub", + }, + { + // recovery layout + name: "grub", sysFile: "/EFI/ubuntu/grub.cfg", + opts: &bootloader.Options{Role: bootloader.RoleRecovery}, + expName: "grub", + }, + + // traditional uboot.env - the uboot.env file needs to be non-empty + {name: "uboot.env", sysFile: "/boot/uboot/uboot.env", expName: "uboot"}, + // boot.sel variant + { + name: "uboot boot.scr", + sysFile: "/uboot/ubuntu/boot.sel", + opts: &bootloader.Options{Role: bootloader.RoleRunMode, NoSlashBoot: true}, + expName: "uboot", + }, + {name: "androidboot", sysFile: "/boot/androidboot/androidboot.env", expName: "androidboot"}, + // lk is detected differently based on runtime/prepare-image + {name: "lk", sysFile: "/dev/disk/by-partlabel/snapbootsel", expName: "lk"}, + { + name: "lk", sysFile: "/boot/lk/snapbootsel.bin", + expName: "lk", opts: &bootloader.Options{PrepareImageTime: true}, + }, + } { + c.Logf("tc: %v", tc.name) + rootDir := c.MkDir() + err := os.MkdirAll(filepath.Join(rootDir, filepath.Dir(tc.sysFile)), 0755) + c.Assert(err, IsNil) + err = ioutil.WriteFile(filepath.Join(rootDir, tc.sysFile), nil, 0644) + c.Assert(err, IsNil) + bl, err := bootloader.Find(rootDir, tc.opts) + c.Assert(err, IsNil) + c.Assert(bl, NotNil) + c.Check(bl.Name(), Equals, tc.expName) + } +} + +func (s *bootenvTestSuite) TestBootloaderForGadget(c *C) { + for _, tc := range []struct { + name string + gadgetFile string + opts *bootloader.Options + expName string + }{ + {name: "grub", gadgetFile: "grub.conf", expName: "grub"}, + {name: "grub", gadgetFile: "grub.conf", opts: &bootloader.Options{Role: bootloader.RoleRunMode, NoSlashBoot: true}, expName: "grub"}, + {name: "grub", gadgetFile: "grub.conf", opts: &bootloader.Options{Role: bootloader.RoleRecovery}, expName: "grub"}, + {name: "uboot", gadgetFile: "uboot.conf", expName: "uboot"}, + {name: "androidboot", gadgetFile: "androidboot.conf", expName: "androidboot"}, + {name: "lk", gadgetFile: "lk.conf", expName: "lk"}, + } { + c.Logf("tc: %v", tc.name) + gadgetDir := c.MkDir() + rootDir := c.MkDir() + err := os.MkdirAll(filepath.Join(rootDir, filepath.Dir(tc.gadgetFile)), 0755) + c.Assert(err, IsNil) + err = ioutil.WriteFile(filepath.Join(gadgetDir, tc.gadgetFile), nil, 0644) + c.Assert(err, IsNil) + bl, err := bootloader.ForGadget(gadgetDir, rootDir, tc.opts) + c.Assert(err, IsNil) + c.Assert(bl, NotNil) + c.Check(bl.Name(), Equals, tc.expName) + } +} + +func (s *bootenvTestSuite) TestBootFileWithPath(c *C) { + a := bootloader.NewBootFile("", "some/path", bootloader.RoleRunMode) + b := a.WithPath("other/path") + c.Assert(a.Path, Equals, "some/path") + c.Assert(b.Path, Equals, "other/path") +} diff -Nru snapd-2.45.1+20.04.2/bootloader/efi/efi.go snapd-2.48.3+20.04/bootloader/efi/efi.go --- snapd-2.45.1+20.04.2/bootloader/efi/efi.go 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/bootloader/efi/efi.go 2021-02-02 08:21:12.000000000 +0000 @@ -29,7 +29,6 @@ "io/ioutil" "os" "path/filepath" - "strings" "unicode/utf16" "github.com/snapcore/snapd/dirs" @@ -48,8 +47,7 @@ ) var ( - isSnapdTest = len(os.Args) > 0 && strings.HasSuffix(os.Args[0], ".test") - openEFIVar = openEFIVarImpl + openEFIVar = openEFIVarImpl ) const expectedEFIvarfsDir = "/sys/firmware/efi/efivars" @@ -164,9 +162,7 @@ // MockVars mocks EFI variables as read by ReadVar*, only to be used // from tests. Set vars to nil to mock a non-EFI system. func MockVars(vars map[string][]byte, attrs map[string]VariableAttr) (restore func()) { - if !isSnapdTest { - panic("MockVars only to be used from tests") - } + osutil.MustBeTestBinary("MockVars only to be used from tests") old := openEFIVar openEFIVar = func(name string) (io.ReadCloser, VariableAttr, int64, error) { if vars == nil { diff -Nru snapd-2.45.1+20.04.2/bootloader/export_test.go snapd-2.48.3+20.04/bootloader/export_test.go --- snapd-2.45.1+20.04.2/bootloader/export_test.go 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/bootloader/export_test.go 2021-02-02 08:21:12.000000000 +0000 @@ -32,7 +32,7 @@ // creates a new Androidboot bootloader object func NewAndroidBoot(rootdir string) Bootloader { - return newAndroidBoot(rootdir) + return newAndroidBoot(rootdir, nil) } func MockAndroidBootFile(c *C, rootdir string, mode os.FileMode) { @@ -44,7 +44,7 @@ } func NewUboot(rootdir string, blOpts *Options) ExtractedRecoveryKernelImageBootloader { - return newUboot(rootdir, blOpts) + return newUboot(rootdir, blOpts).(ExtractedRecoveryKernelImageBootloader) } func MockUbootFiles(c *C, rootdir string, blOpts *Options) { @@ -62,7 +62,7 @@ } func NewGrub(rootdir string, opts *Options) RecoveryAwareBootloader { - return newGrub(rootdir, opts) + return newGrub(rootdir, opts).(RecoveryAwareBootloader) } func MockGrubFiles(c *C, rootdir string) { @@ -102,3 +102,10 @@ lk := b.(*lk) return lk.inRuntimeMode } + +var ( + EditionFromDiskConfigAsset = editionFromDiskConfigAsset + EditionFromConfigAsset = editionFromConfigAsset + ConfigAssetFrom = configAssetFrom + StaticCommandLineForGrubAssetEdition = staticCommandLineForGrubAssetEdition +) diff -Nru snapd-2.45.1+20.04.2/bootloader/grub.go snapd-2.48.3+20.04/bootloader/grub.go --- snapd-2.45.1+20.04.2/bootloader/grub.go 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/bootloader/grub.go 2021-02-02 08:21:12.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2014-2015 Canonical Ltd + * Copyright (C) 2014-2020 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,7 +23,9 @@ "fmt" "os" "path/filepath" + "strings" + "github.com/snapcore/snapd/bootloader/assets" "github.com/snapcore/snapd/bootloader/grubenv" "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/snap" @@ -35,6 +37,7 @@ _ installableBootloader = (*grub)(nil) _ RecoveryAwareBootloader = (*grub)(nil) _ ExtractedRunKernelImageBootloader = (*grub)(nil) + _ TrustedAssetsBootloader = (*grub)(nil) ) type grub struct { @@ -43,22 +46,27 @@ basedir string uefiRunKernelExtraction bool + recovery bool + nativePartitionLayout bool } // newGrub create a new Grub bootloader object -func newGrub(rootdir string, opts *Options) RecoveryAwareBootloader { +func newGrub(rootdir string, opts *Options) Bootloader { g := &grub{rootdir: rootdir} - if opts != nil && (opts.Recovery || opts.NoSlashBoot) { + if opts != nil { + // Set the flag to extract the run kernel, only + // for UC20 run mode. + // Both UC16/18 and the recovery mode of UC20 load + // the kernel directly from snaps. + g.uefiRunKernelExtraction = opts.Role == RoleRunMode + g.recovery = opts.Role == RoleRecovery + g.nativePartitionLayout = opts.NoSlashBoot || g.recovery + } + if g.nativePartitionLayout { g.basedir = "EFI/ubuntu" } else { g.basedir = "boot/grub" } - if !osutil.FileExists(g.ConfigFile()) { - return nil - } - if opts != nil { - g.uefiRunKernelExtraction = opts.ExtractedRunKernelImage - } return g } @@ -78,12 +86,28 @@ return filepath.Join(g.rootdir, g.basedir) } -func (g *grub) InstallBootConfig(gadgetDir string, opts *Options) (bool, error) { - if opts != nil && opts.Recovery { - recoveryGrubCfg := filepath.Join(gadgetDir, g.Name()+"-recovery.conf") - systemFile := filepath.Join(g.rootdir, "/EFI/ubuntu/grub.cfg") - return genericInstallBootConfig(recoveryGrubCfg, systemFile) +func (g *grub) installManagedRecoveryBootConfig(gadgetDir string) error { + assetName := g.Name() + "-recovery.cfg" + systemFile := filepath.Join(g.rootdir, "/EFI/ubuntu/grub.cfg") + return genericSetBootConfigFromAsset(systemFile, assetName) +} + +func (g *grub) installManagedBootConfig(gadgetDir string) error { + assetName := g.Name() + ".cfg" + systemFile := filepath.Join(g.rootdir, "/EFI/ubuntu/grub.cfg") + return genericSetBootConfigFromAsset(systemFile, assetName) +} + +func (g *grub) InstallBootConfig(gadgetDir string, opts *Options) error { + if opts != nil && opts.Role == RoleRecovery { + // install managed config for the recovery partition + return g.installManagedRecoveryBootConfig(gadgetDir) + } + if opts != nil && opts.Role == RoleRunMode { + // install managed boot config that can handle kernel.efi + return g.installManagedBootConfig(gadgetDir) } + gadgetFile := filepath.Join(gadgetDir, g.Name()+".conf") systemFile := filepath.Join(g.rootdir, "/boot/grub/grub.cfg") return genericInstallBootConfig(gadgetFile, systemFile) @@ -104,6 +128,21 @@ return genv.Save() } +func (g *grub) GetRecoverySystemEnv(recoverySystemDir string, key string) (string, error) { + if recoverySystemDir == "" { + return "", fmt.Errorf("internal error: recoverySystemDir unset") + } + recoverySystemGrubEnv := filepath.Join(g.rootdir, recoverySystemDir, "grubenv") + genv := grubenv.NewEnv(recoverySystemGrubEnv) + if err := genv.Load(); err != nil { + if os.IsNotExist(err) { + return "", nil + } + return "", err + } + return genv.Get(key), nil +} + func (g *grub) ConfigFile() string { return filepath.Join(g.dir(), "grub.cfg") } @@ -300,3 +339,172 @@ } return nil, ErrNoTryKernelRef } + +// UpdateBootConfig updates the grub boot config only if it is already managed +// and has a lower edition. +// +// Implements ManagedAssetsBootloader for the grub bootloader. +func (g *grub) UpdateBootConfig(opts *Options) error { + // XXX: do we need to take opts here? + bootScriptName := "grub.cfg" + currentBootConfig := filepath.Join(g.dir(), "grub.cfg") + if opts != nil && opts.Role == RoleRecovery { + // use the recovery asset when asked to do so + bootScriptName = "grub-recovery.cfg" + } + return genericUpdateBootConfigFromAssets(currentBootConfig, bootScriptName) +} + +// ManagedAssets returns a list relative paths to boot assets inside the root +// directory of the filesystem. +// +// Implements ManagedAssetsBootloader for the grub bootloader. +func (g *grub) ManagedAssets() []string { + return []string{ + filepath.Join(g.basedir, "grub.cfg"), + } +} + +func (g *grub) commandLineForEdition(edition uint, modeArg, systemArg, extraArgs string) (string, error) { + assetName := "grub.cfg" + if g.recovery { + assetName = "grub-recovery.cfg" + } + staticCmdline := staticCommandLineForGrubAssetEdition(assetName, edition) + args, err := osutil.KernelCommandLineSplit(staticCmdline + " " + extraArgs) + if err != nil { + return "", fmt.Errorf("cannot use badly formatted kernel command line: %v", err) + } + // join all argument with a single space, see + // grub-core/lib/cmdline.c:grub_create_loader_cmdline() for reference, + // arguments are separated by a single space, the space after last is + // replaced with terminating NULL + snapdArgs := make([]string, 0, 2) + if modeArg != "" { + snapdArgs = append(snapdArgs, modeArg) + } + if systemArg != "" { + snapdArgs = append(snapdArgs, systemArg) + } + return strings.Join(append(snapdArgs, args...), " "), nil +} + +// CommandLine returns the kernel command line composed of mode and +// system arguments, built-in bootloader specific static arguments +// corresponding to the on-disk boot asset edition, followed by any +// extra arguments. The command line may be different when using a +// recovery bootloader. +// +// Implements ManagedAssetsBootloader for the grub bootloader. +func (g *grub) CommandLine(modeArg, systemArg, extraArgs string) (string, error) { + currentBootConfig := filepath.Join(g.dir(), "grub.cfg") + edition, err := editionFromDiskConfigAsset(currentBootConfig) + if err != nil { + if err != errNoEdition { + return "", fmt.Errorf("cannot obtain edition number of current boot config: %v", err) + } + // we were called using the ManagedAssetsBootloader interface + // meaning the caller expects to us to use the managed assets, + // since one on disk is not managed, use the initial edition of + // the internal boot asset which is compatible with grub.cfg + // used before we started writing out the files ourselves + edition = 1 + } + return g.commandLineForEdition(edition, modeArg, systemArg, extraArgs) +} + +// CandidateCommandLine is similar to CommandLine, but uses the current +// edition of managed built-in boot assets as reference. +// +// Implements ManagedAssetsBootloader for the grub bootloader. +func (g *grub) CandidateCommandLine(modeArg, systemArg, extraArgs string) (string, error) { + assetName := "grub.cfg" + if g.recovery { + assetName = "grub-recovery.cfg" + } + edition, err := editionFromInternalConfigAsset(assetName) + if err != nil { + return "", err + } + return g.commandLineForEdition(edition, modeArg, systemArg, extraArgs) +} + +// staticCommandLineForGrubAssetEdition fetches a static command line for given +// grub asset edition +func staticCommandLineForGrubAssetEdition(asset string, edition uint) string { + cmdline := assets.SnippetForEdition(fmt.Sprintf("%s:static-cmdline", asset), edition) + if cmdline == nil { + return "" + } + return string(cmdline) +} + +var ( + grubRecoveryModeTrustedAssets = []string{ + // recovery mode shim EFI binary + "EFI/boot/bootx64.efi", + // recovery mode grub EFI binary + "EFI/boot/grubx64.efi", + } + + grubRunModeTrustedAssets = []string{ + // run mode grub EFI binary + "EFI/boot/grubx64.efi", + } +) + +// TrustedAssets returns the list of relative paths to assets inside +// the bootloader's rootdir that are measured in the boot process in the +// order of loading during the boot. +func (g *grub) TrustedAssets() ([]string, error) { + if !g.nativePartitionLayout { + return nil, fmt.Errorf("internal error: trusted assets called without native host-partition layout") + } + if g.recovery { + return grubRecoveryModeTrustedAssets, nil + } + return grubRunModeTrustedAssets, nil +} + +// RecoveryBootChain returns the load chain for recovery modes. +// It should be called on a RoleRecovery bootloader. +func (g *grub) RecoveryBootChain(kernelPath string) ([]BootFile, error) { + if !g.recovery { + return nil, fmt.Errorf("not a recovery bootloader") + } + + // add trusted assets to the recovery chain + chain := make([]BootFile, 0, len(grubRecoveryModeTrustedAssets)+1) + for _, ta := range grubRecoveryModeTrustedAssets { + chain = append(chain, NewBootFile("", ta, RoleRecovery)) + } + // add recovery kernel to the recovery chain + chain = append(chain, NewBootFile(kernelPath, "kernel.efi", RoleRecovery)) + + return chain, nil +} + +// BootChain returns the load chain for run mode. +// It should be called on a RoleRecovery bootloader passing the +// RoleRunMode bootloader. +func (g *grub) BootChain(runBl Bootloader, kernelPath string) ([]BootFile, error) { + if !g.recovery { + return nil, fmt.Errorf("not a recovery bootloader") + } + if runBl.Name() != "grub" { + return nil, fmt.Errorf("run mode bootloader must be grub") + } + + // add trusted assets to the recovery chain + chain := make([]BootFile, 0, len(grubRecoveryModeTrustedAssets)+len(grubRunModeTrustedAssets)+1) + for _, ta := range grubRecoveryModeTrustedAssets { + chain = append(chain, NewBootFile("", ta, RoleRecovery)) + } + for _, ta := range grubRunModeTrustedAssets { + chain = append(chain, NewBootFile("", ta, RoleRunMode)) + } + // add kernel to the boot chain + chain = append(chain, NewBootFile(kernelPath, "kernel.efi", RoleRunMode)) + + return chain, nil +} diff -Nru snapd-2.45.1+20.04.2/bootloader/grub_test.go snapd-2.48.3+20.04/bootloader/grub_test.go --- snapd-2.45.1+20.04.2/bootloader/grub_test.go 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/bootloader/grub_test.go 2021-02-02 08:21:12.000000000 +0000 @@ -30,6 +30,7 @@ . "gopkg.in/check.v1" "github.com/snapcore/snapd/bootloader" + "github.com/snapcore/snapd/bootloader/assets" "github.com/snapcore/snapd/bootloader/grubenv" "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/osutil" @@ -98,11 +99,6 @@ s.grubEditenvSet(c, "k", "v") } -func (s *grubTestSuite) TestNewGrubNoGrubReturnsNil(c *C) { - g := bootloader.NewGrub("/something/not/there", nil) - c.Assert(g, IsNil) -} - func (s *grubTestSuite) TestNewGrub(c *C) { s.makeFakeGrubEnv(c) @@ -229,37 +225,37 @@ return filepath.Join(s.bootdir, "grub") } -func (s *grubTestSuite) grubRecoveryDir() string { +func (s *grubTestSuite) grubEFINativeDir() string { return filepath.Join(s.rootdir, "EFI/ubuntu") } -func (s *grubTestSuite) makeFakeGrubRecoveryEnv(c *C) { - err := os.MkdirAll(s.grubRecoveryDir(), 0755) +func (s *grubTestSuite) makeFakeGrubEFINativeEnv(c *C, content []byte) { + err := os.MkdirAll(s.grubEFINativeDir(), 0755) c.Assert(err, IsNil) - err = ioutil.WriteFile(filepath.Join(s.grubRecoveryDir(), "grub.cfg"), nil, 0644) + err = ioutil.WriteFile(filepath.Join(s.grubEFINativeDir(), "grub.cfg"), content, 0644) c.Assert(err, IsNil) } func (s *grubTestSuite) TestNewGrubWithOptionRecovery(c *C) { - s.makeFakeGrubRecoveryEnv(c) + s.makeFakeGrubEFINativeEnv(c, nil) - g := bootloader.NewGrub(s.rootdir, &bootloader.Options{Recovery: true}) + g := bootloader.NewGrub(s.rootdir, &bootloader.Options{Role: bootloader.RoleRecovery}) c.Assert(g, NotNil) c.Assert(g.Name(), Equals, "grub") } func (s *grubTestSuite) TestNewGrubWithOptionRecoveryBootEnv(c *C) { - s.makeFakeGrubRecoveryEnv(c) - g := bootloader.NewGrub(s.rootdir, &bootloader.Options{Recovery: true}) + s.makeFakeGrubEFINativeEnv(c, nil) + g := bootloader.NewGrub(s.rootdir, &bootloader.Options{Role: bootloader.RoleRecovery}) // check that setting vars goes to the right place - c.Check(filepath.Join(s.grubRecoveryDir(), "grubenv"), testutil.FileAbsent) + c.Check(filepath.Join(s.grubEFINativeDir(), "grubenv"), testutil.FileAbsent) err := g.SetBootVars(map[string]string{ "k1": "v1", "k2": "v2", }) c.Assert(err, IsNil) - c.Check(filepath.Join(s.grubRecoveryDir(), "grubenv"), testutil.FilePresent) + c.Check(filepath.Join(s.grubEFINativeDir(), "grubenv"), testutil.FilePresent) env, err := g.GetBootVars("k1", "k2") c.Assert(err, IsNil) @@ -274,13 +270,14 @@ s.makeFakeGrubEnv(c) // we can't create a recovery grub with that - g := bootloader.NewGrub(s.rootdir, &bootloader.Options{Recovery: true}) + g, err := bootloader.Find(s.rootdir, &bootloader.Options{Role: bootloader.RoleRecovery}) c.Assert(g, IsNil) + c.Assert(err, Equals, bootloader.ErrBootloader) } func (s *grubTestSuite) TestGrubSetRecoverySystemEnv(c *C) { - s.makeFakeGrubRecoveryEnv(c) - g := bootloader.NewGrub(s.rootdir, &bootloader.Options{Recovery: true}) + s.makeFakeGrubEFINativeEnv(c, nil) + g := bootloader.NewGrub(s.rootdir, &bootloader.Options{Role: bootloader.RoleRecovery}) // check that we can set a recovery system specific bootenv bvars := map[string]string{ @@ -300,6 +297,36 @@ c.Check(genv.Get("other_options"), Equals, "are-supported") } +func (s *grubTestSuite) TestGetRecoverySystemEnv(c *C) { + s.makeFakeGrubEFINativeEnv(c, nil) + g := bootloader.NewGrub(s.rootdir, &bootloader.Options{Role: bootloader.RoleRecovery}) + + err := os.MkdirAll(filepath.Join(s.rootdir, "/systems/20191209"), 0755) + c.Assert(err, IsNil) + recoverySystemGrubenv := filepath.Join(s.rootdir, "/systems/20191209/grubenv") + + // does not fail when there is no recovery env + value, err := g.GetRecoverySystemEnv("/systems/20191209", "no_file") + c.Assert(err, IsNil) + c.Check(value, Equals, "") + + genv := grubenv.NewEnv(recoverySystemGrubenv) + genv.Set("snapd_extra_cmdline_args", "foo bar baz") + genv.Set("random_option", `has "some spaces"`) + err = genv.Save() + c.Assert(err, IsNil) + + value, err = g.GetRecoverySystemEnv("/systems/20191209", "snapd_extra_cmdline_args") + c.Assert(err, IsNil) + c.Check(value, Equals, "foo bar baz") + value, err = g.GetRecoverySystemEnv("/systems/20191209", "random_option") + c.Assert(err, IsNil) + c.Check(value, Equals, `has "some spaces"`) + value, err = g.GetRecoverySystemEnv("/systems/20191209", "not_set") + c.Assert(err, IsNil) + c.Check(value, Equals, ``) +} + func (s *grubTestSuite) makeKernelAssetSnap(c *C, snapFileName string) snap.PlaceInfo { kernelSnap, err := snap.ParsePlaceInfoFromSnapFileName(snapFileName) c.Assert(err, IsNil) @@ -444,6 +471,10 @@ } func (s *grubTestSuite) TestGrubExtractedRunKernelImageDisableTryKernel(c *C) { + if os.Geteuid() == 0 { + c.Skip("the test cannot be run by the root user") + } + s.makeFakeGrubEnv(c) g := bootloader.NewGrub(s.rootdir, nil) eg, ok := g.(bootloader.ExtractedRunKernelImageBootloader) @@ -479,7 +510,7 @@ func (s *grubTestSuite) TestKernelExtractionRunImageKernel(c *C) { s.makeFakeGrubEnv(c) - g := bootloader.NewGrub(s.rootdir, &bootloader.Options{ExtractedRunKernelImage: true}) + g := bootloader.NewGrub(s.rootdir, &bootloader.Options{Role: bootloader.RoleRunMode}) c.Assert(g, NotNil) files := [][]string{ @@ -519,9 +550,9 @@ func (s *grubTestSuite) TestKernelExtractionRunImageKernelNoSlashBoot(c *C) { // this is ubuntu-boot but during install we use the native EFI/ubuntu // layout, same as Recovery, without the /boot mount - s.makeFakeGrubRecoveryEnv(c) + s.makeFakeGrubEFINativeEnv(c, nil) - g := bootloader.NewGrub(s.rootdir, &bootloader.Options{ExtractedRunKernelImage: true, NoSlashBoot: true}) + g := bootloader.NewGrub(s.rootdir, &bootloader.Options{Role: bootloader.RoleRunMode, NoSlashBoot: true}) c.Assert(g, NotNil) files := [][]string{ @@ -569,3 +600,482 @@ c.Assert(err, IsNil) c.Check(exists, Equals, false) } + +func (s *grubTestSuite) TestListManagedAssets(c *C) { + s.makeFakeGrubEFINativeEnv(c, []byte(`this is +some random boot config`)) + + opts := &bootloader.Options{NoSlashBoot: true} + g := bootloader.NewGrub(s.rootdir, opts) + c.Assert(g, NotNil) + + tg, ok := g.(bootloader.TrustedAssetsBootloader) + c.Assert(ok, Equals, true) + + c.Check(tg.ManagedAssets(), DeepEquals, []string{ + "EFI/ubuntu/grub.cfg", + }) + + opts = &bootloader.Options{Role: bootloader.RoleRecovery} + tg = bootloader.NewGrub(s.rootdir, opts).(bootloader.TrustedAssetsBootloader) + c.Check(tg.ManagedAssets(), DeepEquals, []string{ + "EFI/ubuntu/grub.cfg", + }) + + // as it called for the root fs rather than a mount point of a partition + // with boot assets + tg = bootloader.NewGrub(s.rootdir, nil).(bootloader.TrustedAssetsBootloader) + c.Check(tg.ManagedAssets(), DeepEquals, []string{ + "boot/grub/grub.cfg", + }) +} + +func (s *grubTestSuite) TestRecoveryUpdateBootConfigNoEdition(c *C) { + // native EFI/ubuntu setup + s.makeFakeGrubEFINativeEnv(c, []byte("recovery boot script")) + + opts := &bootloader.Options{Role: bootloader.RoleRecovery} + g := bootloader.NewGrub(s.rootdir, opts) + c.Assert(g, NotNil) + + restore := assets.MockInternal("grub-recovery.cfg", []byte(`# Snapd-Boot-Config-Edition: 5 +this is mocked grub-recovery.conf +`)) + defer restore() + + tg, ok := g.(bootloader.TrustedAssetsBootloader) + c.Assert(ok, Equals, true) + // install the recovery boot script + err := tg.UpdateBootConfig(opts) + c.Assert(err, IsNil) + + c.Assert(filepath.Join(s.grubEFINativeDir(), "grub.cfg"), testutil.FileEquals, `recovery boot script`) +} + +func (s *grubTestSuite) TestRecoveryUpdateBootConfigUpdates(c *C) { + // native EFI/ubuntu setup + s.makeFakeGrubEFINativeEnv(c, []byte(`# Snapd-Boot-Config-Edition: 1 +recovery boot script`)) + + opts := &bootloader.Options{Role: bootloader.RoleRecovery} + g := bootloader.NewGrub(s.rootdir, opts) + c.Assert(g, NotNil) + + restore := assets.MockInternal("grub-recovery.cfg", []byte(`# Snapd-Boot-Config-Edition: 3 +this is mocked grub-recovery.conf +`)) + defer restore() + restore = assets.MockInternal("grub.cfg", []byte(`# Snapd-Boot-Config-Edition: 4 +this is mocked grub.conf +`)) + defer restore() + tg, ok := g.(bootloader.TrustedAssetsBootloader) + c.Assert(ok, Equals, true) + // install the recovery boot script + err := tg.UpdateBootConfig(opts) + c.Assert(err, IsNil) + // the recovery boot asset was picked + c.Assert(filepath.Join(s.grubEFINativeDir(), "grub.cfg"), testutil.FileEquals, `# Snapd-Boot-Config-Edition: 3 +this is mocked grub-recovery.conf +`) +} + +func (s *grubTestSuite) testBootUpdateBootConfigUpdates(c *C, oldConfig, newConfig string, update bool) { + // native EFI/ubuntu setup + s.makeFakeGrubEFINativeEnv(c, []byte(oldConfig)) + + opts := &bootloader.Options{NoSlashBoot: true} + g := bootloader.NewGrub(s.rootdir, opts) + c.Assert(g, NotNil) + + restore := assets.MockInternal("grub.cfg", []byte(newConfig)) + defer restore() + + tg, ok := g.(bootloader.TrustedAssetsBootloader) + c.Assert(ok, Equals, true) + err := tg.UpdateBootConfig(opts) + c.Assert(err, IsNil) + if update { + c.Assert(filepath.Join(s.grubEFINativeDir(), "grub.cfg"), testutil.FileEquals, newConfig) + } else { + c.Assert(filepath.Join(s.grubEFINativeDir(), "grub.cfg"), testutil.FileEquals, oldConfig) + } +} + +func (s *grubTestSuite) TestNoSlashBootUpdateBootConfigNoUpdateWhenNotManaged(c *C) { + oldConfig := `not managed` + newConfig := `# Snapd-Boot-Config-Edition: 3 +this update is not applied +` + // the current boot config is not managed, no update applied + const updateApplied = false + s.testBootUpdateBootConfigUpdates(c, oldConfig, newConfig, updateApplied) +} + +func (s *grubTestSuite) TestNoSlashBootUpdateBootConfigUpdates(c *C) { + oldConfig := `# Snapd-Boot-Config-Edition: 2 +boot script +` + // edition is higher, update is applied + newConfig := `# Snapd-Boot-Config-Edition: 3 +this is updated grub.cfg +` + const updateApplied = true + s.testBootUpdateBootConfigUpdates(c, oldConfig, newConfig, updateApplied) +} + +func (s *grubTestSuite) TestNoSlashBootUpdateBootConfigNoUpdate(c *C) { + oldConfig := `# Snapd-Boot-Config-Edition: 2 +boot script +` + // edition is lower, no update is applied + newConfig := `# Snapd-Boot-Config-Edition: 1 +this is updated grub.cfg +` + const updateApplied = false + s.testBootUpdateBootConfigUpdates(c, oldConfig, newConfig, updateApplied) +} + +func (s *grubTestSuite) TestNoSlashBootUpdateBootConfigSameEdition(c *C) { + oldConfig := `# Snapd-Boot-Config-Edition: 1 +boot script +` + // edition is equal, no update is applied + newConfig := `# Snapd-Boot-Config-Edition: 1 +this is updated grub.cfg +` + const updateApplied = false + s.testBootUpdateBootConfigUpdates(c, oldConfig, newConfig, updateApplied) +} + +func (s *grubTestSuite) TestBootUpdateBootConfigTrivialErr(c *C) { + if os.Geteuid() == 0 { + c.Skip("the test cannot be run by the root user") + } + + oldConfig := `# Snapd-Boot-Config-Edition: 2 +boot script +` + // edition is higher, update is applied + newConfig := `# Snapd-Boot-Config-Edition: 3 +this is updated grub.cfg +` + // native EFI/ubuntu setup + s.makeFakeGrubEFINativeEnv(c, []byte(oldConfig)) + restore := assets.MockInternal("grub.cfg", []byte(newConfig)) + defer restore() + + opts := &bootloader.Options{NoSlashBoot: true} + g := bootloader.NewGrub(s.rootdir, opts) + c.Assert(g, NotNil) + tg, ok := g.(bootloader.TrustedAssetsBootloader) + c.Assert(ok, Equals, true) + + err := os.Chmod(s.grubEFINativeDir(), 0000) + c.Assert(err, IsNil) + defer os.Chmod(s.grubEFINativeDir(), 0755) + + err = tg.UpdateBootConfig(opts) + c.Assert(err, ErrorMatches, "cannot load existing config asset: .*/EFI/ubuntu/grub.cfg: permission denied") + err = os.Chmod(s.grubEFINativeDir(), 0555) + c.Assert(err, IsNil) + + c.Assert(filepath.Join(s.grubEFINativeDir(), "grub.cfg"), testutil.FileEquals, oldConfig) + + // writing out new config fails + err = os.Chmod(s.grubEFINativeDir(), 0111) + c.Assert(err, IsNil) + err = tg.UpdateBootConfig(opts) + c.Assert(err, ErrorMatches, `open .*/EFI/ubuntu/grub.cfg\..+: permission denied`) + c.Assert(filepath.Join(s.grubEFINativeDir(), "grub.cfg"), testutil.FileEquals, oldConfig) +} + +func (s *grubTestSuite) TestStaticCmdlineForGrubAsset(c *C) { + restore := assets.MockSnippetsForEdition("grub-asset:static-cmdline", []assets.ForEditions{ + {FirstEdition: 2, Snippet: []byte(`static cmdline "with spaces"`)}, + }) + defer restore() + cmdline := bootloader.StaticCommandLineForGrubAssetEdition("grub-asset", 1) + c.Check(cmdline, Equals, ``) + cmdline = bootloader.StaticCommandLineForGrubAssetEdition("grub-asset", 2) + c.Check(cmdline, Equals, `static cmdline "with spaces"`) + cmdline = bootloader.StaticCommandLineForGrubAssetEdition("grub-asset", 4) + c.Check(cmdline, Equals, `static cmdline "with spaces"`) +} + +func (s *grubTestSuite) TestCommandLineNotManaged(c *C) { + grubCfg := "boot script\n" + + // native EFI/ubuntu setup + s.makeFakeGrubEFINativeEnv(c, []byte(grubCfg)) + + restore := assets.MockSnippetsForEdition("grub.cfg:static-cmdline", []assets.ForEditions{ + {FirstEdition: 1, Snippet: []byte(`static=1`)}, + {FirstEdition: 2, Snippet: []byte(`static=2`)}, + }) + defer restore() + restore = assets.MockSnippetsForEdition("grub-recovery.cfg:static-cmdline", []assets.ForEditions{ + {FirstEdition: 1, Snippet: []byte(`static=1 recovery`)}, + {FirstEdition: 2, Snippet: []byte(`static=2 recovery`)}, + }) + defer restore() + + opts := &bootloader.Options{NoSlashBoot: true} + mg := bootloader.NewGrub(s.rootdir, opts).(bootloader.TrustedAssetsBootloader) + + args, err := mg.CommandLine("snapd_recovery_mode=run", "", "extra") + c.Assert(err, IsNil) + c.Check(args, Equals, "snapd_recovery_mode=run static=1 extra") + + optsRecovery := &bootloader.Options{NoSlashBoot: true, Role: bootloader.RoleRecovery} + mgr := bootloader.NewGrub(s.rootdir, optsRecovery).(bootloader.TrustedAssetsBootloader) + + args, err = mgr.CommandLine("snapd_recovery_mode=recover", "snapd_recovery_system=1234", "extra") + c.Assert(err, IsNil) + c.Check(args, Equals, "snapd_recovery_mode=recover snapd_recovery_system=1234 static=1 recovery extra") +} + +func (s *grubTestSuite) TestCommandLineMocked(c *C) { + grubCfg := `# Snapd-Boot-Config-Edition: 2 +boot script +` + staticCmdline := `arg1 foo=123 panic=-1 arg2="with spaces "` + staticCmdlineEdition3 := `edition=3 static args` + restore := assets.MockSnippetsForEdition("grub.cfg:static-cmdline", []assets.ForEditions{ + {FirstEdition: 1, Snippet: []byte(staticCmdline)}, + {FirstEdition: 3, Snippet: []byte(staticCmdlineEdition3)}, + }) + defer restore() + staticCmdlineRecovery := `recovery config panic=-1` + restore = assets.MockSnippetsForEdition("grub-recovery.cfg:static-cmdline", []assets.ForEditions{ + {FirstEdition: 1, Snippet: []byte(staticCmdlineRecovery)}, + }) + defer restore() + + // native EFI/ubuntu setup + s.makeFakeGrubEFINativeEnv(c, []byte(grubCfg)) + + optsNoSlashBoot := &bootloader.Options{NoSlashBoot: true} + g := bootloader.NewGrub(s.rootdir, optsNoSlashBoot) + c.Assert(g, NotNil) + tg, ok := g.(bootloader.TrustedAssetsBootloader) + c.Assert(ok, Equals, true) + + extraArgs := `extra_arg=1 extra_foo=-1 panic=3 baz="more spaces"` + args, err := tg.CommandLine("snapd_recovery_mode=run", "", extraArgs) + c.Assert(err, IsNil) + c.Check(args, Equals, `snapd_recovery_mode=run arg1 foo=123 panic=-1 arg2="with spaces " extra_arg=1 extra_foo=-1 panic=3 baz="more spaces"`) + + // empty mode/system do not produce confusing results + args, err = tg.CommandLine("", "", extraArgs) + c.Assert(err, IsNil) + c.Check(args, Equals, `arg1 foo=123 panic=-1 arg2="with spaces " extra_arg=1 extra_foo=-1 panic=3 baz="more spaces"`) + + // now check the recovery bootloader + optsRecovery := &bootloader.Options{NoSlashBoot: true, Role: bootloader.RoleRecovery} + mrg := bootloader.NewGrub(s.rootdir, optsRecovery).(bootloader.TrustedAssetsBootloader) + args, err = mrg.CommandLine("snapd_recovery_mode=recover", "snapd_recovery_system=20200202", extraArgs) + c.Assert(err, IsNil) + // static command line from recovery asset + c.Check(args, Equals, `snapd_recovery_mode=recover snapd_recovery_system=20200202 recovery config panic=-1 extra_arg=1 extra_foo=-1 panic=3 baz="more spaces"`) + + // try with a different edition + grubCfg3 := `# Snapd-Boot-Config-Edition: 3 +boot script +` + s.makeFakeGrubEFINativeEnv(c, []byte(grubCfg3)) + tg = bootloader.NewGrub(s.rootdir, optsNoSlashBoot).(bootloader.TrustedAssetsBootloader) + c.Assert(g, NotNil) + extraArgs = `extra_arg=1` + args, err = tg.CommandLine("snapd_recovery_mode=run", "", extraArgs) + c.Assert(err, IsNil) + c.Check(args, Equals, `snapd_recovery_mode=run edition=3 static args extra_arg=1`) +} + +func (s *grubTestSuite) TestCandidateCommandLineMocked(c *C) { + grubCfg := `# Snapd-Boot-Config-Edition: 1 +boot script +` + // edition on disk + s.makeFakeGrubEFINativeEnv(c, []byte(grubCfg)) + + edition2 := []byte(`# Snapd-Boot-Config-Edition: 2`) + edition3 := []byte(`# Snapd-Boot-Config-Edition: 3`) + edition4 := []byte(`# Snapd-Boot-Config-Edition: 4`) + + restore := assets.MockInternal("grub.cfg", edition2) + defer restore() + restore = assets.MockInternal("grub-recovery.cfg", edition2) + defer restore() + + restore = assets.MockSnippetsForEdition("grub.cfg:static-cmdline", []assets.ForEditions{ + {FirstEdition: 1, Snippet: []byte(`edition=1`)}, + {FirstEdition: 3, Snippet: []byte(`edition=3`)}, + }) + defer restore() + restore = assets.MockSnippetsForEdition("grub-recovery.cfg:static-cmdline", []assets.ForEditions{ + {FirstEdition: 1, Snippet: []byte(`recovery edition=1`)}, + {FirstEdition: 3, Snippet: []byte(`recovery edition=3`)}, + {FirstEdition: 4, Snippet: []byte(`recovery edition=4up`)}, + }) + defer restore() + + optsNoSlashBoot := &bootloader.Options{NoSlashBoot: true} + mg := bootloader.NewGrub(s.rootdir, optsNoSlashBoot).(bootloader.TrustedAssetsBootloader) + optsRecovery := &bootloader.Options{NoSlashBoot: true, Role: bootloader.RoleRecovery} + recoverymg := bootloader.NewGrub(s.rootdir, optsRecovery).(bootloader.TrustedAssetsBootloader) + + args, err := mg.CandidateCommandLine("snapd_recovery_mode=run", "", "extra=1") + c.Assert(err, IsNil) + c.Check(args, Equals, `snapd_recovery_mode=run edition=1 extra=1`) + args, err = recoverymg.CandidateCommandLine("snapd_recovery_mode=recover", "snapd_recovery_system=20200202", "extra=1") + c.Assert(err, IsNil) + c.Check(args, Equals, `snapd_recovery_mode=recover snapd_recovery_system=20200202 recovery edition=1 extra=1`) + + restore = assets.MockInternal("grub.cfg", edition3) + defer restore() + restore = assets.MockInternal("grub-recovery.cfg", edition3) + defer restore() + + args, err = mg.CandidateCommandLine("snapd_recovery_mode=run", "", "extra=1") + c.Assert(err, IsNil) + c.Check(args, Equals, `snapd_recovery_mode=run edition=3 extra=1`) + args, err = recoverymg.CandidateCommandLine("snapd_recovery_mode=recover", "snapd_recovery_system=20200202", "extra=1") + c.Assert(err, IsNil) + c.Check(args, Equals, `snapd_recovery_mode=recover snapd_recovery_system=20200202 recovery edition=3 extra=1`) + + // bump edition only for recovery + restore = assets.MockInternal("grub-recovery.cfg", edition4) + defer restore() + // boot bootloader unchanged + args, err = mg.CandidateCommandLine("snapd_recovery_mode=run", "", "extra=1") + c.Assert(err, IsNil) + c.Check(args, Equals, `snapd_recovery_mode=run edition=3 extra=1`) + // recovery uses a new edition + args, err = recoverymg.CandidateCommandLine("snapd_recovery_mode=recover", "snapd_recovery_system=20200202", "extra=1") + c.Assert(err, IsNil) + c.Check(args, Equals, `snapd_recovery_mode=recover snapd_recovery_system=20200202 recovery edition=4up extra=1`) +} + +func (s *grubTestSuite) TestCommandLineReal(c *C) { + grubCfg := `# Snapd-Boot-Config-Edition: 1 +boot script +` + // native EFI/ubuntu setup + s.makeFakeGrubEFINativeEnv(c, []byte(grubCfg)) + + opts := &bootloader.Options{NoSlashBoot: true} + g := bootloader.NewGrub(s.rootdir, opts) + c.Assert(g, NotNil) + tg, ok := g.(bootloader.TrustedAssetsBootloader) + c.Assert(ok, Equals, true) + + extraArgs := "foo bar baz=1" + args, err := tg.CommandLine("snapd_recovery_mode=run", "", extraArgs) + c.Assert(err, IsNil) + c.Check(args, Equals, `snapd_recovery_mode=run console=ttyS0 console=tty1 panic=-1 foo bar baz=1`) + + // now check the recovery bootloader + opts = &bootloader.Options{NoSlashBoot: true, Role: bootloader.RoleRecovery} + mrg := bootloader.NewGrub(s.rootdir, opts).(bootloader.TrustedAssetsBootloader) + args, err = mrg.CommandLine("snapd_recovery_mode=recover", "snapd_recovery_system=20200202", extraArgs) + c.Assert(err, IsNil) + // static command line from recovery asset + c.Check(args, Equals, `snapd_recovery_mode=recover snapd_recovery_system=20200202 console=ttyS0 console=tty1 panic=-1 foo bar baz=1`) +} + +func (s *grubTestSuite) TestTrustedAssetsNativePartitionLayout(c *C) { + // native EFI/ubuntu setup + s.makeFakeGrubEFINativeEnv(c, []byte("grub.cfg")) + opts := &bootloader.Options{NoSlashBoot: true} + g := bootloader.NewGrub(s.rootdir, opts) + c.Assert(g, NotNil) + + tab, ok := g.(bootloader.TrustedAssetsBootloader) + c.Assert(ok, Equals, true) + + ta, err := tab.TrustedAssets() + c.Assert(err, IsNil) + c.Check(ta, DeepEquals, []string{ + "EFI/boot/grubx64.efi", + }) + + // recovery bootloader + recoveryOpts := &bootloader.Options{NoSlashBoot: true, Role: bootloader.RoleRecovery} + tarb := bootloader.NewGrub(s.rootdir, recoveryOpts).(bootloader.TrustedAssetsBootloader) + c.Assert(tarb, NotNil) + + ta, err = tarb.TrustedAssets() + c.Assert(err, IsNil) + c.Check(ta, DeepEquals, []string{ + "EFI/boot/bootx64.efi", + "EFI/boot/grubx64.efi", + }) + +} + +func (s *grubTestSuite) TestTrustedAssetsRoot(c *C) { + s.makeFakeGrubEnv(c) + g := bootloader.NewGrub(s.rootdir, nil) + tab, ok := g.(bootloader.TrustedAssetsBootloader) + c.Assert(ok, Equals, true) + + ta, err := tab.TrustedAssets() + c.Assert(err, ErrorMatches, "internal error: trusted assets called without native host-partition layout") + c.Check(ta, IsNil) +} + +func (s *grubTestSuite) TestRecoveryBootChains(c *C) { + s.makeFakeGrubEFINativeEnv(c, nil) + g := bootloader.NewGrub(s.rootdir, &bootloader.Options{Role: bootloader.RoleRecovery}) + tab, ok := g.(bootloader.TrustedAssetsBootloader) + c.Assert(ok, Equals, true) + + chain, err := tab.RecoveryBootChain("kernel.snap") + c.Assert(err, IsNil) + c.Assert(chain, DeepEquals, []bootloader.BootFile{ + {Path: "EFI/boot/bootx64.efi", Role: bootloader.RoleRecovery}, + {Path: "EFI/boot/grubx64.efi", Role: bootloader.RoleRecovery}, + {Snap: "kernel.snap", Path: "kernel.efi", Role: bootloader.RoleRecovery}, + }) +} + +func (s *grubTestSuite) TestRecoveryBootChainsNotRecoveryBootloader(c *C) { + s.makeFakeGrubEnv(c) + g := bootloader.NewGrub(s.rootdir, nil) + tab, ok := g.(bootloader.TrustedAssetsBootloader) + c.Assert(ok, Equals, true) + + _, err := tab.RecoveryBootChain("kernel.snap") + c.Assert(err, ErrorMatches, "not a recovery bootloader") +} + +func (s *grubTestSuite) TestBootChains(c *C) { + s.makeFakeGrubEFINativeEnv(c, nil) + g := bootloader.NewGrub(s.rootdir, &bootloader.Options{Role: bootloader.RoleRecovery}) + tab, ok := g.(bootloader.TrustedAssetsBootloader) + c.Assert(ok, Equals, true) + + g2 := bootloader.NewGrub(s.rootdir, &bootloader.Options{Role: bootloader.RoleRunMode}) + + chain, err := tab.BootChain(g2, "kernel.snap") + c.Assert(err, IsNil) + c.Assert(chain, DeepEquals, []bootloader.BootFile{ + {Path: "EFI/boot/bootx64.efi", Role: bootloader.RoleRecovery}, + {Path: "EFI/boot/grubx64.efi", Role: bootloader.RoleRecovery}, + {Path: "EFI/boot/grubx64.efi", Role: bootloader.RoleRunMode}, + {Snap: "kernel.snap", Path: "kernel.efi", Role: bootloader.RoleRunMode}, + }) +} + +func (s *grubTestSuite) TestBootChainsNotRecoveryBootloader(c *C) { + s.makeFakeGrubEnv(c) + g := bootloader.NewGrub(s.rootdir, nil) + tab, ok := g.(bootloader.TrustedAssetsBootloader) + c.Assert(ok, Equals, true) + + g2 := bootloader.NewGrub(s.rootdir, &bootloader.Options{NoSlashBoot: true, Role: bootloader.RoleRunMode}) + + _, err := tab.BootChain(g2, "kernel.snap") + c.Assert(err, ErrorMatches, "not a recovery bootloader") +} diff -Nru snapd-2.45.1+20.04.2/bootloader/lkenv/lkenv.go snapd-2.48.3+20.04/bootloader/lkenv/lkenv.go --- snapd-2.45.1+20.04.2/bootloader/lkenv/lkenv.go 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/bootloader/lkenv/lkenv.go 2021-02-02 08:21:12.000000000 +0000 @@ -51,7 +51,7 @@ /** * Following structure has to be kept in sync with c structure defined by * include/lk/snappy-boot_v1.h - * c headerfile is used by bootloader, this ensures sync of the environment + * c headerfile is used by bootloader, this ensures sync of the environment * between snapd and bootloader * when this structure needs to be updated, @@ -93,7 +93,7 @@ Reboot_reason [SNAP_NAME_MAX_LEN]byte /** - * Matrix for mapping of boot img partion to installed kernel snap revision + * Matrix for mapping of boot img partition to installed kernel snap revision * * First column represents boot image partition label (e.g. boot_a,boot_b ) * value are static and should be populated at gadget built time @@ -134,7 +134,7 @@ Bootimg_file_name [SNAP_NAME_MAX_LEN]byte /** - * gadget assets: Matrix for mapping of gadget asset partions + * gadget assets: Matrix for mapping of gadget asset partitions * Optional boot asset tracking, based on bootloader support * Some boot chains support A/B boot assets for increased robustness * example being A/B TrustExecutionEnvironment @@ -170,7 +170,7 @@ Unused_key_20 [SNAP_NAME_MAX_LEN]byte /* unused array of 10 key value pairs */ - Kye_value_pairs [10][2][SNAP_NAME_MAX_LEN]byte + Key_value_pairs [10][2][SNAP_NAME_MAX_LEN]byte /* crc32 value for structure */ Crc32 uint32 @@ -337,20 +337,20 @@ w.Truncate(ss - 4) binary.Write(w, binary.LittleEndian, &l.env.Crc32) - err := l.SaveEnv(l.path, w) + err := l.saveEnv(l.path, w) if err != nil { logger.Debugf("Save: failed to save main environment") } // if there is backup environment file save to it as well if osutil.FileExists(l.pathbak) { - if err := l.SaveEnv(l.pathbak, w); err != nil { + if err := l.saveEnv(l.pathbak, w); err != nil { logger.Debugf("Save: failed to save backup environment %v", err) } } return err } -func (l *Env) SaveEnv(path string, buf *bytes.Buffer) error { +func (l *Env) saveEnv(path string, buf *bytes.Buffer) error { f, err := os.OpenFile(path, os.O_WRONLY, 0660) if err != nil { return fmt.Errorf("cannot open LK env file for env storing: %v", err) @@ -384,7 +384,9 @@ return "", fmt.Errorf("cannot find free partition for boot image") } -// SetBootPartition set kernel revision name to passed boot partition +// SetBootPartition sets the kernel revision reference in the provided boot +// partition reference to the provided kernel revision. It returns a non-nil err +// if the provided boot partition reference was not found. func (l *Env) SetBootPartition(bootpart, kernel string) error { for x := range l.env.Bootimg_matrix { if bootpart == cToGoString(l.env.Bootimg_matrix[x][MATRIX_ROW_PARTITION][:]) { @@ -395,6 +397,9 @@ return fmt.Errorf("cannot find defined [%s] boot image partition", bootpart) } +// GetBootPartition returns the first found boot partition that contains a +// reference to the given kernel revision. If the revision was not found, a +// non-nil error is returned. func (l *Env) GetBootPartition(kernel string) (string, error) { for x := range l.env.Bootimg_matrix { if kernel == cToGoString(l.env.Bootimg_matrix[x][MATRIX_ROW_KERNEL][:]) { @@ -404,18 +409,21 @@ return "", fmt.Errorf("cannot find kernel %q in boot image partitions", kernel) } -// FreeBootPartition free passed kernel revision from any boot partition -// ignore if there is no boot partition with given kernel revision -func (l *Env) FreeBootPartition(kernel string) (bool, error) { +// RemoveKernelRevisionFromBootPartition removes from the boot image matrix the +// first found boot partition that contains a reference to the given kernel +// revision. If the referenced kernel revision was not found, a non-nil err is +// returned, otherwise the reference is removed and nil is returned. +// Note that to persist this change the env must be saved afterwards with Save. +func (l *Env) RemoveKernelRevisionFromBootPartition(kernel string) error { for x := range l.env.Bootimg_matrix { if "" != cToGoString(l.env.Bootimg_matrix[x][MATRIX_ROW_PARTITION][:]) { if kernel == cToGoString(l.env.Bootimg_matrix[x][MATRIX_ROW_KERNEL][:]) { l.env.Bootimg_matrix[x][1][MATRIX_ROW_PARTITION] = 0 - return true, nil + return nil } } } - return false, fmt.Errorf("cannot find defined [%s] boot image partition", kernel) + return fmt.Errorf("cannot find defined [%s] boot image partition", kernel) } // GetBootImageName return expected boot image file name in kernel snap diff -Nru snapd-2.45.1+20.04.2/bootloader/lkenv/lkenv_test.go snapd-2.48.3+20.04/bootloader/lkenv/lkenv_test.go --- snapd-2.45.1+20.04.2/bootloader/lkenv/lkenv_test.go 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/bootloader/lkenv/lkenv_test.go 2021-02-02 08:21:12.000000000 +0000 @@ -276,13 +276,11 @@ err = env.SetBootPartition("boot_a", "kernel-3") c.Assert(err, IsNil) // remove kernel - used, err := env.FreeBootPartition("kernel-3") + err = env.RemoveKernelRevisionFromBootPartition("kernel-3") c.Assert(err, IsNil) - c.Check(used, Equals, true) // repeated use should return false and error - used, err = env.FreeBootPartition("kernel-3") + err = env.RemoveKernelRevisionFromBootPartition("kernel-3") c.Assert(err, NotNil) - c.Check(used, Equals, false) } func (l *lkenvTestSuite) TestZippedDataSample(c *C) { diff -Nru snapd-2.45.1+20.04.2/bootloader/lk.go snapd-2.48.3+20.04/bootloader/lk.go --- snapd-2.45.1+20.04.2/bootloader/lk.go 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/bootloader/lk.go 2021-02-02 08:21:12.000000000 +0000 @@ -28,7 +28,6 @@ "github.com/snapcore/snapd/bootloader/lkenv" "github.com/snapcore/snapd/logger" - "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/snap" ) @@ -41,17 +40,15 @@ func newLk(rootdir string, opts *Options) Bootloader { l := &lk{rootdir: rootdir} - // XXX: in the long run we want this to go away, we probably add - // something like "boot.PrepareImage()" and add an (optional) - // method "PrepareImage" to the bootloader interface that is - // used to setup a bootloader from prepare-image if things - // are very different from runtime vs image-building mode. - // - // determine mode we are in, runtime or image build - l.inRuntimeMode = !opts.PrepareImageTime - - if !osutil.FileExists(l.envFile()) { - return nil + if opts != nil { + // XXX: in the long run we want this to go away, we probably add + // something like "boot.PrepareImage()" and add an (optional) + // method "PrepareImage" to the bootloader interface that is + // used to setup a bootloader from prepare-image if things + // are very different from runtime vs image-building mode. + // + // determine mode we are in, runtime or image build + l.inRuntimeMode = !opts.PrepareImageTime } return l @@ -75,7 +72,7 @@ return filepath.Join(l.rootdir, "/boot/lk/") } -func (l *lk) InstallBootConfig(gadgetDir string, opts *Options) (bool, error) { +func (l *lk) InstallBootConfig(gadgetDir string, opts *Options) error { gadgetFile := filepath.Join(gadgetDir, l.Name()+".conf") systemFile := l.ConfigFile() return genericInstallBootConfig(gadgetFile, systemFile) @@ -208,8 +205,10 @@ if err := env.Load(); err != nil && !os.IsNotExist(err) { return err } - dirty, _ := env.FreeBootPartition(blobName) - if dirty { + err := env.RemoveKernelRevisionFromBootPartition(blobName) + if err == nil { + // found and removed the revision from the bootimg matrix, need to + // update the env to persist the change return env.Save() } return nil diff -Nru snapd-2.45.1+20.04.2/bootloader/lk_test.go snapd-2.48.3+20.04/bootloader/lk_test.go --- snapd-2.45.1+20.04.2/bootloader/lk_test.go 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/bootloader/lk_test.go 2021-02-02 08:21:12.000000000 +0000 @@ -42,11 +42,6 @@ var _ = Suite(&lkTestSuite{}) -func (s *lkTestSuite) TestNewLkNolkReturnsNil(c *C) { - l := bootloader.NewLk("/does/not/exist", nil) - c.Assert(l, IsNil) -} - func (s *lkTestSuite) TestNewLk(c *C) { bootloader.MockLkFiles(c, s.rootdir, nil) l := bootloader.NewLk(s.rootdir, nil) @@ -227,7 +222,7 @@ // now remove the kernel err = lk.RemoveKernelAssets(info) c.Assert(err, IsNil) - // and ensure its no longer available in the boot partions + // and ensure its no longer available in the boot partitions err = lkenv.Load() c.Assert(err, IsNil) bootPart, err = lkenv.GetBootPartition("ubuntu-kernel_42.snap") diff -Nru snapd-2.45.1+20.04.2/bootloader/ubootenv/env.go snapd-2.48.3+20.04/bootloader/ubootenv/env.go --- snapd-2.45.1+20.04.2/bootloader/ubootenv/env.go 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/bootloader/ubootenv/env.go 2021-02-02 08:21:12.000000000 +0000 @@ -100,6 +100,11 @@ if err != nil { return nil, err } + + if len(contentWithHeader) < headerSize { + return nil, fmt.Errorf("cannot open %q: smaller than expected header", fname) + } + crc := readUint32(contentWithHeader) payload := contentWithHeader[headerSize:] diff -Nru snapd-2.45.1+20.04.2/bootloader/ubootenv/env_test.go snapd-2.48.3+20.04/bootloader/ubootenv/env_test.go --- snapd-2.45.1+20.04.2/bootloader/ubootenv/env_test.go 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/bootloader/ubootenv/env_test.go 2021-02-02 08:21:12.000000000 +0000 @@ -67,6 +67,16 @@ c.Assert(env2.String(), Equals, "foo=bar\n") } +func (u *uenvTestSuite) TestOpenEnvBadEmpty(c *C) { + empty := filepath.Join(c.MkDir(), "empty.env") + + err := ioutil.WriteFile(empty, nil, 0644) + c.Assert(err, IsNil) + + _, err = ubootenv.Open(empty) + c.Assert(err, ErrorMatches, `cannot open ".*": smaller than expected header`) +} + func (u *uenvTestSuite) TestOpenEnvBadCRC(c *C) { corrupted := filepath.Join(c.MkDir(), "corrupted.env") diff -Nru snapd-2.45.1+20.04.2/bootloader/uboot.go snapd-2.48.3+20.04/bootloader/uboot.go --- snapd-2.45.1+20.04.2/bootloader/uboot.go 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/bootloader/uboot.go 2021-02-02 08:21:12.000000000 +0000 @@ -25,7 +25,6 @@ "path/filepath" "github.com/snapcore/snapd/bootloader/ubootenv" - "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/snap" ) @@ -44,32 +43,29 @@ func (u *uboot) processBlOpts(blOpts *Options) { if blOpts != nil { switch { - case blOpts.NoSlashBoot, blOpts.Recovery: - // Recovery or NoSlashBoot imply we use the "boot.sel" simple text - // format file in /uboot/ubuntu as it exists on the partition + case blOpts.Role == RoleRecovery || blOpts.NoSlashBoot: + // RoleRecovery or NoSlashBoot imply we use + // the "boot.sel" simple text format file in + // /uboot/ubuntu as it exists on the partition // directly u.basedir = "/uboot/ubuntu/" fallthrough - case blOpts.ExtractedRunKernelImage: - // if just ExtractedRunKernelImage is defined, we expect to find - // /boot/uboot/boot.sel + case blOpts.Role == RoleRunMode: + // if RoleRunMode (and no NoSlashBoot), we + // expect to find /boot/uboot/boot.sel u.ubootEnvFileName = "boot.sel" } } } // newUboot create a new Uboot bootloader object -func newUboot(rootdir string, blOpts *Options) ExtractedRecoveryKernelImageBootloader { +func newUboot(rootdir string, blOpts *Options) Bootloader { u := &uboot{ rootdir: rootdir, } u.setDefaults() u.processBlOpts(blOpts) - if !osutil.FileExists(u.envFile()) { - return nil - } - return u } @@ -88,7 +84,7 @@ return filepath.Join(u.rootdir, u.basedir) } -func (u *uboot) InstallBootConfig(gadgetDir string, blOpts *Options) (bool, error) { +func (u *uboot) InstallBootConfig(gadgetDir string, blOpts *Options) error { gadgetFile := filepath.Join(gadgetDir, u.Name()+".conf") // if the gadget file is empty, then we don't install anything // this is because there are some gadgets, namely the 20 pi gadget right @@ -99,7 +95,7 @@ // actual format? st, err := os.Stat(gadgetFile) if err != nil { - return false, err + return err } if st.Size() == 0 { // we have an empty uboot.conf, and hence a uboot bootloader in the @@ -109,30 +105,30 @@ err := os.MkdirAll(filepath.Dir(u.envFile()), 0755) if err != nil { - return false, err + return err } // TODO:UC20: what's a reasonable size for this file? env, err := ubootenv.Create(u.envFile(), 4096) if err != nil { - return false, err + return err } if err := env.Save(); err != nil { - return false, nil + return nil } - return true, nil + return nil } // InstallBootConfig gets called on a uboot that does not come from newUboot // so we need to apply the defaults here u.setDefaults() - if blOpts != nil && blOpts.Recovery { + if blOpts != nil && blOpts.Role == RoleRecovery { // not supported yet, this is traditional uboot.env from gadget // TODO:UC20: support this use-case - return false, 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.ConfigFile() diff -Nru snapd-2.45.1+20.04.2/bootloader/uboot_test.go snapd-2.48.3+20.04/bootloader/uboot_test.go --- snapd-2.45.1+20.04.2/bootloader/uboot_test.go 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/bootloader/uboot_test.go 2021-02-02 08:21:12.000000000 +0000 @@ -41,11 +41,6 @@ var _ = Suite(&ubootTestSuite{}) -func (s *ubootTestSuite) TestNewUbootNoUbootReturnsNil(c *C) { - u := bootloader.NewUboot(s.rootdir, nil) - c.Assert(u, IsNil) -} - func (s *ubootTestSuite) TestNewUboot(c *C) { bootloader.MockUbootFiles(c, s.rootdir, nil) u := bootloader.NewUboot(s.rootdir, nil) @@ -234,17 +229,17 @@ "traditional uboot.env", }, { - &bootloader.Options{NoSlashBoot: true}, + &bootloader.Options{Role: bootloader.RoleRunMode, NoSlashBoot: true}, "/uboot/ubuntu/boot.sel", "uc20 install mode boot.sel", }, { - &bootloader.Options{ExtractedRunKernelImage: true}, + &bootloader.Options{Role: bootloader.RoleRunMode}, "/boot/uboot/boot.sel", "uc20 run mode boot.sel", }, { - &bootloader.Options{Recovery: true}, + &bootloader.Options{Role: bootloader.RoleRecovery}, "/uboot/ubuntu/boot.sel", "uc20 recovery boot.sel", }, diff -Nru snapd-2.45.1+20.04.2/check-pr-has-label.py snapd-2.48.3+20.04/check-pr-has-label.py --- snapd-2.45.1+20.04.2/check-pr-has-label.py 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48.3+20.04/check-pr-has-label.py 2021-02-02 08:21:12.000000000 +0000 @@ -0,0 +1,114 @@ +#!/usr/bin/python3 + +import argparse +import urllib.request +import logging + +from html.parser import HTMLParser + +LABELS = { + # PR label indicating that spread job should be skipped + 'LABEL_SKIP_SPREAD_JOB': "Skip spread", + # PR label indicating that nested tests need to be executed + 'LABEL_RUN_SPREAD_NESTED': "Run nested" + } + + +class GithubLabelsParser(HTMLParser): + def __init__(self): + super().__init__() + self.in_labels = False + self.deep = 0 + self.labels = [] + + def handle_starttag(self, tag, attributes): + logging.debug(attributes) + + if not self.in_labels: + attr_class = [attr[1] for attr in attributes if attr[0] == "class"] + if len(attr_class) == 0: + return + # labels are in separate div defined like: + #
+ elems = attr_class[0].split(" ") + if "labels" in elems: + self.in_labels = True + self.deep = 1 + logging.debug("labels start") + else: + # nesting counter + self.deep += 1 + + # inside labels + # label entry has + # + attr_data_name = [attr[1] for attr in attributes if attr[0] == "data-name"] + if len(attr_data_name) == 0: + return + data_name = attr_data_name[0] + logging.debug("found label: %s", data_name) + self.labels.append(data_name) + + def handle_endtag(self, tag): + if self.in_labels: + self.deep -= 1 + if self.deep < 1: + logging.debug("labels end") + self.in_labels = False + + def handle_data(self, data): + if self.in_labels: + logging.debug("data: %s", data) + + +def grab_pr_labels(pr_number: int): + # ideally we would use the github API - however we can't because: + # a) its rate limiting and travis IPs hit the API a lot so we regularly + # get errors + # b) using a API token is tricky because travis will not allow the secure + # vars for forks + # so instead we just scrape the html title which is unlikely to change + # radically + parser = GithubLabelsParser() + with urllib.request.urlopen( + "https://github.com/snapcore/snapd/pull/{}".format(pr_number) + ) as f: + parser.feed(f.read().decode("utf-8")) + return parser.labels + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument( + "pr_number", + metavar="PR number", + help="the github PR number to check" + ) + parser.add_argument( + "label", + metavar="Label name", + choices=LABELS.keys(), + help="the github PR label to check" + ) + parser.add_argument( + "-d", "--debug", help="enable debug logging", action="store_true" + ) + args = parser.parse_args() + + lvl = logging.INFO + if args.debug: + lvl = logging.DEBUG + logging.basicConfig(level=lvl) + + pr_labels = grab_pr_labels(args.pr_number) + print("pr labels:", pr_labels) + + if LABELS.get(args.label) not in pr_labels: + raise SystemExit(1) + + print("Label: '{}' found in PR: '{}'".format( + LABELS.get(args.label), args.pr_number)) + + +if __name__ == "__main__": + main() diff -Nru snapd-2.45.1+20.04.2/check-pr-skip-spread.py snapd-2.48.3+20.04/check-pr-skip-spread.py --- snapd-2.45.1+20.04.2/check-pr-skip-spread.py 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/check-pr-skip-spread.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,101 +0,0 @@ -#!/usr/bin/python3 - -import argparse -import urllib.request -import logging - -from html.parser import HTMLParser - -# PR label indicating that spread job should be skipped -LABEL_SKIP_SPREAD_JOB = "Skip spread" - - -class GithubLabelsParser(HTMLParser): - def __init__(self): - super().__init__() - self.in_labels = False - self.deep = 0 - self.labels = [] - - def handle_starttag(self, tag, attributes): - logging.debug(attributes) - - if not self.in_labels: - attr_class = [attr[1] for attr in attributes if attr[0] == "class"] - if len(attr_class) == 0: - return - # labels are in separate div defined like: - #
- elems = attr_class[0].split(" ") - if "labels" in elems: - self.in_labels = True - self.deep = 1 - logging.debug("labels start") - else: - # nesting counter - self.deep += 1 - - # inside labels - # label entry has - # - attr_data_name = [attr[1] for attr in attributes if attr[0] == "data-name"] - if len(attr_data_name) == 0: - return - data_name = attr_data_name[0] - logging.debug("found label: %s", data_name) - self.labels.append(data_name) - - def handle_endtag(self, tag): - if self.in_labels: - self.deep -= 1 - if self.deep < 1: - logging.debug("labels end") - self.in_labels = False - - def handle_data(self, data): - if self.in_labels: - logging.debug("data: %s", data) - - -def grab_pr_labels(pr_number: int): - # ideally we would use the github API - however we can't because: - # a) its rate limiting and travis IPs hit the API a lot so we regularly - # get errors - # b) using a API token is tricky because travis will not allow the secure - # vars for forks - # so instead we just scrape the html title which is unlikely to change - # radically - parser = GithubLabelsParser() - with urllib.request.urlopen( - "https://github.com/snapcore/snapd/pull/{}".format(pr_number) - ) as f: - parser.feed(f.read().decode("utf-8")) - return parser.labels - - -def main(): - parser = argparse.ArgumentParser() - parser.add_argument( - "pr_number", metavar="PR number", help="the github PR number to check" - ) - parser.add_argument( - "-d", "--debug", help="enable debug logging", action="store_true" - ) - args = parser.parse_args() - - lvl = logging.INFO - if args.debug: - lvl = logging.DEBUG - logging.basicConfig(level=lvl) - - labels = grab_pr_labels(args.pr_number) - print("labels:", labels) - - if LABEL_SKIP_SPREAD_JOB not in labels: - raise SystemExit(1) - - print("requested to skip the spread job") - - -if __name__ == "__main__": - main() diff -Nru snapd-2.45.1+20.04.2/check-pr-title.py snapd-2.48.3+20.04/check-pr-title.py --- snapd-2.45.1+20.04.2/check-pr-title.py 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/check-pr-title.py 2021-02-02 08:21:12.000000000 +0000 @@ -53,7 +53,7 @@ # package, otherpackage/subpackage: this is a title # tests/regression/lp-12341234: foo # [RFC] foo: bar - if not re.match(r"[a-zA-Z0-9_\-/,. \[\]{}]+: .*", title): + if not re.match(r"[a-zA-Z0-9_\-\*/,. \[\]{}]+: .*", title): raise InvalidPRTitle(title) diff -Nru snapd-2.45.1+20.04.2/client/asserts.go snapd-2.48.3+20.04/client/asserts.go --- snapd-2.45.1+20.04.2/client/asserts.go 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/client/asserts.go 2021-02-02 08:21:12.000000000 +0000 @@ -84,14 +84,12 @@ q.Set("remote", "true") } - ctx, cancel := context.WithTimeout(context.Background(), doTimeout) - defer cancel() - response, err := client.raw(ctx, "GET", path, q, nil, nil) + response, cancel, err := client.rawWithTimeout(context.Background(), "GET", path, q, nil, nil, nil) if err != nil { fmt := "failed to query assertions: %w" return nil, xerrors.Errorf(fmt, err) } - + defer cancel() defer response.Body.Close() if response.StatusCode != 200 { return nil, parseError(response) diff -Nru snapd-2.45.1+20.04.2/client/client.go snapd-2.48.3+20.04/client/client.go --- snapd-2.45.1+20.04.2/client/client.go 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/client/client.go 2021-02-02 08:21:12.000000000 +0000 @@ -31,6 +31,7 @@ "net/url" "os" "path" + "strconv" "time" "github.com/snapcore/snapd/dirs" @@ -204,7 +205,7 @@ const AllowInteractionHeader = "X-Allow-Interaction" // raw performs a request and returns the resulting http.Response and -// error you usually only need to call this directly if you expect the +// error. You usually only need to call this directly if you expect the // response to not be JSON, otherwise you'd call Do(...) instead. func (client *Client) raw(ctx context.Context, method, urlpath string, query url.Values, headers map[string]string, body io.Reader) (*http.Response, error) { // fake a url to keep http.Client happy @@ -222,6 +223,16 @@ for key, value := range headers { req.Header.Set(key, value) } + // Content-length headers are special and need to be set + // directly to the request. Just setting it to the header + // will be ignored by go http. + if clStr := req.Header.Get("Content-Length"); clStr != "" { + cl, err := strconv.ParseInt(clStr, 10, 64) + if err != nil { + return nil, err + } + req.ContentLength = cl + } if !client.disableAuth { // set Authorization header if there are user's credentials @@ -247,16 +258,19 @@ return rsp, nil } -// rawWithTimeout is like raw(), but sets a timeout for the whole of request and -// response (including rsp.Body() read) round trip. The caller is responsible -// for canceling the internal context to release the resources associated with -// the request by calling the returned cancel function. -func (client *Client) rawWithTimeout(ctx context.Context, method, urlpath string, query url.Values, headers map[string]string, body io.Reader, timeout time.Duration) (*http.Response, context.CancelFunc, error) { - if timeout == 0 { - return nil, nil, fmt.Errorf("internal error: timeout not set for rawWithTimeout") +// rawWithTimeout is like raw(), but sets a timeout based on opts for +// the whole of request and response (including rsp.Body() read) round +// trip. If opts is nil the default doTimeout is used. +// The caller is responsible for canceling the internal context +// to release the resources associated with the request by calling the +// returned cancel function. +func (client *Client) rawWithTimeout(ctx context.Context, method, urlpath string, query url.Values, headers map[string]string, body io.Reader, opts *doOptions) (*http.Response, context.CancelFunc, error) { + opts = ensureDoOpts(opts) + if opts.Timeout <= 0 { + return nil, nil, fmt.Errorf("internal error: timeout not set in options for rawWithTimeout") } - ctx, cancel := context.WithTimeout(ctx, timeout) + ctx, cancel := context.WithTimeout(ctx, opts.Timeout) rsp, err := client.raw(ctx, method, urlpath, query, headers, body) if err != nil && ctx.Err() != nil { cancel() @@ -272,7 +286,7 @@ // timeout for the whole of a single request to complete, requests are // retried for up to 38s in total, make sure that the client timeout is // not shorter than that - doTimeout = 50 * time.Second + doTimeout = 120 * time.Second ) // MockDoTimings mocks the delay used by the do retry loop and request timeout. @@ -300,43 +314,73 @@ client.doer = hijacked{f} } -type doFlags struct { - NoTimeout bool +type doOptions struct { + // Timeout is the overall request timeout + Timeout time.Duration + // Retry interval. + // Note for a request with a Timeout but without a retry, Retry should just + // be set to something larger than the Timeout. + Retry time.Duration +} + +func ensureDoOpts(opts *doOptions) *doOptions { + if opts == nil { + // defaults + opts = &doOptions{ + Timeout: doTimeout, + Retry: doRetry, + } + } + return opts +} + +// doNoTimeoutAndRetry can be passed to the do family to not have timeout +// nor retries. +var doNoTimeoutAndRetry = &doOptions{ + Timeout: time.Duration(-1), } // do performs a request and decodes the resulting json into the given // value. It's low-level, for testing/experimenting only; you should // usually use a higher level interface that builds on this. -func (client *Client) do(method, path string, query url.Values, headers map[string]string, body io.Reader, v interface{}, flags doFlags) (statusCode int, err error) { - retry := time.NewTicker(doRetry) - defer retry.Stop() - timeout := time.NewTimer(doTimeout) - defer timeout.Stop() +func (client *Client) do(method, path string, query url.Values, headers map[string]string, body io.Reader, v interface{}, opts *doOptions) (statusCode int, err error) { + opts = ensureDoOpts(opts) + + client.checkMaintenanceJSON() var rsp *http.Response var ctx context.Context = context.Background() - for { - if flags.NoTimeout { - rsp, err = client.raw(ctx, method, path, query, headers, body) - } else { + if opts.Timeout <= 0 { + // no timeout and retries + rsp, err = client.raw(ctx, method, path, query, headers, body) + } else { + if opts.Retry <= 0 { + return 0, fmt.Errorf("internal error: retry setting %s invalid", opts.Retry) + } + retry := time.NewTicker(opts.Retry) + defer retry.Stop() + timeout := time.NewTimer(opts.Timeout) + defer timeout.Stop() + + for { var cancel context.CancelFunc // use the same timeout as for the whole of the retry // loop to error out the whole do() call when a single // request exceeds the deadline - rsp, cancel, err = client.rawWithTimeout(ctx, method, path, query, headers, body, doTimeout) + rsp, cancel, err = client.rawWithTimeout(ctx, method, path, query, headers, body, opts) if err == nil { defer cancel() } - } - if err == nil || method != "GET" { + if err == nil || method != "GET" { + break + } + select { + case <-retry.C: + continue + case <-timeout.C: + } break } - select { - case <-retry.C: - continue - case <-timeout.C: - } - break } if err != nil { return 0, err @@ -370,8 +414,60 @@ // response payload into the given value using the "UseNumber" json decoding // which produces json.Numbers instead of float64 types for numbers. func (client *Client) doSync(method, path string, query url.Values, headers map[string]string, body io.Reader, v interface{}) (*ResultInfo, error) { + return client.doSyncWithOpts(method, path, query, headers, body, v, nil) +} + +// checkMaintenanceJSON checks if there is a maintenance.json file written by +// snapd the daemon that positively identifies snapd as being unavailable due to +// maintenance, either for snapd restarting itself to update, or rebooting the +// system to update the kernel or base snap, etc. If there is ongoing +// maintenance, then the maintenance object on the client is set appropriately. +// note that currently checkMaintenanceJSON does not return errors, such that +// if the file is missing or corrupt or empty, nothing will happen and it will +// be silently ignored +func (client *Client) checkMaintenanceJSON() { + f, err := os.Open(dirs.SnapdMaintenanceFile) + // just continue if we can't read the maintenance file + if err != nil { + return + } + defer f.Close() + + // we have a maintenance file, try to read it + maintenance := &Error{} + + if err := json.NewDecoder(f).Decode(&maintenance); err != nil { + // if the json is malformed, just ignore it for now, we only use it for + // positive identification of snapd down for maintenance + return + } + + if maintenance != nil { + switch maintenance.Kind { + case ErrorKindDaemonRestart: + client.maintenance = maintenance + case ErrorKindSystemRestart: + client.maintenance = maintenance + } + // don't set maintenance for other kinds, as we don't know what it + // is yet + + // this also means an empty json object in maintenance.json doesn't get + // treated as a real maintenance downtime for example + } +} + +func (client *Client) doSyncWithOpts(method, path string, query url.Values, headers map[string]string, body io.Reader, v interface{}, opts *doOptions) (*ResultInfo, error) { + // first check maintenance.json to see if snapd is down for a restart, and + // set cli.maintenance as appropriate, then perform the request + // TODO: it would be a nice thing to skip the request if we know that snapd + // won't respond and return a specific error, but that's a big behavior + // change we probably shouldn't make right now, not to mention it probably + // requires adjustments in other areas too + client.checkMaintenanceJSON() + var rsp response - statusCode, err := client.do(method, path, query, headers, body, &rsp, doFlags{}) + statusCode, err := client.do(method, path, query, headers, body, &rsp, opts) if err != nil { return nil, err } @@ -395,18 +491,13 @@ } func (client *Client) doAsync(method, path string, query url.Values, headers map[string]string, body io.Reader) (changeID string, err error) { - _, changeID, err = client.doAsyncFull(method, path, query, headers, body, doFlags{}) + _, changeID, err = client.doAsyncFull(method, path, query, headers, body, nil) return } -func (client *Client) doAsyncNoTimeout(method, path string, query url.Values, headers map[string]string, body io.Reader) (changeID string, err error) { - _, changeID, err = client.doAsyncFull(method, path, query, headers, body, doFlags{NoTimeout: true}) - return changeID, err -} - -func (client *Client) doAsyncFull(method, path string, query url.Values, headers map[string]string, body io.Reader, flags doFlags) (result json.RawMessage, changeID string, err error) { +func (client *Client) doAsyncFull(method, path string, query url.Values, headers map[string]string, body io.Reader, opts *doOptions) (result json.RawMessage, changeID string, err error) { var rsp response - statusCode, err := client.do(method, path, query, headers, body, &rsp, flags) + statusCode, err := client.do(method, path, query, headers, body, &rsp, opts) if err != nil { return nil, "", err } @@ -474,7 +565,7 @@ // Error is the real value of response.Result when an error occurs. type Error struct { - Kind string `json:"kind"` + Kind ErrorKind `json:"kind"` Value interface{} `json:"value"` Message string `json:"message"` @@ -485,57 +576,12 @@ return e.Message } -const ( - ErrorKindTwoFactorRequired = "two-factor-required" - ErrorKindTwoFactorFailed = "two-factor-failed" - ErrorKindLoginRequired = "login-required" - ErrorKindInvalidAuthData = "invalid-auth-data" - ErrorKindTermsNotAccepted = "terms-not-accepted" - ErrorKindNoPaymentMethods = "no-payment-methods" - ErrorKindPaymentDeclined = "payment-declined" - ErrorKindPasswordPolicy = "password-policy" - - ErrorKindSnapAlreadyInstalled = "snap-already-installed" - ErrorKindSnapNotInstalled = "snap-not-installed" - ErrorKindSnapNotFound = "snap-not-found" - ErrorKindAppNotFound = "app-not-found" - ErrorKindSnapLocal = "snap-local" - ErrorKindSnapNeedsDevMode = "snap-needs-devmode" - ErrorKindSnapNeedsClassic = "snap-needs-classic" - ErrorKindSnapNeedsClassicSystem = "snap-needs-classic-system" - ErrorKindSnapNotClassic = "snap-not-classic" - ErrorKindNoUpdateAvailable = "snap-no-update-available" - - ErrorKindRevisionNotAvailable = "snap-revision-not-available" - ErrorKindChannelNotAvailable = "snap-channel-not-available" - ErrorKindArchitectureNotAvailable = "snap-architecture-not-available" - - ErrorKindChangeConflict = "snap-change-conflict" - - ErrorKindNotSnap = "snap-not-a-snap" - - ErrorKindNetworkTimeout = "network-timeout" - ErrorKindDNSFailure = "dns-failure" - - ErrorKindInterfacesUnchanged = "interfaces-unchanged" - - ErrorKindBadQuery = "bad-query" - ErrorKindConfigNoSuchOption = "option-not-found" - - ErrorKindSystemRestart = "system-restart" - ErrorKindDaemonRestart = "daemon-restart" - - ErrorKindAssertionNotFound = "assertion-not-found" - - ErrorKindUnsuccessful = "unsuccessful" -) - // IsRetryable returns true if the given error is an error // that can be retried later. func IsRetryable(err error) bool { switch e := err.(type) { case *Error: - return e.Kind == ErrorKindChangeConflict + return e.Kind == ErrorKindSnapChangeConflict } return false } @@ -687,3 +733,13 @@ _, err := client.doSync("GET", "/v2/debug", urlParams, nil, nil, &result) return err } + +type SystemRecoveryKeysResponse struct { + RecoveryKey string `json:"recovery-key"` + ReinstallKey string `json:"reinstall-key"` +} + +func (client *Client) SystemRecoveryKeys(result interface{}) error { + _, err := client.doSync("GET", "/v2/system-recovery-keys", nil, nil, nil, &result) + return err +} diff -Nru snapd-2.45.1+20.04.2/client/client_test.go snapd-2.48.3+20.04/client/client_test.go --- snapd-2.45.1+20.04.2/client/client_test.go 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/client/client_test.go 2021-02-02 08:21:12.000000000 +0000 @@ -20,8 +20,10 @@ package client_test import ( + "encoding/json" "errors" "fmt" + "io" "io/ioutil" "net" "net/http" @@ -37,12 +39,15 @@ "github.com/snapcore/snapd/client" "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/testutil" ) // Hook up check.v1 into the "go test" runner func Test(t *testing.T) { TestingT(t) } type clientSuite struct { + testutil.BaseTest + cli *client.Client req *http.Request reqs []*http.Request @@ -53,13 +58,16 @@ header http.Header status int contentLength int64 - restore func() + + countingCloser *countingCloser } var _ = Suite(&clientSuite{}) func (cs *clientSuite) SetUpTest(c *C) { os.Setenv(client.TestAuthFileEnvKey, filepath.Join(c.MkDir(), "auth.json")) + cs.AddCleanup(func() { os.Unsetenv(client.TestAuthFileEnvKey) }) + cs.cli = client.New(nil) cs.cli.SetDoer(cs) cs.err = nil @@ -72,15 +80,22 @@ cs.status = 200 cs.doCalls = 0 cs.contentLength = 0 + cs.countingCloser = nil dirs.SetRootDir(c.MkDir()) + cs.AddCleanup(func() { dirs.SetRootDir("") }) - cs.restore = client.MockDoTimings(time.Millisecond, 100*time.Millisecond) + cs.AddCleanup(client.MockDoTimings(time.Millisecond, 100*time.Millisecond)) } -func (cs *clientSuite) TearDownTest(c *C) { - os.Unsetenv(client.TestAuthFileEnvKey) - cs.restore() +type countingCloser struct { + io.Reader + closeCalled int +} + +func (n *countingCloser) Close() error { + n.closeCalled++ + return nil } func (cs *clientSuite) Do(req *http.Request) (*http.Response, error) { @@ -90,8 +105,9 @@ if cs.doCalls < len(cs.rsps) { body = cs.rsps[cs.doCalls] } + cs.countingCloser = &countingCloser{Reader: strings.NewReader(body)} rsp := &http.Response{ - Body: ioutil.NopCloser(strings.NewReader(body)), + Body: cs.countingCloser, Header: cs.header, StatusCode: cs.status, ContentLength: cs.contentLength, @@ -108,7 +124,7 @@ func (cs *clientSuite) TestClientDoReportsErrors(c *C) { cs.err = errors.New("ouchie") - _, err := cs.cli.Do("GET", "/", nil, nil, nil, client.DoFlags{}) + _, err := cs.cli.Do("GET", "/", nil, nil, nil, nil) c.Check(err, ErrorMatches, "cannot communicate with server: ouchie") if cs.doCalls < 2 { c.Fatalf("do did not retry") @@ -119,7 +135,7 @@ var v []int cs.rsp = `[1,2]` reqBody := ioutil.NopCloser(strings.NewReader("")) - statusCode, err := cs.cli.Do("GET", "/this", nil, reqBody, &v, client.DoFlags{}) + statusCode, err := cs.cli.Do("GET", "/this", nil, reqBody, &v, nil) c.Check(err, IsNil) c.Check(statusCode, Equals, 200) c.Check(v, DeepEquals, []int{1, 2}) @@ -130,12 +146,91 @@ c.Check(cs.req.URL.Path, Equals, "/this") } +func makeMaintenanceFile(c *C, b []byte) { + c.Assert(os.MkdirAll(filepath.Dir(dirs.SnapdMaintenanceFile), 0755), IsNil) + c.Assert(ioutil.WriteFile(dirs.SnapdMaintenanceFile, b, 0644), IsNil) +} + +func (cs *clientSuite) TestClientSetMaintenanceForMaintenanceJSON(c *C) { + // write a maintenance.json that says snapd is down for a restart + maintErr := &client.Error{ + Kind: client.ErrorKindSystemRestart, + Message: "system is restarting", + } + b, err := json.Marshal(maintErr) + c.Assert(err, IsNil) + makeMaintenanceFile(c, b) + + // now after a Do(), we will have maintenance set to what we wrote + // originally + _, err = cs.cli.Do("GET", "/this", nil, nil, nil, nil) + c.Check(err, IsNil) + + returnedErr := cs.cli.Maintenance() + c.Assert(returnedErr, DeepEquals, maintErr) +} + +func (cs *clientSuite) TestClientIgnoresGarbageMaintenanceJSON(c *C) { + // write a garbage maintenance.json that can't be unmarshalled + makeMaintenanceFile(c, []byte("blah blah blah not json")) + + // after a Do(), no maintenance set and also no error returned from Do() + _, err := cs.cli.Do("GET", "/this", nil, nil, nil, nil) + c.Check(err, IsNil) + + returnedErr := cs.cli.Maintenance() + c.Assert(returnedErr, IsNil) +} + +func (cs *clientSuite) TestClientDoNoTimeoutIgnoresRetry(c *C) { + var v []int + cs.rsp = `[1,2]` + cs.err = fmt.Errorf("borken") + reqBody := ioutil.NopCloser(strings.NewReader("")) + doOpts := &client.DoOptions{ + // Timeout is unset, thus 0, and thus we ignore the retry and only run + // once even though there is an error + Retry: time.Duration(time.Second), + } + _, err := cs.cli.Do("GET", "/this", nil, reqBody, &v, doOpts) + c.Check(err, ErrorMatches, "cannot communicate with server: borken") + c.Assert(cs.doCalls, Equals, 1) +} + +func (cs *clientSuite) TestClientDoRetryValidation(c *C) { + var v []int + cs.rsp = `[1,2]` + reqBody := ioutil.NopCloser(strings.NewReader("")) + doOpts := &client.DoOptions{ + Retry: time.Duration(-1), + Timeout: time.Duration(time.Minute), + } + _, err := cs.cli.Do("GET", "/this", nil, reqBody, &v, doOpts) + c.Check(err, ErrorMatches, "internal error: retry setting.*invalid") + c.Assert(cs.req, IsNil) +} + +func (cs *clientSuite) TestClientDoRetryWorks(c *C) { + reqBody := ioutil.NopCloser(strings.NewReader("")) + cs.err = fmt.Errorf("borken") + doOpts := &client.DoOptions{ + Retry: time.Duration(time.Millisecond), + Timeout: time.Duration(time.Second), + } + _, err := cs.cli.Do("GET", "/this", nil, reqBody, nil, doOpts) + c.Check(err, ErrorMatches, "cannot communicate with server: borken") + // best effort checking given that execution could be slow + // on some machines + c.Assert(cs.doCalls > 500, Equals, true) + c.Assert(cs.doCalls < 1100, Equals, true) +} + func (cs *clientSuite) TestClientUnderstandsStatusCode(c *C) { var v []int cs.status = 202 cs.rsp = `[1,2]` reqBody := ioutil.NopCloser(strings.NewReader("")) - statusCode, err := cs.cli.Do("GET", "/this", nil, reqBody, &v, client.DoFlags{}) + statusCode, err := cs.cli.Do("GET", "/this", nil, reqBody, &v, nil) c.Check(err, IsNil) c.Check(statusCode, Equals, 202) c.Check(v, DeepEquals, []int{1, 2}) @@ -151,7 +246,7 @@ defer os.Unsetenv(client.TestAuthFileEnvKey) var v string - _, _ = cs.cli.Do("GET", "/this", nil, nil, &v, client.DoFlags{}) + _, _ = cs.cli.Do("GET", "/this", nil, nil, &v, nil) c.Assert(cs.req, NotNil) authorization := cs.req.Header.Get("Authorization") c.Check(authorization, Equals, "") @@ -169,7 +264,7 @@ c.Assert(err, IsNil) var v string - _, _ = cs.cli.Do("GET", "/this", nil, nil, &v, client.DoFlags{}) + _, _ = cs.cli.Do("GET", "/this", nil, nil, &v, nil) authorization := cs.req.Header.Get("Authorization") c.Check(authorization, Equals, `Macaroon root="macaroon", discharge="discharge"`) } @@ -188,7 +283,7 @@ var v string cli := client.New(&client.Config{DisableAuth: true}) cli.SetDoer(cs) - _, _ = cli.Do("GET", "/this", nil, nil, &v, client.DoFlags{}) + _, _ = cli.Do("GET", "/this", nil, nil, &v, nil) authorization := cs.req.Header.Get("Authorization") c.Check(authorization, Equals, "") } @@ -197,13 +292,13 @@ var v string cli := client.New(&client.Config{Interactive: false}) cli.SetDoer(cs) - _, _ = cli.Do("GET", "/this", nil, nil, &v, client.DoFlags{}) + _, _ = cli.Do("GET", "/this", nil, nil, &v, nil) interactive := cs.req.Header.Get(client.AllowInteractionHeader) c.Check(interactive, Equals, "") cli = client.New(&client.Config{Interactive: true}) cli.SetDoer(cs) - _, _ = cli.Do("GET", "/this", nil, nil, &v, client.DoFlags{}) + _, _ = cli.Do("GET", "/this", nil, nil, &v, nil) interactive = cs.req.Header.Get(client.AllowInteractionHeader) c.Check(interactive, Equals, "true") } @@ -340,7 +435,7 @@ Args: []string{"bar", "--baz"}, } - stdout, stderr, err := cli.RunSnapctl(options) + stdout, stderr, err := cli.RunSnapctl(options, nil) c.Check(err, IsNil) c.Check(string(stdout), Equals, "test stdout") c.Check(string(stderr), Equals, "test stderr") @@ -461,7 +556,7 @@ c.Check(client.IsRetryable(errors.New("some-error")), Equals, false) c.Check(client.IsRetryable(&client.Error{Kind: "something-else"}), Equals, false) // happy - c.Check(client.IsRetryable(&client.Error{Kind: client.ErrorKindChangeConflict}), Equals, true) + c.Check(client.IsRetryable(&client.Error{Kind: client.ErrorKindSnapChangeConflict}), Equals, true) } func (cs *clientSuite) TestUserAgent(c *C) { @@ -469,7 +564,7 @@ cli.SetDoer(cs) var v string - _, _ = cli.Do("GET", "/", nil, nil, &v, client.DoFlags{}) + _, _ = cli.Do("GET", "/", nil, nil, &v, nil) c.Assert(cs.req, NotNil) c.Check(cs.req.Header.Get("User-Agent"), Equals, "some-agent/9.87") } @@ -528,9 +623,21 @@ defer func() { testServer.Close() }() cli := client.New(&client.Config{BaseURL: testServer.URL}) - _, err := cli.Do("GET", "/", nil, nil, nil, client.DoFlags{}) + _, err := cli.Do("GET", "/", nil, nil, nil, nil) c.Assert(err, ErrorMatches, `.* timeout exceeded while waiting for response`) - _, err = cli.Do("POST", "/", nil, nil, nil, client.DoFlags{}) + _, err = cli.Do("POST", "/", nil, nil, nil, nil) c.Assert(err, ErrorMatches, `.* timeout exceeded while waiting for response`) } + +func (cs *clientSuite) TestClientSystemRecoveryKeys(c *C) { + cs.rsp = `{"type":"sync", "result":{"recovery-key":"42"}}` + + var key client.SystemRecoveryKeysResponse + err := cs.cli.SystemRecoveryKeys(&key) + c.Assert(err, IsNil) + c.Check(cs.reqs, HasLen, 1) + c.Check(cs.reqs[0].Method, Equals, "GET") + c.Check(cs.reqs[0].URL.Path, Equals, "/v2/system-recovery-keys") + c.Check(key.RecoveryKey, Equals, "42") +} diff -Nru snapd-2.45.1+20.04.2/client/clientutil/snapinfo.go snapd-2.48.3+20.04/client/clientutil/snapinfo.go --- snapd-2.45.1+20.04.2/client/clientutil/snapinfo.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48.3+20.04/client/clientutil/snapinfo.go 2021-02-02 08:21:12.000000000 +0000 @@ -0,0 +1,149 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2018-2020 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 offers utilities to turn snap.Info and related +// structs into client structs and to work with the latter. +package clientutil + +import ( + "sort" + "strings" + + "github.com/snapcore/snapd/client" + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/snap" +) + +// A StatusDecorator is able to decorate client.AppInfos with service status. +type StatusDecorator interface { + DecorateWithStatus(appInfo *client.AppInfo, snapApp *snap.AppInfo) error +} + +// ClientSnapFromSnapInfo returns a client.Snap derived from snap.Info. +// If an optional StatusDecorator is provided it will be used to +// add service status information. +func ClientSnapFromSnapInfo(snapInfo *snap.Info, decorator StatusDecorator) (*client.Snap, error) { + var publisher *snap.StoreAccount + if snapInfo.Publisher.Username != "" { + publisher = &snapInfo.Publisher + } + + confinement := snapInfo.Confinement + if confinement == "" { + confinement = snap.StrictConfinement + } + + snapapps := make([]*snap.AppInfo, 0, len(snapInfo.Apps)) + for _, app := range snapInfo.Apps { + snapapps = append(snapapps, app) + } + sort.Sort(snap.AppInfoBySnapApp(snapapps)) + + apps, err := ClientAppInfosFromSnapAppInfos(snapapps, decorator) + result := &client.Snap{ + Description: snapInfo.Description(), + Developer: snapInfo.Publisher.Username, + Publisher: publisher, + Icon: snapInfo.Media.IconURL(), + ID: snapInfo.ID(), + InstallDate: snapInfo.InstallDate(), + Name: snapInfo.InstanceName(), + Revision: snapInfo.Revision, + Summary: snapInfo.Summary(), + Type: string(snapInfo.Type()), + Base: snapInfo.Base, + Version: snapInfo.Version, + Channel: snapInfo.Channel, + Private: snapInfo.Private, + Confinement: string(confinement), + Apps: apps, + Broken: snapInfo.Broken, + Contact: snapInfo.Contact, + Title: snapInfo.Title(), + License: snapInfo.License, + Media: snapInfo.Media, + Prices: snapInfo.Prices, + Channels: snapInfo.Channels, + Tracks: snapInfo.Tracks, + CommonIDs: snapInfo.CommonIDs, + Website: snapInfo.Website, + StoreURL: snapInfo.StoreURL, + } + + return result, err +} + +func ClientAppInfoNotes(app *client.AppInfo) string { + if !app.IsService() { + return "-" + } + + var notes = make([]string, 0, 2) + var seenTimer, seenSocket bool + for _, act := range app.Activators { + switch act.Type { + case "timer": + seenTimer = true + case "socket": + seenSocket = true + } + } + if seenTimer { + notes = append(notes, "timer-activated") + } + if seenSocket { + notes = append(notes, "socket-activated") + } + if len(notes) == 0 { + return "-" + } + return strings.Join(notes, ",") +} + +// ClientAppInfosFromSnapAppInfos returns client.AppInfos derived from +// the given snap.AppInfos. +// If an optional StatusDecorator is provided it will be used to add +// service status information as well, this will be done only if the +// snap is active and when the app is a service. +func ClientAppInfosFromSnapAppInfos(apps []*snap.AppInfo, decorator StatusDecorator) ([]client.AppInfo, error) { + out := make([]client.AppInfo, 0, len(apps)) + for _, app := range apps { + appInfo := client.AppInfo{ + Snap: app.Snap.InstanceName(), + Name: app.Name, + CommonID: app.CommonID, + } + if fn := app.DesktopFile(); osutil.FileExists(fn) { + appInfo.DesktopFile = fn + } + + appInfo.Daemon = app.Daemon + if !app.IsService() || decorator == nil || !app.Snap.IsActive() { + out = append(out, appInfo) + continue + } + + if err := decorator.DecorateWithStatus(&appInfo, app); err != nil { + return nil, err + } + out = append(out, appInfo) + } + + return out, nil +} diff -Nru snapd-2.45.1+20.04.2/client/clientutil/snapinfo_test.go snapd-2.48.3+20.04/client/clientutil/snapinfo_test.go --- snapd-2.45.1+20.04.2/client/clientutil/snapinfo_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48.3+20.04/client/clientutil/snapinfo_test.go 2021-02-02 08:21:12.000000000 +0000 @@ -0,0 +1,286 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2018-2020 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 ( + "io/ioutil" + "os" + "path/filepath" + "reflect" + "testing" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/client" + "github.com/snapcore/snapd/client/clientutil" + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/testutil" +) + +func Test(t *testing.T) { TestingT(t) } + +type cmdSuite struct { + testutil.BaseTest +} + +func (s *cmdSuite) SetUpTest(c *C) { + s.BaseTest.SetUpTest(c) + dirs.SetRootDir(c.MkDir()) + s.AddCleanup(func() { dirs.SetRootDir("/") }) +} + +var _ = Suite(&cmdSuite{}) + +func (*cmdSuite) TestClientSnapFromSnapInfo(c *C) { + si := &snap.Info{ + SnapType: snap.TypeApp, + SuggestedName: "", + InstanceKey: "insta", + Version: "v1", + Confinement: snap.StrictConfinement, + License: "Proprietary", + Publisher: snap.StoreAccount{ + ID: "ZvtzsxbsHivZLdvzrt0iqW529riGLfXJ", + Username: "thingyinc", + DisplayName: "Thingy Inc.", + Validation: "unproven", + }, + Base: "core18", + SideInfo: snap.SideInfo{ + RealName: "the-snap", + SnapID: "snapidid", + Revision: snap.R(99), + EditedTitle: "the-title", + EditedSummary: "the-summary", + EditedDescription: "the-description", + Channel: "latest/stable", + Contact: "https://thingy.com", + Private: true, + }, + Channels: map[string]*snap.ChannelSnapInfo{}, + Tracks: []string{}, + Prices: map[string]float64{}, + Media: []snap.MediaInfo{ + {Type: "icon", URL: "https://dashboard.snapcraft.io/site_media/appmedia/2017/12/Thingy.png"}, + {Type: "screenshot", URL: "https://dashboard.snapcraft.io/site_media/appmedia/2018/01/Thingy_01.png"}, + {Type: "screenshot", URL: "https://dashboard.snapcraft.io/site_media/appmedia/2018/01/Thingy_02.png", Width: 600, Height: 200}, + }, + CommonIDs: []string{"org.thingy"}, + Website: "http://example.com/thingy", + StoreURL: "https://snapcraft.io/thingy", + Broken: "broken", + } + // valid InstallDate + err := os.MkdirAll(si.MountDir(), 0755) + c.Assert(err, IsNil) + err = os.Symlink(si.Revision.String(), filepath.Join(filepath.Dir(si.MountDir()), "current")) + c.Assert(err, IsNil) + + ci, err := clientutil.ClientSnapFromSnapInfo(si, nil) + c.Check(err, IsNil) + + // check that fields are filled + // see daemon/snap.go for fields filled after this + expectedZeroFields := []string{ + "Screenshots", // unused nowadays + "DownloadSize", + "InstalledSize", + "Health", + "Status", + "TrackingChannel", + "IgnoreValidation", + "CohortKey", + "DevMode", + "TryMode", + "JailMode", + "MountedFrom", + } + var checker func(string, reflect.Value) + checker = func(pfx string, x reflect.Value) { + t := x.Type() + for i := 0; i < x.NumField(); i++ { + f := t.Field(i) + if f.PkgPath != "" { + // not exported, ignore + continue + } + v := x.Field(i) + if f.Anonymous { + checker(pfx+f.Name+".", v) + continue + } + if reflect.DeepEqual(v.Interface(), reflect.Zero(f.Type).Interface()) { + name := pfx + f.Name + c.Check(expectedZeroFields, testutil.Contains, name, Commentf("%s not set", name)) + } + } + } + x := reflect.ValueOf(ci).Elem() + checker("", x) + + // check some values + c.Check(ci.Name, Equals, "the-snap_insta") + c.Check(ci.Type, Equals, "app") + c.Check(ci.ID, Equals, si.ID()) + c.Check(ci.Revision, Equals, snap.R(99)) + c.Check(ci.Version, Equals, "v1") + c.Check(ci.Title, Equals, "the-title") + c.Check(ci.Summary, Equals, "the-summary") + c.Check(ci.Description, Equals, "the-description") + c.Check(ci.Icon, Equals, si.Media.IconURL()) + c.Check(ci.Website, Equals, si.Website) + c.Check(ci.StoreURL, Equals, si.StoreURL) + c.Check(ci.Developer, Equals, "thingyinc") + c.Check(ci.Publisher, DeepEquals, &si.Publisher) +} + +type testStatusDecorator struct { + calls int +} + +func (sd *testStatusDecorator) DecorateWithStatus(appInfo *client.AppInfo, app *snap.AppInfo) error { + sd.calls++ + if appInfo.Snap != app.Snap.InstanceName() || appInfo.Name != app.Name { + panic("mismatched") + } + appInfo.Enabled = true + appInfo.Active = true + return nil +} + +func (*cmdSuite) TestClientSnapFromSnapInfoAppsInactive(c *C) { + si := &snap.Info{ + SnapType: snap.TypeApp, + SuggestedName: "", + InstanceKey: "insta", + SideInfo: snap.SideInfo{ + RealName: "the-snap", + SnapID: "snapidid", + Revision: snap.R(99), + }, + } + si.Apps = map[string]*snap.AppInfo{ + "svc": {Snap: si, Name: "svc", Daemon: "simple"}, + "app": {Snap: si, Name: "app", CommonID: "common.id"}, + } + // sanity + c.Check(si.IsActive(), Equals, false) + // desktop file + df := si.Apps["app"].DesktopFile() + err := os.MkdirAll(filepath.Dir(df), 0755) + c.Assert(err, IsNil) + err = ioutil.WriteFile(df, nil, 0644) + c.Assert(err, IsNil) + + sd := &testStatusDecorator{} + ci, err := clientutil.ClientSnapFromSnapInfo(si, sd) + c.Check(err, IsNil) + + c.Check(ci.Name, Equals, "the-snap_insta") + c.Check(ci.Apps, DeepEquals, []client.AppInfo{ + { + Snap: "the-snap_insta", + Name: "app", + CommonID: "common.id", + DesktopFile: df, + }, + {Snap: "the-snap_insta", Name: "svc", Daemon: "simple"}, + }) + // not called on inactive snaps + c.Check(sd.calls, Equals, 0) +} + +func (*cmdSuite) TestClientSnapFromSnapInfoAppsActive(c *C) { + si := &snap.Info{ + SnapType: snap.TypeApp, + SuggestedName: "", + InstanceKey: "insta", + SideInfo: snap.SideInfo{ + RealName: "the-snap", + SnapID: "snapidid", + Revision: snap.R(99), + }, + } + si.Apps = map[string]*snap.AppInfo{ + "svc": {Snap: si, Name: "svc", Daemon: "simple"}, + } + // make it active + err := os.MkdirAll(si.MountDir(), 0755) + c.Assert(err, IsNil) + err = os.Symlink(si.Revision.String(), filepath.Join(filepath.Dir(si.MountDir()), "current")) + c.Assert(err, IsNil) + c.Check(si.IsActive(), Equals, true) + + sd := &testStatusDecorator{} + ci, err := clientutil.ClientSnapFromSnapInfo(si, sd) + c.Check(err, IsNil) + // ... service status + c.Check(ci.Name, Equals, "the-snap_insta") + c.Check(ci.Apps, DeepEquals, []client.AppInfo{ + {Snap: "the-snap_insta", Name: "svc", Daemon: "simple", Enabled: true, Active: true}, + }) + + c.Check(sd.calls, Equals, 1) +} + +func (*cmdSuite) TestAppStatusNotes(c *C) { + ai := client.AppInfo{} + c.Check(clientutil.ClientAppInfoNotes(&ai), Equals, "-") + + ai = client.AppInfo{ + Daemon: "oneshot", + } + c.Check(clientutil.ClientAppInfoNotes(&ai), Equals, "-") + + ai = client.AppInfo{ + Daemon: "oneshot", + Activators: []client.AppActivator{ + {Type: "timer"}, + }, + } + c.Check(clientutil.ClientAppInfoNotes(&ai), Equals, "timer-activated") + + ai = client.AppInfo{ + Daemon: "oneshot", + Activators: []client.AppActivator{ + {Type: "socket"}, + }, + } + c.Check(clientutil.ClientAppInfoNotes(&ai), Equals, "socket-activated") + + // check that the output is stable regardless of the order of activators + ai = client.AppInfo{ + Daemon: "oneshot", + Activators: []client.AppActivator{ + {Type: "timer"}, + {Type: "socket"}, + }, + } + c.Check(clientutil.ClientAppInfoNotes(&ai), Equals, "timer-activated,socket-activated") + ai = client.AppInfo{ + Daemon: "oneshot", + Activators: []client.AppActivator{ + {Type: "socket"}, + {Type: "timer"}, + }, + } + c.Check(clientutil.ClientAppInfoNotes(&ai), Equals, "timer-activated,socket-activated") +} diff -Nru snapd-2.45.1+20.04.2/client/console_conf.go snapd-2.48.3+20.04/client/console_conf.go --- snapd-2.45.1+20.04.2/client/console_conf.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48.3+20.04/client/console_conf.go 2021-02-02 08:21:12.000000000 +0000 @@ -0,0 +1,44 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2020 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 client + +import "time" + +// InternalConsoleConfStartResponse is the response from console-conf start +// support +type InternalConsoleConfStartResponse struct { + ActiveAutoRefreshChanges []string `json:"active-auto-refreshes,omitempty"` + ActiveAutoRefreshSnaps []string `json:"active-auto-refresh-snaps,omitempty"` +} + +// InternalConsoleConfStart invokes the dedicated console-conf start support +// to handle intervening auto-refreshes. +// Not for general use. +func (client *Client) InternalConsoleConfStart() ([]string, []string, error) { + resp := &InternalConsoleConfStartResponse{} + // do the post with a short timeout so that if snapd is not available due to + // maintenance we will return very quickly so the caller can handle that + opts := &doOptions{ + Timeout: 2 * time.Second, + Retry: 1 * time.Hour, + } + _, err := client.doSyncWithOpts("POST", "/v2/internal/console-conf-start", nil, nil, nil, resp, opts) + return resp.ActiveAutoRefreshChanges, resp.ActiveAutoRefreshSnaps, err +} diff -Nru snapd-2.45.1+20.04.2/client/console_conf_test.go snapd-2.48.3+20.04/client/console_conf_test.go --- snapd-2.45.1+20.04.2/client/console_conf_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48.3+20.04/client/console_conf_test.go 2021-02-02 08:21:12.000000000 +0000 @@ -0,0 +1,63 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2020 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 client_test + +import ( + . "gopkg.in/check.v1" +) + +func (cs *clientSuite) TestClientInternalConsoleConfEndpointEmpty(c *C) { + // no changes and no snaps + cs.status = 200 + cs.rsp = `{ + "type": "sync", + "status-code": 200, + "result": {} + }` + + chgs, snaps, err := cs.cli.InternalConsoleConfStart() + c.Assert(chgs, HasLen, 0) + c.Assert(snaps, HasLen, 0) + c.Assert(err, IsNil) + c.Check(cs.req.Method, Equals, "POST") + c.Check(cs.req.URL.Path, Equals, "/v2/internal/console-conf-start") + c.Check(cs.doCalls, Equals, 1) +} + +func (cs *clientSuite) TestClientInternalConsoleConfEndpoint(c *C) { + // some changes and snaps + cs.status = 200 + cs.rsp = `{ + "type": "sync", + "status-code": 200, + "result": { + "active-auto-refreshes": ["1"], + "active-auto-refresh-snaps": ["pc-kernel"] + } + }` + + chgs, snaps, err := cs.cli.InternalConsoleConfStart() + c.Assert(err, IsNil) + c.Assert(chgs, DeepEquals, []string{"1"}) + c.Assert(snaps, DeepEquals, []string{"pc-kernel"}) + c.Check(cs.req.Method, Equals, "POST") + c.Check(cs.req.URL.Path, Equals, "/v2/internal/console-conf-start") + c.Check(cs.doCalls, Equals, 1) +} diff -Nru snapd-2.45.1+20.04.2/client/errors.go snapd-2.48.3+20.04/client/errors.go --- snapd-2.45.1+20.04.2/client/errors.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48.3+20.04/client/errors.go 2021-02-02 08:21:12.000000000 +0000 @@ -0,0 +1,145 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2020 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 client + +// ErrorKind distinguishes kind of errors. +type ErrorKind string + +// error kind const value doc comments here have a non-default, +// specialized style (to help docs/error-kind.go): +// +// // ErrorKind...: DESCRIPTION . +// +// Note the mandatory dot at the end. +// `code-like` quoting should be used when meaningful. + +// Error kinds. Keep https://forum.snapcraft.io/t/using-the-rest-api/18603#heading--errors in sync using doc/error-kinds.go. +const ( + // ErrorKindTwoFactorRequired: the client needs to retry the + // `login` command including an OTP. + ErrorKindTwoFactorRequired ErrorKind = "two-factor-required" + // ErrorKindTwoFactorFailed: the OTP provided wasn't recognised. + ErrorKindTwoFactorFailed ErrorKind = "two-factor-failed" + // ErrorKindLoginRequired: the requested operation cannot be + // performed without an authenticated user. This is the kind + // of any other 401 Unauthorized response. + ErrorKindLoginRequired ErrorKind = "login-required" + // ErrorKindInvalidAuthData: the authentication data provided + // failed to validate (e.g. a malformed email address). The + // `value` of the error is an object with a key per failed field + // and a list of the failures on each field. + ErrorKindInvalidAuthData ErrorKind = "invalid-auth-data" + // ErrorKindPasswordPolicy: provided password doesn't meet + // system policy. + ErrorKindPasswordPolicy ErrorKind = "password-policy" + // ErrorKindAuthCancelled: authentication was cancelled by the user. + ErrorKindAuthCancelled ErrorKind = "auth-cancelled" + + // ErrorKindTermsNotAccepted: deprecated, do not document. + ErrorKindTermsNotAccepted ErrorKind = "terms-not-accepted" + // ErrorKindNoPaymentMethods: deprecated, do not document. + ErrorKindNoPaymentMethods ErrorKind = "no-payment-methods" + // ErrorKindPaymentDeclined: deprecated, do not document. + ErrorKindPaymentDeclined ErrorKind = "payment-declined" + + // ErrorKindSnapAlreadyInstalled: the requested snap is + // already installed. + ErrorKindSnapAlreadyInstalled ErrorKind = "snap-already-installed" + // ErrorKindSnapNotInstalled: the requested snap is not installed. + ErrorKindSnapNotInstalled ErrorKind = "snap-not-installed" + // ErrorKindSnapNotFound: the requested snap couldn't be found. + ErrorKindSnapNotFound ErrorKind = "snap-not-found" + // ErrorKindAppNotFound: the requested app couldn't be found. + ErrorKindAppNotFound ErrorKind = "app-not-found" + // ErrorKindSnapLocal: cannot perform operation on local snap. + ErrorKindSnapLocal ErrorKind = "snap-local" + // ErrorKindSnapNeedsDevMode: the requested snap needs devmode + // to be installed. + ErrorKindSnapNeedsDevMode ErrorKind = "snap-needs-devmode" + // ErrorKindSnapNeedsClassic: the requested snap needs classic + // confinement to be installed. + ErrorKindSnapNeedsClassic ErrorKind = "snap-needs-classic" + // ErrorKindSnapNeedsClassicSystem: the requested snap can't + // be installed on the current non-classic system. + ErrorKindSnapNeedsClassicSystem ErrorKind = "snap-needs-classic-system" + // ErrorKindSnapNotClassic: snap not compatible with classic mode. + ErrorKindSnapNotClassic ErrorKind = "snap-not-classic" + // ErrorKindSnapNoUpdateAvailable: the requested snap does not + // have an update available. + ErrorKindSnapNoUpdateAvailable ErrorKind = "snap-no-update-available" + // ErrorKindSnapRevisionNotAvailable: no snap revision available + // as specified. + ErrorKindSnapRevisionNotAvailable ErrorKind = "snap-revision-not-available" + // ErrorKindSnapChannelNotAvailable: no snap revision on specified + // channel. The `value` of the error is a rich object with + // requested `snap-name`, `action`, `channel`, `architecture`, and + // actually available `releases` as list of + // `{"architecture":... , "channel": ...}` objects. + ErrorKindSnapChannelNotAvailable ErrorKind = "snap-channel-not-available" + // ErrorKindSnapArchitectureNotAvailable: no snap revision on + // specified architecture. Value has the same format as for + // `snap-channel-not-available`. + ErrorKindSnapArchitectureNotAvailable ErrorKind = "snap-architecture-not-available" + + // ErrorKindSnapChangeConflict: the requested operation would + // conflict with currently ongoing change. This is a temporary + // error. The error `value` is an object with optional fields + // `snap-name`, `change-kind` of the ongoing change. + ErrorKindSnapChangeConflict ErrorKind = "snap-change-conflict" + + // ErrorKindNotSnap: the given snap or directory does not + // look like a snap. + ErrorKindNotSnap ErrorKind = "snap-not-a-snap" + + // ErrorKindInterfacesUnchanged: the requested interfaces' + // operation would have no effect. + ErrorKindInterfacesUnchanged ErrorKind = "interfaces-unchanged" + + // ErrorKindBadQuery: a bad query was provided. + ErrorKindBadQuery ErrorKind = "bad-query" + // ErrorKindConfigNoSuchOption: the given configuration option + // does not exist. + ErrorKindConfigNoSuchOption ErrorKind = "option-not-found" + + // ErrorKindAssertionNotFound: assertion can not be found. + ErrorKindAssertionNotFound ErrorKind = "assertion-not-found" + + // ErrorKindUnsuccessful: snapctl command was unsuccessful. + ErrorKindUnsuccessful ErrorKind = "unsuccessful" + + // ErrorKindNetworkTimeout: a timeout occurred during the request. + ErrorKindNetworkTimeout ErrorKind = "network-timeout" + + // ErrorKindDNSFailure: DNS not responding. + ErrorKindDNSFailure ErrorKind = "dns-failure" + + // ErrorKindInsufficientDiskSpace: not enough disk space to perform the request. + ErrorKindInsufficientDiskSpace ErrorKind = "insufficient-disk-space" +) + +// Maintenance error kinds. +// These are used only inside the maintenance field of responses. +// Keep https://forum.snapcraft.io/t/using-the-rest-api/18603#heading--maint-errors in sync using doc/error-kinds.go. +const ( + // ErrorKindDaemonRestart: daemon is restarting. + ErrorKindDaemonRestart ErrorKind = "daemon-restart" + // ErrorKindSystemRestart: system is restarting. + ErrorKindSystemRestart ErrorKind = "system-restart" +) diff -Nru snapd-2.45.1+20.04.2/client/export_test.go snapd-2.48.3+20.04/client/export_test.go --- snapd-2.45.1+20.04.2/client/export_test.go 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/client/export_test.go 2021-02-02 08:21:12.000000000 +0000 @@ -30,11 +30,11 @@ client.doer = d } -type DoFlags = doFlags +type DoOptions = doOptions // Do does do. -func (client *Client) Do(method, path string, query url.Values, body io.Reader, v interface{}, flags DoFlags) (statusCode int, err error) { - return client.do(method, path, query, nil, body, v, flags) +func (client *Client) Do(method, path string, query url.Values, body io.Reader, v interface{}, opts *DoOptions) (statusCode int, err error) { + return client.do(method, path, query, nil, body, v, opts) } // expose parseError for testing diff -Nru snapd-2.45.1+20.04.2/client/icons.go snapd-2.48.3+20.04/client/icons.go --- snapd-2.45.1+20.04.2/client/icons.go 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/client/icons.go 2021-02-02 08:21:12.000000000 +0000 @@ -40,13 +40,12 @@ func (c *Client) Icon(pkgID string) (*Icon, error) { const errPrefix = "cannot retrieve icon" - ctx, cancel := context.WithTimeout(context.Background(), doTimeout) - defer cancel() - response, err := c.raw(ctx, "GET", fmt.Sprintf("/v2/icons/%s/icon", pkgID), nil, nil, nil) + response, cancel, err := c.rawWithTimeout(context.Background(), "GET", fmt.Sprintf("/v2/icons/%s/icon", pkgID), nil, nil, nil, nil) if err != nil { fmt := "%s: failed to communicate with server: %w" return nil, xerrors.Errorf(fmt, errPrefix, err) } + defer cancel() defer response.Body.Close() if response.StatusCode != 200 { diff -Nru snapd-2.45.1+20.04.2/client/login.go snapd-2.48.3+20.04/client/login.go --- snapd-2.45.1+20.04.2/client/login.go 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/client/login.go 2021-02-02 08:21:12.000000000 +0000 @@ -94,7 +94,7 @@ } if homeDir == "" { - real, err := osutil.RealUser() + real, err := osutil.UserMaybeSudoUser() if err != nil { panic(err) } @@ -106,7 +106,7 @@ // writeAuthData saves authentication details for later reuse through ReadAuthData func writeAuthData(user User) error { - real, err := osutil.RealUser() + real, err := osutil.UserMaybeSudoUser() if err != nil { return err } diff -Nru snapd-2.45.1+20.04.2/client/model.go snapd-2.48.3+20.04/client/model.go --- snapd-2.45.1+20.04.2/client/model.go 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/client/model.go 2021-02-02 08:21:12.000000000 +0000 @@ -80,13 +80,12 @@ func currentAssertion(client *Client, path string) (asserts.Assertion, error) { q := url.Values{} - ctx, cancel := context.WithTimeout(context.Background(), doTimeout) - defer cancel() - response, err := client.raw(ctx, "GET", path, q, nil, nil) + response, cancel, err := client.rawWithTimeout(context.Background(), "GET", path, q, nil, nil, nil) if err != nil { fmt := "failed to query current assertion: %w" return nil, xerrors.Errorf(fmt, err) } + defer cancel() defer response.Body.Close() if response.StatusCode != 200 { return nil, parseError(response) diff -Nru snapd-2.45.1+20.04.2/client/snapctl.go snapd-2.48.3+20.04/client/snapctl.go --- snapd-2.45.1+20.04.2/client/snapctl.go 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/client/snapctl.go 2021-02-02 08:21:12.000000000 +0000 @@ -23,8 +23,21 @@ "bytes" "encoding/json" "fmt" + "io" + "io/ioutil" ) +// InternalSsnapctlCmdNeedsStdin returns true if the given snapctl command +// needs data from stdin +func InternalSnapctlCmdNeedsStdin(name string) bool { + switch name { + case "fde-setup-result": + return true + default: + return false + } +} + // SnapCtlOptions holds the various options with which snapctl is invoked. type SnapCtlOptions struct { // ContextID is a string used to determine the context of this call (e.g. @@ -35,14 +48,36 @@ Args []string `json:"args"` } +// SnapCtlPostData is the data posted to the daemon /v2/snapctl endpoint +// TODO: this can be removed again once we no longer need to pass stdin data +// but instead use a real stdin stream +type SnapCtlPostData struct { + SnapCtlOptions + + Stdin []byte `json:"stdin,omitempty"` +} + type snapctlOutput struct { Stdout string `json:"stdout"` Stderr string `json:"stderr"` } // RunSnapctl requests a snapctl run for the given options. -func (client *Client) RunSnapctl(options *SnapCtlOptions) (stdout, stderr []byte, err error) { - b, err := json.Marshal(options) +func (client *Client) RunSnapctl(options *SnapCtlOptions, stdin io.Reader) (stdout, stderr []byte, err error) { + // TODO: instead of reading all of stdin here we need to forward it to + // the daemon eventually + var stdinData []byte + if stdin != nil { + stdinData, err = ioutil.ReadAll(stdin) + if err != nil { + return nil, nil, fmt.Errorf("cannot read stdin: %v", err) + } + } + + b, err := json.Marshal(SnapCtlPostData{ + SnapCtlOptions: *options, + Stdin: stdinData, + }) if err != nil { return nil, nil, fmt.Errorf("cannot marshal options: %s", err) } diff -Nru snapd-2.45.1+20.04.2/client/snapctl_test.go snapd-2.48.3+20.04/client/snapctl_test.go --- snapd-2.45.1+20.04.2/client/snapctl_test.go 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/client/snapctl_test.go 2021-02-02 08:21:12.000000000 +0000 @@ -20,6 +20,8 @@ package client_test import ( + "bytes" + "encoding/base64" "encoding/json" "github.com/snapcore/snapd/client" @@ -32,7 +34,7 @@ ContextID: "1234ABCD", Args: []string{"foo", "bar"}, } - cs.cli.RunSnapctl(options) + cs.cli.RunSnapctl(options, nil) c.Check(cs.req.Method, check.Equals, "POST") c.Check(cs.req.URL.Path, check.Equals, "/v2/snapctl") } @@ -47,12 +49,13 @@ } }` + mockStdin := bytes.NewBufferString("some-input") options := &client.SnapCtlOptions{ ContextID: "1234ABCD", Args: []string{"foo", "bar"}, } - stdout, stderr, err := cs.cli.RunSnapctl(options) + stdout, stderr, err := cs.cli.RunSnapctl(options, mockStdin) c.Assert(err, check.IsNil) c.Check(string(stdout), check.Equals, "test stdout") c.Check(string(stderr), check.Equals, "test stderr") @@ -64,5 +67,18 @@ c.Check(body, check.DeepEquals, map[string]interface{}{ "context-id": "1234ABCD", "args": []interface{}{"foo", "bar"}, + + // json byte-stream is b64 encoded + "stdin": base64.StdEncoding.EncodeToString([]byte("some-input")), }) } + +func (cs *clientSuite) TestInternalSnapctlCmdNeedsStdin(c *check.C) { + res := client.InternalSnapctlCmdNeedsStdin("fde-setup-result") + c.Check(res, check.Equals, true) + + for _, s := range []string{"help", "other"} { + res := client.InternalSnapctlCmdNeedsStdin(s) + c.Check(res, check.Equals, false) + } +} diff -Nru snapd-2.45.1+20.04.2/client/snap_op.go snapd-2.48.3+20.04/client/snap_op.go --- snapd-2.45.1+20.04.2/client/snap_op.go 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/client/snap_op.go 2021-02-02 08:21:12.000000000 +0000 @@ -40,6 +40,7 @@ Classic bool `json:"classic,omitempty"` Dangerous bool `json:"dangerous,omitempty"` IgnoreValidation bool `json:"ignore-validation,omitempty"` + IgnoreRunning bool `json:"ignore-running,omitempty"` Unaliased bool `json:"unaliased,omitempty"` Purge bool `json:"purge,omitempty"` Amend bool `json:"amend,omitempty"` @@ -74,6 +75,9 @@ } func (opts *SnapOptions) writeOptionFields(mw *multipart.Writer) error { + if err := writeFieldBool(mw, "ignore-running", opts.IgnoreRunning); err != nil { + return err + } return writeFieldBool(mw, "unaliased", opts.Unaliased) } @@ -204,7 +208,7 @@ "Content-Type": "application/json", } - return client.doAsyncFull("POST", "/v2/snaps", nil, headers, bytes.NewBuffer(data), doFlags{}) + return client.doAsyncFull("POST", "/v2/snaps", nil, headers, bytes.NewBuffer(data), nil) } // InstallPath sideloads the snap with the given path under optional provided name, @@ -230,7 +234,8 @@ "Content-Type": mw.FormDataContentType(), } - return client.doAsyncNoTimeout("POST", "/v2/snaps", nil, headers, pr) + _, changeID, err = client.doAsyncFull("POST", "/v2/snaps", nil, headers, pr, doNoTimeoutAndRetry) + return changeID, err } // Try diff -Nru snapd-2.45.1+20.04.2/client/snap_op_test.go snapd-2.48.3+20.04/client/snap_op_test.go --- snapd-2.45.1+20.04.2/client/snap_op_test.go 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/client/snap_op_test.go 2021-02-02 08:21:12.000000000 +0000 @@ -242,6 +242,37 @@ c.Check(id, check.Equals, "66b3") } +func (cs *clientSuite) TestClientOpInstallPathIgnoreRunning(c *check.C) { + cs.status = 202 + cs.rsp = `{ + "change": "66b3", + "status-code": 202, + "type": "async" + }` + bodyData := []byte("snap-data") + + snap := filepath.Join(c.MkDir(), "foo.snap") + err := ioutil.WriteFile(snap, bodyData, 0644) + c.Assert(err, check.IsNil) + + id, err := cs.cli.InstallPath(snap, "", &client.SnapOptions{IgnoreRunning: true}) + c.Assert(err, check.IsNil) + + body, err := ioutil.ReadAll(cs.req.Body) + c.Assert(err, check.IsNil) + + c.Assert(string(body), check.Matches, "(?s).*\r\nsnap-data\r\n.*") + c.Assert(string(body), check.Matches, "(?s).*Content-Disposition: form-data; name=\"action\"\r\n\r\ninstall\r\n.*") + c.Assert(string(body), check.Matches, "(?s).*Content-Disposition: form-data; name=\"ignore-running\"\r\n\r\ntrue\r\n.*") + + c.Check(cs.req.Method, check.Equals, "POST") + c.Check(cs.req.URL.Path, check.Equals, fmt.Sprintf("/v2/snaps")) + c.Assert(cs.req.Header.Get("Content-Type"), check.Matches, "multipart/form-data; boundary=.*") + _, ok := cs.req.Context().Deadline() + c.Assert(ok, check.Equals, false) + c.Check(id, check.Equals, "66b3") +} + func (cs *clientSuite) TestClientOpInstallPathInstance(c *check.C) { cs.status = 202 cs.rsp = `{ diff -Nru snapd-2.45.1+20.04.2/client/snapshot.go snapd-2.48.3+20.04/client/snapshot.go --- snapd-2.45.1+20.04.2/client/snapshot.go 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/client/snapshot.go 2021-02-02 08:21:12.000000000 +0000 @@ -21,9 +21,11 @@ import ( "bytes" + "context" "encoding/json" "errors" "fmt" + "io" "net/url" "strconv" "strings" @@ -32,6 +34,9 @@ "github.com/snapcore/snapd/snap" ) +// SnapshotExportMediaType is the media type used to identify snapshot exports in the API. +const SnapshotExportMediaType = "application/x.snapd.snapshot" + var ( ErrSnapshotSetNotFound = errors.New("no snapshot set with the given ID") ErrSnapshotSnapsNotFound = errors.New("no snapshot for the requested snaps found in the set with the given ID") @@ -176,3 +181,50 @@ return client.doAsync("POST", "/v2/snapshots", nil, headers, bytes.NewBuffer(data)) } + +// SnapshotExport streams the requested snapshot set. +// +// The return value includes the length of the returned stream. +func (client *Client) SnapshotExport(setID uint64) (stream io.ReadCloser, contentLength int64, err error) { + rsp, err := client.raw(context.Background(), "GET", fmt.Sprintf("/v2/snapshots/%v/export", setID), nil, nil, nil) + if err != nil { + return nil, 0, err + } + if rsp.StatusCode != 200 { + defer rsp.Body.Close() + + var r response + specificErr := r.err(client, rsp.StatusCode) + if err != nil { + return nil, 0, specificErr + } + return nil, 0, fmt.Errorf("unexpected status code: %v", rsp.Status) + } + contentType := rsp.Header.Get("Content-Type") + if contentType != SnapshotExportMediaType { + return nil, 0, fmt.Errorf("unexpected snapshot export content type %q", contentType) + } + + return rsp.Body, rsp.ContentLength, nil +} + +// SnapshotImportSet is a snapshot import created by a "snap import-snapshot". +type SnapshotImportSet struct { + ID uint64 `json:"set-id"` + Snaps []string `json:"snaps"` +} + +// SnapshotImport imports an exported snapshot set. +func (client *Client) SnapshotImport(exportStream io.Reader, size int64) (SnapshotImportSet, error) { + headers := map[string]string{ + "Content-Type": SnapshotExportMediaType, + "Content-Length": strconv.FormatInt(size, 10), + } + + var importSet SnapshotImportSet + if _, err := client.doSync("POST", "/v2/snapshots", nil, headers, exportStream, &importSet); err != nil { + return importSet, err + } + + return importSet, nil +} diff -Nru snapd-2.45.1+20.04.2/client/snapshot_test.go snapd-2.48.3+20.04/client/snapshot_test.go --- snapd-2.45.1+20.04.2/client/snapshot_test.go 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/client/snapshot_test.go 2021-02-02 08:21:12.000000000 +0000 @@ -20,7 +20,11 @@ package client_test import ( + "io/ioutil" + "net/http" "net/url" + "strconv" + "strings" "time" "gopkg.in/check.v1" @@ -137,3 +141,79 @@ func (cs *clientSuite) TestClientRestoreSnapshots(c *check.C) { cs.testClientSnapshotAction(c, "restore", cs.cli.RestoreSnapshots) } + +func (cs *clientSuite) TestClientExportSnapshot(c *check.C) { + type tableT struct { + content string + contentType string + status int + } + + table := []tableT{ + {"dummy-export", client.SnapshotExportMediaType, 200}, + {"dummy-export", "application/x-tar", 400}, + {"", "", 400}, + } + + for i, t := range table { + comm := check.Commentf("%d: %q", i, t.content) + + cs.contentLength = int64(len(t.content)) + cs.header = http.Header{"Content-Type": []string{t.contentType}} + cs.rsp = t.content + cs.status = t.status + + r, size, err := cs.cli.SnapshotExport(42) + if t.status == 200 { + c.Assert(err, check.IsNil, comm) + c.Assert(cs.countingCloser.closeCalled, check.Equals, 0) + c.Assert(size, check.Equals, int64(len(t.content)), comm) + } else { + c.Assert(err.Error(), check.Equals, "unexpected status code: ") + c.Assert(cs.countingCloser.closeCalled, check.Equals, 1) + } + + if t.status == 200 { + buf, err := ioutil.ReadAll(r) + c.Assert(err, check.IsNil) + c.Assert(string(buf), check.Equals, t.content) + } + } +} + +func (cs *clientSuite) TestClientSnapshotImport(c *check.C) { + type tableT struct { + rsp string + status int + setID uint64 + error string + } + table := []tableT{ + {`{"type": "sync", "result": {"set-id": 42, "snaps": ["baz", "bar", "foo"]}}`, 200, 42, ""}, + {`{"type": "error"}`, 400, 0, "server error: \"Bad Request\""}, + } + + for i, t := range table { + comm := check.Commentf("%d: %s", i, t.rsp) + + cs.rsp = t.rsp + cs.status = t.status + + fakeSnapshotData := "fake" + r := strings.NewReader(fakeSnapshotData) + importSet, err := cs.cli.SnapshotImport(r, int64(len(fakeSnapshotData))) + if t.error != "" { + c.Assert(err, check.NotNil, comm) + c.Check(err.Error(), check.Equals, t.error, comm) + continue + } + c.Assert(err, check.IsNil, comm) + c.Assert(cs.req.Header.Get("Content-Type"), check.Equals, client.SnapshotExportMediaType) + c.Assert(cs.req.Header.Get("Content-Length"), check.Equals, strconv.Itoa(len(fakeSnapshotData))) + c.Check(importSet.ID, check.Equals, t.setID, comm) + c.Check(importSet.Snaps, check.DeepEquals, []string{"baz", "bar", "foo"}, comm) + d, err := ioutil.ReadAll(cs.req.Body) + c.Assert(err, check.IsNil) + c.Check(string(d), check.Equals, fakeSnapshotData) + } +} diff -Nru snapd-2.45.1+20.04.2/client/systems.go snapd-2.48.3+20.04/client/systems.go --- snapd-2.45.1+20.04.2/client/systems.go 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/client/systems.go 2021-02-02 08:21:12.000000000 +0000 @@ -103,3 +103,38 @@ } return nil } + +// RebootToSystem issues a request to reboot into system with the +// given label and the given mode. +// +// When called without a systemLabel and without a mode it will just +// trigger a regular reboot. +// +// When called without a systemLabel but with a mode it will use +// the current system to enter the given mode. +// +// Note that "recover" and "run" modes are only available for the +// current system. +func (client *Client) RebootToSystem(systemLabel, mode string) error { + // verification is done by the backend + + req := struct { + Action string `json:"action"` + Mode string `json:"mode"` + }{ + Action: "reboot", + Mode: mode, + } + + var body bytes.Buffer + if err := json.NewEncoder(&body).Encode(&req); err != nil { + return err + } + if _, err := client.doSync("POST", "/v2/systems/"+systemLabel, nil, nil, &body, nil); err != nil { + if systemLabel != "" { + return xerrors.Errorf("cannot request system reboot into %q: %v", systemLabel, err) + } + return xerrors.Errorf("cannot request system reboot: %v", err) + } + return nil +} diff -Nru snapd-2.45.1+20.04.2/client/systems_test.go snapd-2.48.3+20.04/client/systems_test.go --- snapd-2.45.1+20.04.2/client/systems_test.go 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/client/systems_test.go 2021-02-02 08:21:12.000000000 +0000 @@ -169,3 +169,49 @@ err = cs.cli.DoSystemAction("1234", nil) c.Assert(err, check.ErrorMatches, "cannot request an action without one") } + +func (cs *clientSuite) TestRequestSystemRebootHappy(c *check.C) { + cs.rsp = `{ + "type": "sync", + "status-code": 200, + "result": {} + }` + err := cs.cli.RebootToSystem("20201212", "install") + c.Assert(err, check.IsNil) + c.Check(cs.req.Method, check.Equals, "POST") + c.Check(cs.req.URL.Path, check.Equals, "/v2/systems/20201212") + + 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": "reboot", + "mode": "install", + }) +} + +func (cs *clientSuite) TestRequestSystemRebootErrorNoSystem(c *check.C) { + cs.rsp = `{ + "type": "error", + "status-code": 500, + "result": {"message": "failed"} + }` + err := cs.cli.RebootToSystem("", "install") + c.Assert(err, check.ErrorMatches, `cannot request system reboot: failed`) + c.Check(cs.req.Method, check.Equals, "POST") + c.Check(cs.req.URL.Path, check.Equals, "/v2/systems") +} + +func (cs *clientSuite) TestRequestSystemRebootErrorWithSystem(c *check.C) { + cs.rsp = `{ + "type": "error", + "status-code": 500, + "result": {"message": "failed"} + }` + err := cs.cli.RebootToSystem("1234", "install") + c.Assert(err, check.ErrorMatches, `cannot request system reboot into "1234": failed`) + c.Check(cs.req.Method, check.Equals, "POST") + c.Check(cs.req.URL.Path, check.Equals, "/v2/systems/1234") +} diff -Nru snapd-2.45.1+20.04.2/client/users.go snapd-2.48.3+20.04/client/users.go --- snapd-2.45.1+20.04.2/client/users.go 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/client/users.go 2021-02-02 08:21:12.000000000 +0000 @@ -45,6 +45,8 @@ Sudoer bool `json:"sudoer,omitempty"` Known bool `json:"known,omitempty"` ForceManaged bool `json:"force-managed,omitempty"` + // Automatic is for internal snapd use, behavior might evolve + Automatic bool `json:"automatic,omitempty"` } // RemoveUserOptions holds options for removing a local system user. @@ -88,7 +90,7 @@ // Results may be provided even if there are errors. func (client *Client) CreateUsers(options []*CreateUserOptions) ([]*CreateUserResult, error) { for _, opts := range options { - if opts == nil || (opts.Email == "" && !opts.Known) { + if opts == nil || (opts.Email == "" && !(opts.Known || opts.Automatic)) { return nil, fmt.Errorf("cannot create user from store details without an email to query for") } } diff -Nru snapd-2.45.1+20.04.2/client/users_test.go snapd-2.48.3+20.04/client/users_test.go --- snapd-2.45.1+20.04.2/client/users_test.go 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/client/users_test.go 2021-02-02 08:21:12.000000000 +0000 @@ -139,11 +139,25 @@ Username: "two", }, { Username: "three", + }, + }, +}, { + options: []*client.CreateUserOptions{{ + Automatic: true, }}, -}} + bodies: []string{ + `{"action":"create","automatic":true}`, + }, + responses: []string{ + // for automatic result can be empty + `{"type": "sync", "result": []}`, + }, +}, +} func (cs *clientSuite) TestClientCreateUsers(c *C) { for _, test := range createUsersTests { + cs.reqs = nil cs.rsps = test.responses results, err := cs.cli.CreateUsers(test.options) diff -Nru snapd-2.45.1+20.04.2/cmd/autogen.sh snapd-2.48.3+20.04/cmd/autogen.sh --- snapd-2.45.1+20.04.2/cmd/autogen.sh 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/cmd/autogen.sh 2021-02-02 08:21:12.000000000 +0000 @@ -32,6 +32,9 @@ debian) extra_opts="--libexecdir=/usr/lib/snapd" ;; + gentoo) + extra_opts="--libexecdir=/usr/lib/snapd --with-snap-mount-dir=/var/lib/snapd/snap --enable-apparmor --enable-nvidia-biarch --enable-merged-usr" + ;; ubuntu) extra_opts="--libexecdir=/usr/lib/snapd --enable-nvidia-multiarch --enable-static-libcap --enable-static-libapparmor --with-host-arch-triplet=$(dpkg-architecture -qDEB_HOST_MULTIARCH)" if [ "$(dpkg-architecture -qDEB_HOST_ARCH)" = "amd64" ]; then diff -Nru snapd-2.45.1+20.04.2/cmd/cmd_linux.go snapd-2.48.3+20.04/cmd/cmd_linux.go --- snapd-2.45.1+20.04.2/cmd/cmd_linux.go 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/cmd/cmd_linux.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,218 +0,0 @@ -// -*- Mode: Go; indent-tabs-mode: t -*- - -/* - * Copyright (C) 2016 Canonical Ltd - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 3 as - * published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - */ - -package cmd - -import ( - "log" - "os" - "path/filepath" - "strings" - "syscall" - - "github.com/snapcore/snapd/cmd/cmdutil" - "github.com/snapcore/snapd/dirs" - "github.com/snapcore/snapd/logger" - "github.com/snapcore/snapd/osutil" - "github.com/snapcore/snapd/release" - "github.com/snapcore/snapd/strutil" -) - -// The SNAP_REEXEC environment variable controls whether the command -// will attempt to re-exec itself from inside an ubuntu-core snap -// present on the system. If not present in the environ it's assumed -// to be set to 1 (do re-exec); that is: set it to 0 to disable. -const reExecKey = "SNAP_REEXEC" - -var ( - // snapdSnap is the place to look for the snapd snap; we will re-exec - // here - snapdSnap = "/snap/snapd/current" - - // coreSnap is the place to look for the core snap; we will re-exec - // here if there is no snapd snap - coreSnap = "/snap/core/current" - - // selfExe is the path to a symlink pointing to the current executable - selfExe = "/proc/self/exe" - - syscallExec = syscall.Exec - osReadlink = os.Readlink -) - -// distroSupportsReExec returns true if the distribution we are running on can use re-exec. -// -// This is true by default except for a "core/all" snap system where it makes -// no sense and in certain distributions that we don't want to enable re-exec -// yet because of missing validation or other issues. -func distroSupportsReExec() bool { - if !release.OnClassic { - return false - } - if !release.DistroLike("debian", "ubuntu") { - logger.Debugf("re-exec not supported on distro %q yet", release.ReleaseInfo.ID) - return false - } - return true -} - -// coreSupportsReExec returns true if the given core/snapd snap should be used as re-exec target. -// -// Ensure we do not use older version of snapd, look for info file and ignore -// version of core that do not yet have it. -func coreSupportsReExec(coreOrSnapdPath string) bool { - infoPath := filepath.Join(coreOrSnapdPath, filepath.Join(dirs.CoreLibExecDir, "info")) - ver, err := cmdutil.SnapdVersionFromInfoFile(infoPath) - if err != nil { - logger.Noticef("%v", err) - return false - } - - // > 0 means our Version is bigger than the version of snapd in core - res, err := strutil.VersionCompare(Version, ver) - if err != nil { - logger.Debugf("cannot version compare %q and %q: %v", Version, ver, err) - return false - } - if res > 0 { - logger.Debugf("snap (at %q) is older (%q) than distribution package (%q)", coreOrSnapdPath, ver, Version) - return false - } - return true -} - -// TODO: move to cmd/cmdutil/ -// -// InternalToolPath returns the path of an internal snapd tool. The tool -// *must* be located inside the same tree as the current binary. -// -// The return value is either the path of the tool in the current distribution -// or in the core/snapd snap (or the ubuntu-core snap) if the current binary is -// ran from that location. -func InternalToolPath(tool string) (string, error) { - distroTool := filepath.Join(dirs.DistroLibExecDir, tool) - - // find the internal path relative to the running snapd, this - // ensure we don't rely on the state of the system (like - // having a valid "current" symlink). - exe, err := osReadlink("/proc/self/exe") - if err != nil { - return "", err - } - - if !strings.HasPrefix(exe, dirs.DistroLibExecDir) { - // either running from mounted location or /usr/bin/snap* - - // find the local prefix to the snap: - // /snap/snapd/123/usr/bin/snap -> /snap/snapd/123 - // /snap/core/234/usr/lib/snapd/snapd -> /snap/core/234 - idx := strings.LastIndex(exe, "/usr/") - if idx > 0 { - // only assume mounted location when path contains - // /usr/, but does not start with one - prefix := exe[:idx] - return filepath.Join(prefix, "/usr/lib/snapd", tool), nil - } - if idx == -1 { - // or perhaps some other random location, make sure the tool - // exists there and is an executable - maybeTool := filepath.Join(filepath.Dir(exe), tool) - if osutil.IsExecutable(maybeTool) { - return maybeTool, nil - } - } - } - - // fallback to distro tool - return distroTool, nil -} - -// mustUnsetenv will unset the given environment key or panic if it -// cannot do that -func mustUnsetenv(key string) { - if err := os.Unsetenv(key); err != nil { - log.Panicf("cannot unset %s: %s", key, err) - } -} - -// ExecInSnapdOrCoreSnap makes sure you're executing the binary that ships in -// the snapd/core snap. -func ExecInSnapdOrCoreSnap() { - // Which executable are we? - exe, err := os.Readlink(selfExe) - if err != nil { - logger.Noticef("cannot read /proc/self/exe: %v", err) - return - } - - // Special case for snapd re-execing from 2.21. In this - // version of snap/snapd we did set SNAP_REEXEC=0 when we - // re-execed. In this case we need to unset the reExecKey to - // ensure that subsequent run of snap/snapd (e.g. when using - // classic confinement) will *not* prevented from re-execing. - if strings.HasPrefix(exe, dirs.SnapMountDir) && !osutil.GetenvBool(reExecKey, true) { - mustUnsetenv(reExecKey) - return - } - - // If we are asked not to re-execute use distribution packages. This is - // "spiritual" re-exec so use the same environment variable to decide. - if !osutil.GetenvBool(reExecKey, true) { - logger.Debugf("re-exec disabled by user") - return - } - - // Did we already re-exec? - if strings.HasPrefix(exe, dirs.SnapMountDir) { - return - } - - // If the distribution doesn't support re-exec or run-from-core then don't do it. - if !distroSupportsReExec() { - return - } - - // Is this executable in the core snap too? - coreOrSnapdPath := snapdSnap - full := filepath.Join(snapdSnap, exe) - if !osutil.FileExists(full) { - coreOrSnapdPath = coreSnap - full = filepath.Join(coreSnap, exe) - if !osutil.FileExists(full) { - return - } - } - - // If the core snap doesn't support re-exec or run-from-core then don't do it. - if !coreSupportsReExec(coreOrSnapdPath) { - return - } - - logger.Debugf("restarting into %q", full) - panic(syscallExec(full, os.Args, os.Environ())) -} - -// MockOsReadlink is for use in tests -func MockOsReadlink(f func(string) (string, error)) func() { - realOsReadlink := osReadlink - osReadlink = f - return func() { - osReadlink = realOsReadlink - } -} diff -Nru snapd-2.45.1+20.04.2/cmd/cmd_linux_test.go snapd-2.48.3+20.04/cmd/cmd_linux_test.go --- snapd-2.45.1+20.04.2/cmd/cmd_linux_test.go 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/cmd/cmd_linux_test.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,117 +0,0 @@ -package cmd - -import ( - "io/ioutil" - "os" - "path/filepath" - "testing" - - "github.com/snapcore/snapd/dirs" -) - -const dataOK = `one line -another line -yadda yadda -VERSION=42 -potatoes -` - -const dataNOK = `a line -another -this is a very long line -that wasn't long what are you talking about long lines are like, so long you need to add things like commas to them for them to even make sense -a short one -and another -what is this -why -no -stop -` - -const dataHuge = `Lorem ipsum dolor sit amet, consectetur adipiscing elit. -Quisque euismod ac elit ac auctor. -Proin malesuada diam ac tellus maximus aliquam. -Aenean tincidunt mi et tortor bibendum fringilla. -Phasellus finibus, urna id convallis vestibulum, metus metus venenatis massa, et efficitur nisi elit in massa. -Mauris at nisl leo. -Nulla ullamcorper risus venenatis massa venenatis, ac finibus lacus aliquam. -Nunc tempor convallis cursus. -Maecenas id rhoncus orci, eget pretium eros. - -Donec et consectetur lacus. -Nam nec mattis elit, id sollicitudin magna. -Aenean sit amet diam vitae tellus finibus tristique. -Duis et pharetra tortor, id pharetra erat. -Suspendisse commodo venenatis blandit. -Morbi tellus est, iaculis et tincidunt nec, semper ut ipsum. -Mauris quis condimentum risus. -Lorem ipsum dolor sit amet, consectetur adipiscing elit. -Mauris gravida turpis ut urna laoreet, sit amet tempor odio porttitor. - -Aliquam nibh libero, venenatis ac vehicula at, blandit id odio. -Etiam malesuada consectetur porta. -Fusce consectetur ligula et metus interdum sollicitudin. -Pellentesque odio neque, pharetra et gravida non, vestibulum nec lorem. -Sed condimentum velit ex, sit amet viverra lectus aliquet quis. -Aliquam tincidunt eu elit at condimentum. -Donec feugiat urna tortor, pellentesque tincidunt quam congue eu. - -Phasellus vel libero molestie, semper erat at, suscipit nisi. -Nullam euismod neque ut turpis molestie, eu fringilla elit volutpat. -Phasellus maximus, urna eget porta congue, diam enim volutpat diam, nec ultrices lorem risus ac metus. -Vivamus convallis eros non nunc pretium bibendum. -Maecenas consectetur metus metus. -Morbi scelerisque urna at arcu tristique feugiat. -Vestibulum condimentum odio sed tortor vulputate, eget hendrerit mi consequat. -Integer egestas finibus augue, ac scelerisque ex pretium aliquam. -Aliquam erat volutpat. -Suspendisse a nulla ultrices, porttitor tellus ut, bibendum diam. -In nibh dui, tempus eget vestibulum in, euismod in ex. -In tempus felis lectus. - -Maecenas suscipit turpis eget velit molestie, quis luctus nibh placerat. -Nulla semper eleifend nisi ut dignissim. -Donec eu massa maximus, blandit massa ac, lobortis risus. -Donec id condimentum libero, vel fringilla diam. -Praesent ultrices, ante congue sollicitudin sagittis, orci ex maximus ipsum, at convallis nunc nisl nec lorem. -Duis iaculis finibus fermentum. -Curabitur quis pharetra metus. -Donec nisl ipsum, faucibus vitae odio sed, mattis feugiat nisl. -Pellentesque nec justo in magna volutpat accumsan. -Pellentesque porttitor justo non velit porta rhoncus. -Nulla ut lectus quis lectus rutrum dignissim. -Pellentesque posuere sagittis felis, quis varius purus pharetra eu. -Nam blandit diam ullamcorper, auctor massa at, aliquet dui. -Aliquam erat volutpat. -Nullam sit amet augue nec diam sollicitudin ullamcorper a vitae neque. -VERSION=42 -` - -func benchmarkCSRE(b *testing.B, data string) { - tempdir, err := ioutil.TempDir("", "") - if err != nil { - b.Fatalf("tempdir: %v", err) - } - defer os.RemoveAll(tempdir) - if err = os.MkdirAll(filepath.Join(tempdir, dirs.CoreLibExecDir), 0755); err != nil { - b.Fatalf("mkdirall: %v", err) - } - - if err = ioutil.WriteFile(filepath.Join(tempdir, dirs.CoreLibExecDir, "info"), []byte(data), 0600); err != nil { - b.Fatalf("%v", err) - } - b.ResetTimer() - for i := 0; i < b.N; i++ { - coreSupportsReExec(tempdir) - } -} - -func BenchmarkCSRE_fakeOK(b *testing.B) { benchmarkCSRE(b, dataOK) } -func BenchmarkCSRE_fakeNOK(b *testing.B) { benchmarkCSRE(b, dataNOK) } -func BenchmarkCSRE_fakeHuge(b *testing.B) { benchmarkCSRE(b, dataHuge) } - -func BenchmarkCSRE_real(b *testing.B) { - for i := 0; i < b.N; i++ { - coreSupportsReExec("/snap/core/current") - } -} diff -Nru snapd-2.45.1+20.04.2/cmd/cmd_other.go snapd-2.48.3+20.04/cmd/cmd_other.go --- snapd-2.45.1+20.04.2/cmd/cmd_other.go 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/cmd/cmd_other.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,40 +0,0 @@ -// -*- Mode: Go; indent-tabs-mode: t -*- -// +build !linux - -/* - * 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 cmd - -import ( - "errors" -) - -// ExecInSnapdOrCoreSnap makes sure you're executing the binary that ships in -// the snapd/core snap. -// On this OS this is a stub. -func ExecInSnapdOrCoreSnap() { - return -} - -// InternalToolPath returns the path of an internal snapd tool. The tool -// *must* be located inside the same tree as the current binary. -// -// On this OS this is a stub and always returns an error. -func InternalToolPath(tool string) (string, error) { - return "", errors.New("unsupported on non-Linux systems") -} diff -Nru snapd-2.45.1+20.04.2/cmd/cmd_test.go snapd-2.48.3+20.04/cmd/cmd_test.go --- snapd-2.45.1+20.04.2/cmd/cmd_test.go 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/cmd/cmd_test.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,419 +0,0 @@ -// -*- 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 cmd_test - -import ( - "fmt" - "io/ioutil" - "os" - "path/filepath" - "testing" - - . "gopkg.in/check.v1" - - "github.com/snapcore/snapd/cmd" - "github.com/snapcore/snapd/dirs" - "github.com/snapcore/snapd/logger" - "github.com/snapcore/snapd/release" -) - -func Test(t *testing.T) { TestingT(t) } - -type cmdSuite struct { - restoreExec func() - restoreLogger func() - execCalled int - lastExecArgv0 string - lastExecArgv []string - lastExecEnvv []string - fakeroot string - snapdPath string - corePath string -} - -var _ = Suite(&cmdSuite{}) - -func (s *cmdSuite) SetUpTest(c *C) { - s.restoreExec = cmd.MockSyscallExec(s.syscallExec) - _, s.restoreLogger = logger.MockLogger() - s.execCalled = 0 - s.lastExecArgv0 = "" - s.lastExecArgv = nil - s.lastExecEnvv = nil - s.fakeroot = c.MkDir() - dirs.SetRootDir(s.fakeroot) - s.snapdPath = filepath.Join(dirs.SnapMountDir, "/snapd/42") - s.corePath = filepath.Join(dirs.SnapMountDir, "/core/21") - c.Assert(os.MkdirAll(filepath.Join(s.fakeroot, "proc/self"), 0755), IsNil) -} - -func (s *cmdSuite) TearDownTest(c *C) { - s.restoreExec() - s.restoreLogger() -} - -func (s *cmdSuite) syscallExec(argv0 string, argv []string, envv []string) (err error) { - s.execCalled++ - s.lastExecArgv0 = argv0 - s.lastExecArgv = argv - s.lastExecEnvv = envv - return fmt.Errorf(">exec of %q in tests<", argv0) -} - -func (s *cmdSuite) fakeCoreVersion(c *C, coreDir, version string) { - p := filepath.Join(coreDir, "/usr/lib/snapd") - c.Assert(os.MkdirAll(p, 0755), IsNil) - c.Assert(ioutil.WriteFile(filepath.Join(p, "info"), []byte("VERSION="+version), 0644), IsNil) -} - -func (s *cmdSuite) fakeInternalTool(c *C, coreDir, toolName string) string { - s.fakeCoreVersion(c, coreDir, "42") - p := filepath.Join(coreDir, "/usr/lib/snapd", toolName) - c.Assert(ioutil.WriteFile(p, nil, 0755), IsNil) - - return p -} - -func (s *cmdSuite) mockReExecingEnv() func() { - restore := []func(){ - release.MockOnClassic(true), - release.MockReleaseInfo(&release.OS{ID: "ubuntu"}), - cmd.MockCoreSnapdPaths(s.corePath, s.snapdPath), - cmd.MockVersion("2"), - } - - return func() { - for i := len(restore) - 1; i >= 0; i-- { - restore[i]() - } - } -} - -func (s *cmdSuite) mockReExecFor(c *C, coreDir, toolName string) func() { - selfExe := filepath.Join(s.fakeroot, "proc/self/exe") - restore := []func(){ - s.mockReExecingEnv(), - cmd.MockSelfExe(selfExe), - } - s.fakeInternalTool(c, coreDir, toolName) - c.Assert(os.Symlink(filepath.Join("/usr/lib/snapd", toolName), selfExe), IsNil) - - return func() { - for i := len(restore) - 1; i >= 0; i-- { - restore[i]() - } - } -} - -func (s *cmdSuite) TestDistroSupportsReExec(c *C) { - restore := release.MockOnClassic(true) - defer restore() - - // Some distributions don't support re-execution yet. - for _, id := range []string{"fedora", "centos", "rhel", "opensuse", "suse", "poky"} { - restore = release.MockReleaseInfo(&release.OS{ID: id}) - defer restore() - c.Check(cmd.DistroSupportsReExec(), Equals, false, Commentf("ID: %q", id)) - } - - // While others do. - for _, id := range []string{"debian", "ubuntu"} { - restore = release.MockReleaseInfo(&release.OS{ID: id}) - defer restore() - c.Check(cmd.DistroSupportsReExec(), Equals, true, Commentf("ID: %q", id)) - } -} - -func (s *cmdSuite) TestNonClassicDistroNoSupportsReExec(c *C) { - restore := release.MockOnClassic(false) - defer restore() - - // no distro supports re-exec when not on classic :-) - for _, id := range []string{ - "fedora", "centos", "rhel", "opensuse", "suse", "poky", - "debian", "ubuntu", "arch", "archlinux", - } { - restore = release.MockReleaseInfo(&release.OS{ID: id}) - defer restore() - c.Check(cmd.DistroSupportsReExec(), Equals, false, Commentf("ID: %q", id)) - } -} - -func (s *cmdSuite) TestCoreSupportsReExecNoInfo(c *C) { - // there's no snapd/info in a just-created tmpdir :-p - c.Check(cmd.CoreSupportsReExec(c.MkDir()), Equals, false) -} - -func (s *cmdSuite) TestCoreSupportsReExecBadInfo(c *C) { - // can't read snapd/info if it's a directory - p := s.snapdPath + "/usr/lib/snapd/info" - c.Assert(os.MkdirAll(p, 0755), IsNil) - - c.Check(cmd.CoreSupportsReExec(s.snapdPath), Equals, false) -} - -func (s *cmdSuite) TestCoreSupportsReExecBadInfoContent(c *C) { - // can't understand snapd/info if all it holds are potatoes - p := s.snapdPath + "/usr/lib/snapd" - c.Assert(os.MkdirAll(p, 0755), IsNil) - c.Assert(ioutil.WriteFile(p+"/info", []byte("potatoes"), 0644), IsNil) - - c.Check(cmd.CoreSupportsReExec(s.snapdPath), Equals, false) -} - -func (s *cmdSuite) TestCoreSupportsReExecBadVersion(c *C) { - // can't understand snapd/info if all its version is gibberish - s.fakeCoreVersion(c, s.snapdPath, "0:") - - c.Check(cmd.CoreSupportsReExec(s.snapdPath), Equals, false) -} - -func (s *cmdSuite) TestCoreSupportsReExecOldVersion(c *C) { - // can't re-exec if core version is too old - defer cmd.MockVersion("2")() - s.fakeCoreVersion(c, s.snapdPath, "0") - - c.Check(cmd.CoreSupportsReExec(s.snapdPath), Equals, false) -} - -func (s *cmdSuite) TestCoreSupportsReExec(c *C) { - defer cmd.MockVersion("2")() - s.fakeCoreVersion(c, s.snapdPath, "9999") - - c.Check(cmd.CoreSupportsReExec(s.snapdPath), Equals, true) -} - -func (s *cmdSuite) TestInternalToolPathNoReexec(c *C) { - restore := cmd.MockOsReadlink(func(string) (string, error) { - return filepath.Join(dirs.DistroLibExecDir, "snapd"), nil - }) - defer restore() - - path, err := cmd.InternalToolPath("potato") - c.Check(err, IsNil) - c.Check(path, Equals, filepath.Join(dirs.DistroLibExecDir, "potato")) -} - -func (s *cmdSuite) TestInternalToolPathWithReexec(c *C) { - s.fakeInternalTool(c, s.snapdPath, "potato") - restore := cmd.MockOsReadlink(func(string) (string, error) { - return filepath.Join(s.snapdPath, "/usr/lib/snapd/snapd"), nil - }) - defer restore() - - path, err := cmd.InternalToolPath("potato") - c.Check(err, IsNil) - c.Check(path, Equals, filepath.Join(dirs.SnapMountDir, "snapd/42/usr/lib/snapd/potato")) -} - -func (s *cmdSuite) TestInternalToolPathWithOtherLocation(c *C) { - s.fakeInternalTool(c, s.snapdPath, "potato") - restore := cmd.MockOsReadlink(func(string) (string, error) { - return filepath.Join("/tmp/tmp.foo_1234/usr/lib/snapd/snapd"), nil - }) - defer restore() - - path, err := cmd.InternalToolPath("potato") - c.Check(err, IsNil) - c.Check(path, Equals, "/tmp/tmp.foo_1234/usr/lib/snapd/potato") -} - -func (s *cmdSuite) TestInternalToolSnapPathWithOtherLocation(c *C) { - restore := cmd.MockOsReadlink(func(string) (string, error) { - return filepath.Join("/tmp/tmp.foo_1234/usr/bin/snap"), nil - }) - defer restore() - - path, err := cmd.InternalToolPath("potato") - c.Check(err, IsNil) - c.Check(path, Equals, "/tmp/tmp.foo_1234/usr/lib/snapd/potato") -} - -func (s *cmdSuite) TestInternalToolPathWithOtherCrazyLocation(c *C) { - restore := cmd.MockOsReadlink(func(string) (string, error) { - return filepath.Join("/usr/foo/usr/tmp/tmp.foo_1234/usr/bin/snap"), nil - }) - defer restore() - - path, err := cmd.InternalToolPath("potato") - c.Check(err, IsNil) - c.Check(path, Equals, "/usr/foo/usr/tmp/tmp.foo_1234/usr/lib/snapd/potato") -} - -func (s *cmdSuite) TestInternalToolPathWithDevLocationFallback(c *C) { - restore := cmd.MockOsReadlink(func(string) (string, error) { - return filepath.Join("/home/dev/snapd/snapd"), nil - }) - defer restore() - - path, err := cmd.InternalToolPath("potato") - c.Check(err, IsNil) - c.Check(path, Equals, filepath.Join(dirs.DistroLibExecDir, "potato")) -} - -func (s *cmdSuite) TestInternalToolPathWithOtherDevLocationWhenExecutable(c *C) { - restore := cmd.MockOsReadlink(func(string) (string, error) { - return filepath.Join(dirs.GlobalRootDir, "/tmp/snapd"), nil - }) - defer restore() - - devTool := filepath.Join(dirs.GlobalRootDir, "/tmp/potato") - err := os.MkdirAll(filepath.Dir(devTool), 0755) - c.Assert(err, IsNil) - err = ioutil.WriteFile(devTool, []byte(""), 0755) - c.Assert(err, IsNil) - - path, err := cmd.InternalToolPath("potato") - c.Check(err, IsNil) - c.Check(path, Equals, filepath.Join(dirs.GlobalRootDir, "/tmp/potato")) -} - -func (s *cmdSuite) TestInternalToolPathWithOtherDevLocationNonExecutable(c *C) { - restore := cmd.MockOsReadlink(func(string) (string, error) { - return filepath.Join(dirs.GlobalRootDir, "/tmp/snapd"), nil - }) - defer restore() - - devTool := filepath.Join(dirs.GlobalRootDir, "/tmp/non-executable-potato") - err := os.MkdirAll(filepath.Dir(devTool), 0755) - c.Assert(err, IsNil) - err = ioutil.WriteFile(devTool, []byte(""), 0644) - c.Assert(err, IsNil) - - path, err := cmd.InternalToolPath("non-executable-potato") - c.Check(err, IsNil) - c.Check(path, Equals, filepath.Join(dirs.DistroLibExecDir, "non-executable-potato")) -} - -func (s *cmdSuite) TestInternalToolPathSnapdPathReexec(c *C) { - restore := cmd.MockOsReadlink(func(string) (string, error) { - return filepath.Join(dirs.SnapMountDir, "core/111/usr/bin/snap"), nil - }) - defer restore() - - p, err := cmd.InternalToolPath("snapd") - c.Assert(err, IsNil) - c.Check(p, Equals, filepath.Join(dirs.SnapMountDir, "/core/111/usr/lib/snapd/snapd")) -} - -func (s *cmdSuite) TestInternalToolPathSnapdSnap(c *C) { - restore := cmd.MockOsReadlink(func(string) (string, error) { - return filepath.Join(dirs.SnapMountDir, "snapd/22/usr/bin/snap"), nil - }) - defer restore() - p, err := cmd.InternalToolPath("snapd") - c.Assert(err, IsNil) - c.Check(p, Equals, filepath.Join(dirs.SnapMountDir, "/snapd/22/usr/lib/snapd/snapd")) -} - -func (s *cmdSuite) TestInternalToolPathWithLibexecdirLocation(c *C) { - defer dirs.SetRootDir(s.fakeroot) - restore := release.MockReleaseInfo(&release.OS{ID: "fedora"}) - defer restore() - // reload directory paths - dirs.SetRootDir("/") - - restore = cmd.MockOsReadlink(func(string) (string, error) { - return filepath.Join("/usr/bin/snap"), nil - }) - defer restore() - - path, err := cmd.InternalToolPath("potato") - c.Check(err, IsNil) - c.Check(path, Equals, filepath.Join("/usr/libexec/snapd/potato")) -} - -func (s *cmdSuite) TestExecInSnapdOrCoreSnap(c *C) { - defer s.mockReExecFor(c, s.snapdPath, "potato")() - - c.Check(cmd.ExecInSnapdOrCoreSnap, PanicMatches, `>exec of "[^"]+/potato" in tests<`) - c.Check(s.execCalled, Equals, 1) - c.Check(s.lastExecArgv0, Equals, filepath.Join(s.snapdPath, "/usr/lib/snapd/potato")) - c.Check(s.lastExecArgv, DeepEquals, os.Args) -} - -func (s *cmdSuite) TestExecInOldCoreSnap(c *C) { - defer s.mockReExecFor(c, s.corePath, "potato")() - - c.Check(cmd.ExecInSnapdOrCoreSnap, PanicMatches, `>exec of "[^"]+/potato" in tests<`) - c.Check(s.execCalled, Equals, 1) - c.Check(s.lastExecArgv0, Equals, filepath.Join(s.corePath, "/usr/lib/snapd/potato")) - c.Check(s.lastExecArgv, DeepEquals, os.Args) -} - -func (s *cmdSuite) TestExecInSnapdOrCoreSnapBailsNoCoreSupport(c *C) { - defer s.mockReExecFor(c, s.snapdPath, "potato")() - - // no "info" -> no core support: - c.Assert(os.Remove(filepath.Join(s.snapdPath, "/usr/lib/snapd/info")), IsNil) - - cmd.ExecInSnapdOrCoreSnap() - c.Check(s.execCalled, Equals, 0) -} - -func (s *cmdSuite) TestExecInSnapdOrCoreSnapMissingExe(c *C) { - defer s.mockReExecFor(c, s.snapdPath, "potato")() - - // missing exe: - c.Assert(os.Remove(filepath.Join(s.snapdPath, "/usr/lib/snapd/potato")), IsNil) - - cmd.ExecInSnapdOrCoreSnap() - c.Check(s.execCalled, Equals, 0) -} - -func (s *cmdSuite) TestExecInSnapdOrCoreSnapBadSelfExe(c *C) { - defer s.mockReExecFor(c, s.snapdPath, "potato")() - - // missing self/exe: - c.Assert(os.Remove(filepath.Join(s.fakeroot, "proc/self/exe")), IsNil) - - cmd.ExecInSnapdOrCoreSnap() - c.Check(s.execCalled, Equals, 0) -} - -func (s *cmdSuite) TestExecInSnapdOrCoreSnapBailsNoDistroSupport(c *C) { - defer s.mockReExecFor(c, s.snapdPath, "potato")() - - // no distro support: - defer release.MockOnClassic(false)() - - cmd.ExecInSnapdOrCoreSnap() - c.Check(s.execCalled, Equals, 0) -} - -func (s *cmdSuite) TestExecInSnapdOrCoreSnapNoDouble(c *C) { - selfExe := filepath.Join(s.fakeroot, "proc/self/exe") - err := os.Symlink(filepath.Join(s.fakeroot, "/snap/core/42/usr/lib/snapd"), selfExe) - c.Assert(err, IsNil) - cmd.MockSelfExe(selfExe) - - cmd.ExecInSnapdOrCoreSnap() - c.Check(s.execCalled, Equals, 0) -} - -func (s *cmdSuite) TestExecInSnapdOrCoreSnapDisabled(c *C) { - defer s.mockReExecFor(c, s.snapdPath, "potato")() - - os.Setenv("SNAP_REEXEC", "0") - defer os.Unsetenv("SNAP_REEXEC") - - cmd.ExecInSnapdOrCoreSnap() - c.Check(s.execCalled, Equals, 0) -} diff -Nru snapd-2.45.1+20.04.2/cmd/cmdutil/cmdutil.go snapd-2.48.3+20.04/cmd/cmdutil/cmdutil.go --- snapd-2.45.1+20.04.2/cmd/cmdutil/cmdutil.go 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/cmd/cmdutil/cmdutil.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,140 +0,0 @@ -// -*- Mode: Go; indent-tabs-mode: t -*- - -/* - * Copyright (C) 2019 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 cmdutil - -import ( - "bufio" - "bytes" - "debug/elf" - "fmt" - "io/ioutil" - "os" - "os/exec" - "path/filepath" - "strings" - - "github.com/snapcore/snapd/dirs" - "github.com/snapcore/snapd/osutil" -) - -func elfInterp(cmd string) (string, error) { - el, err := elf.Open(cmd) - if err != nil { - return "", err - } - defer el.Close() - - for _, prog := range el.Progs { - if prog.Type == elf.PT_INTERP { - r := prog.Open() - interp, err := ioutil.ReadAll(r) - if err != nil { - return "", nil - } - - return string(bytes.Trim(interp, "\x00")), nil - } - } - - return "", fmt.Errorf("cannot find PT_INTERP header") -} - -func parseLdSoConf(root string, confPath string) []string { - f, err := os.Open(filepath.Join(root, confPath)) - if err != nil { - return nil - } - defer f.Close() - - var out []string - scanner := bufio.NewScanner(f) - for scanner.Scan() { - line := scanner.Text() - switch { - case strings.HasPrefix(line, "#"): - // nothing - case strings.TrimSpace(line) == "": - // nothing - case strings.HasPrefix(line, "include "): - l := strings.SplitN(line, "include ", 2) - files, err := filepath.Glob(filepath.Join(root, l[1])) - if err != nil { - return nil - } - for _, f := range files { - out = append(out, parseLdSoConf(root, f[len(root):])...) - } - default: - out = append(out, filepath.Join(root, line)) - } - - } - if err := scanner.Err(); err != nil { - return nil - } - - return out -} - -// CommandFromSystemSnap runs a command from the snapd/core snap -// using the proper interpreter and library paths. -// -// At the moment it can only run ELF files, expects a standard ld.so -// interpreter, and can't handle RPATH. -func CommandFromSystemSnap(name string, cmdArgs ...string) (*exec.Cmd, error) { - from := "snapd" - root := filepath.Join(dirs.SnapMountDir, "/snapd/current") - if !osutil.FileExists(root) { - from = "core" - root = filepath.Join(dirs.SnapMountDir, "/core/current") - } - - cmdPath := filepath.Join(root, name) - interp, err := elfInterp(cmdPath) - if err != nil { - return nil, err - } - coreLdSo := filepath.Join(root, interp) - // we cannot use EvalSymlink here because we need to resolve - // relative and an absolute symlinks differently. A absolute - // symlink is relative to root of the snapd/core snap. - seen := map[string]bool{} - for osutil.IsSymlink(coreLdSo) { - link, err := os.Readlink(coreLdSo) - if err != nil { - return nil, err - } - if filepath.IsAbs(link) { - coreLdSo = filepath.Join(root, link) - } else { - coreLdSo = filepath.Join(filepath.Dir(coreLdSo), link) - } - if seen[coreLdSo] { - return nil, fmt.Errorf("cannot run command from %s: symlink cycle found", from) - } - seen[coreLdSo] = true - } - - ldLibraryPathForCore := parseLdSoConf(root, "/etc/ld.so.conf") - - ldSoArgs := []string{"--library-path", strings.Join(ldLibraryPathForCore, ":"), cmdPath} - allArgs := append(ldSoArgs, cmdArgs...) - return exec.Command(coreLdSo, allArgs...), nil -} diff -Nru snapd-2.45.1+20.04.2/cmd/cmdutil/cmdutil_test.go snapd-2.48.3+20.04/cmd/cmdutil/cmdutil_test.go --- snapd-2.45.1+20.04.2/cmd/cmdutil/cmdutil_test.go 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/cmd/cmdutil/cmdutil_test.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,118 +0,0 @@ -// -*- Mode: Go; indent-tabs-mode: t -*- - -/* - * Copyright (C) 2016 Canonical Ltd - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 3 as - * published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - */ - -package cmdutil_test - -import ( - "fmt" - "io/ioutil" - "os" - "os/exec" - "path/filepath" - "strings" - "testing" - - . "gopkg.in/check.v1" - - "github.com/snapcore/snapd/cmd/cmdutil" - "github.com/snapcore/snapd/dirs" - "github.com/snapcore/snapd/osutil" -) - -// Hook up check.v1 into the "go test" runner -func Test(t *testing.T) { TestingT(t) } - -var truePath = osutil.LookPathDefault("true", "/bin/true") - -type cmdutilSuite struct{} - -var _ = Suite(&cmdutilSuite{}) - -func (s *cmdutilSuite) SetUpTest(c *C) { - dirs.SetRootDir(c.MkDir()) -} - -func (s *cmdutilSuite) TearDownTest(c *C) { - dirs.SetRootDir("") -} - -func (s *cmdutilSuite) makeMockLdSoConf(c *C, root string) { - ldSoConf := filepath.Join(root, "/etc/ld.so.conf") - ldSoConfD := ldSoConf + ".d" - - err := os.MkdirAll(filepath.Dir(ldSoConf), 0755) - c.Assert(err, IsNil) - err = os.MkdirAll(ldSoConfD, 0755) - c.Assert(err, IsNil) - - err = ioutil.WriteFile(ldSoConf, []byte("include /etc/ld.so.conf.d/*.conf"), 0644) - c.Assert(err, IsNil) - - ldSoConf1 := filepath.Join(ldSoConfD, "x86_64-linux-gnu.conf") - - err = ioutil.WriteFile(ldSoConf1, []byte(` -# Multiarch support -/lib/x86_64-linux-gnu -/usr/lib/x86_64-linux-gnu`), 0644) - c.Assert(err, IsNil) -} - -func (s *cmdutilSuite) TestCommandFromSystemSnap(c *C) { - for _, snap := range []string{"core", "snapd"} { - - root := filepath.Join(dirs.SnapMountDir, snap, "current") - s.makeMockLdSoConf(c, root) - - os.MkdirAll(filepath.Join(root, "/usr/bin"), 0755) - osutil.CopyFile(truePath, filepath.Join(root, "/usr/bin/xdelta3"), 0) - cmd, err := cmdutil.CommandFromSystemSnap("/usr/bin/xdelta3", "--some-xdelta-arg") - c.Assert(err, IsNil) - - out, err := exec.Command("/bin/sh", "-c", fmt.Sprintf("readelf -l %s |grep interpreter:|cut -f2 -d:|cut -f1 -d]", truePath)).Output() - c.Assert(err, IsNil) - interp := strings.TrimSpace(string(out)) - - c.Check(cmd.Args, DeepEquals, []string{ - filepath.Join(root, interp), - "--library-path", - fmt.Sprintf("%s/lib/x86_64-linux-gnu:%s/usr/lib/x86_64-linux-gnu", root, root), - filepath.Join(root, "/usr/bin/xdelta3"), - "--some-xdelta-arg", - }) - } -} - -func (s *cmdutilSuite) TestCommandFromCoreSymlinkCycle(c *C) { - root := filepath.Join(dirs.SnapMountDir, "/core/current") - s.makeMockLdSoConf(c, root) - - os.MkdirAll(filepath.Join(root, "/usr/bin"), 0755) - osutil.CopyFile(truePath, filepath.Join(root, "/usr/bin/xdelta3"), 0) - - out, err := exec.Command("/bin/sh", "-c", "readelf -l /bin/true |grep interpreter:|cut -f2 -d:|cut -f1 -d]").Output() - c.Assert(err, IsNil) - interp := strings.TrimSpace(string(out)) - - coreInterp := filepath.Join(root, interp) - c.Assert(os.MkdirAll(filepath.Dir(coreInterp), 0755), IsNil) - c.Assert(os.Symlink(filepath.Base(coreInterp), coreInterp), IsNil) - - _, err = cmdutil.CommandFromSystemSnap("/usr/bin/xdelta3", "--some-xdelta-arg") - c.Assert(err, ErrorMatches, "cannot run command from core: symlink cycle found") -} diff -Nru snapd-2.45.1+20.04.2/cmd/cmdutil/version.go snapd-2.48.3+20.04/cmd/cmdutil/version.go --- snapd-2.45.1+20.04.2/cmd/cmdutil/version.go 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/cmd/cmdutil/version.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,53 +0,0 @@ -// -*- Mode: Go; indent-tabs-mode: t -*- - -/* - * Copyright (C) 2019 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 cmdutil - -import ( - "bytes" - "fmt" - "io/ioutil" -) - -// SnapdVersionFromInfoFile returns snapd version read for the -// given info" file, pointed by infoPath. -// The format of the "info" file is a single line with "VERSION=..." -// in it. The file is produced by mkversion.sh and normally installed -// along snapd binary in /usr/lib/snapd. -func SnapdVersionFromInfoFile(infoPath string) (string, error) { - content, err := ioutil.ReadFile(infoPath) - if err != nil { - return "", fmt.Errorf("cannot open snapd info file %q: %s", infoPath, err) - } - - if !bytes.HasPrefix(content, []byte("VERSION=")) { - idx := bytes.Index(content, []byte("\nVERSION=")) - if idx < 0 { - return "", fmt.Errorf("cannot find snapd version information in %q", content) - } - content = content[idx+1:] - } - content = content[8:] - idx := bytes.IndexByte(content, '\n') - if idx > -1 { - content = content[:idx] - } - - return string(content), nil -} diff -Nru snapd-2.45.1+20.04.2/cmd/cmdutil/version_test.go snapd-2.48.3+20.04/cmd/cmdutil/version_test.go --- snapd-2.45.1+20.04.2/cmd/cmdutil/version_test.go 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/cmd/cmdutil/version_test.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,57 +0,0 @@ -// -*- Mode: Go; indent-tabs-mode: t -*- - -/* - * Copyright (C) 2019 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 cmdutil_test - -import ( - "io/ioutil" - "path/filepath" - - . "gopkg.in/check.v1" - - "github.com/snapcore/snapd/cmd/cmdutil" -) - -type versionSuite struct{} - -var _ = Suite(&versionSuite{}) - -func (s *versionSuite) TestNoVersionFile(c *C) { - _, err := cmdutil.SnapdVersionFromInfoFile("/non-existing-file") - c.Assert(err, ErrorMatches, `cannot open snapd info file "/non-existing-file":.*`) -} - -func (s *versionSuite) TestNoVersionData(c *C) { - top := c.MkDir() - infoFile := filepath.Join(top, "info") - c.Assert(ioutil.WriteFile(infoFile, []byte("foo"), 0644), IsNil) - - _, err := cmdutil.SnapdVersionFromInfoFile(infoFile) - c.Assert(err, ErrorMatches, `cannot find snapd version information in "foo"`) -} - -func (s *versionSuite) TestVersionHappy(c *C) { - top := c.MkDir() - infoFile := filepath.Join(top, "info") - c.Assert(ioutil.WriteFile(infoFile, []byte("VERSION=1.2.3"), 0644), IsNil) - - ver, err := cmdutil.SnapdVersionFromInfoFile(infoFile) - c.Assert(err, IsNil) - c.Check(ver, Equals, "1.2.3") -} diff -Nru snapd-2.45.1+20.04.2/cmd/export_test.go snapd-2.48.3+20.04/cmd/export_test.go --- snapd-2.45.1+20.04.2/cmd/export_test.go 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/cmd/export_test.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,52 +0,0 @@ -// -*- 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 cmd - -var ( - DistroSupportsReExec = distroSupportsReExec - CoreSupportsReExec = coreSupportsReExec -) - -func MockCoreSnapdPaths(newCoreSnap, newSnapdSnap string) func() { - oldOldCore := coreSnap - oldNewCore := snapdSnap - snapdSnap = newSnapdSnap - coreSnap = newCoreSnap - return func() { - snapdSnap = oldNewCore - coreSnap = oldOldCore - } -} - -func MockSelfExe(newSelfExe string) func() { - oldSelfExe := selfExe - selfExe = newSelfExe - return func() { - selfExe = oldSelfExe - } -} - -func MockSyscallExec(f func(argv0 string, argv []string, envv []string) (err error)) func() { - oldSyscallExec := syscallExec - syscallExec = f - return func() { - syscallExec = oldSyscallExec - } -} diff -Nru snapd-2.45.1+20.04.2/cmd/libsnap-confine-private/cgroup-pids-support.c snapd-2.48.3+20.04/cmd/libsnap-confine-private/cgroup-pids-support.c --- snapd-2.45.1+20.04.2/cmd/libsnap-confine-private/cgroup-pids-support.c 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/cmd/libsnap-confine-private/cgroup-pids-support.c 1970-01-01 00:00:00.000000000 +0000 @@ -1,26 +0,0 @@ -/* - * Copyright (C) 2019 Canonical Ltd - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 3 as - * published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - */ - -#include "cgroup-pids-support.h" - -#include "cgroup-support.h" - -static const char *pids_cgroup_dir = "/sys/fs/cgroup/pids"; - -void sc_cgroup_pids_join(const char *snap_security_tag, pid_t pid) { - sc_cgroup_create_and_join(pids_cgroup_dir, snap_security_tag, pid); -} diff -Nru snapd-2.45.1+20.04.2/cmd/libsnap-confine-private/cgroup-pids-support.h snapd-2.48.3+20.04/cmd/libsnap-confine-private/cgroup-pids-support.h --- snapd-2.45.1+20.04.2/cmd/libsnap-confine-private/cgroup-pids-support.h 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/cmd/libsnap-confine-private/cgroup-pids-support.h 1970-01-01 00:00:00.000000000 +0000 @@ -1,43 +0,0 @@ -/* - * Copyright (C) 2019 Canonical Ltd - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 3 as - * published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - */ - -#ifndef SC_CGROUP_PIDS_SUPPORT_H -#define SC_CGROUP_PIDS_SUPPORT_H - -#include - -/** - * Join the pid cgroup for the given snap application. - * - * This function adds the specified task to the pid cgroup specific to the - * given snap. The name of the cgroup is "snap.$snap_name.$app_name" for apps - * or "snap.$snap_name.hook.$hook_name" for hooks. - * - * The "tasks" file belonging to the cgroup contains the set of all the - * threads that originate from the given snap app or hook. Examining that - * file one can reliably determine if the set is empty or not. - * - * Similarly the "cgroup.procs" file belonging to the same directory contains - * the set of all the processes that originate from the given snap app or - * hook. - * - * For more details please review: - * https://www.kernel.org/doc/Documentation/cgroup-v1/pids.txt - **/ -void sc_cgroup_pids_join(const char *snap_security_tag, pid_t pid); - -#endif diff -Nru snapd-2.45.1+20.04.2/cmd/libsnap-confine-private/feature.c snapd-2.48.3+20.04/cmd/libsnap-confine-private/feature.c --- snapd-2.45.1+20.04.2/cmd/libsnap-confine-private/feature.c 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/cmd/libsnap-confine-private/feature.c 2021-02-02 08:21:12.000000000 +0000 @@ -43,6 +43,9 @@ case SC_FEATURE_PARALLEL_INSTANCES: file_name = "parallel-instances"; break; + case SC_FEATURE_HIDDEN_SNAP_FOLDER: + file_name = "hidden-snap-folder"; + break; default: die("unknown feature flag code %d", flag); } diff -Nru snapd-2.45.1+20.04.2/cmd/libsnap-confine-private/feature.h snapd-2.48.3+20.04/cmd/libsnap-confine-private/feature.h --- snapd-2.45.1+20.04.2/cmd/libsnap-confine-private/feature.h 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/cmd/libsnap-confine-private/feature.h 2021-02-02 08:21:12.000000000 +0000 @@ -24,6 +24,7 @@ SC_FEATURE_PER_USER_MOUNT_NAMESPACE = 1 << 0, SC_FEATURE_REFRESH_APP_AWARENESS = 1 << 1, SC_FEATURE_PARALLEL_INSTANCES = 1 << 2, + SC_FEATURE_HIDDEN_SNAP_FOLDER = 1 << 3, } sc_feature_flag; /** diff -Nru snapd-2.45.1+20.04.2/cmd/libsnap-confine-private/feature-test.c snapd-2.48.3+20.04/cmd/libsnap-confine-private/feature-test.c --- snapd-2.45.1+20.04.2/cmd/libsnap-confine-private/feature-test.c 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/cmd/libsnap-confine-private/feature-test.c 2021-02-02 08:21:12.000000000 +0000 @@ -89,6 +89,20 @@ g_assert(sc_feature_enabled(SC_FEATURE_PARALLEL_INSTANCES)); } +static void test_feature_hidden_snap_folder(void) +{ + const char *d = sc_testdir(); + sc_mock_feature_flag_dir(d); + + g_assert(!sc_feature_enabled(SC_FEATURE_HIDDEN_SNAP_FOLDER)); + + char pname[PATH_MAX]; + sc_must_snprintf(pname, sizeof pname, "%s/hidden-snap-folder", d); + g_file_set_contents(pname, "", -1, NULL); + + g_assert(sc_feature_enabled(SC_FEATURE_HIDDEN_SNAP_FOLDER)); +} + static void __attribute__((constructor)) init(void) { g_test_add_func("/feature/missing_dir", @@ -99,4 +113,6 @@ test_feature_enabled__present_file); g_test_add_func("/feature/parallel_instances", test_feature_parallel_instances); + g_test_add_func("/feature/hidden_snap_folder", + test_feature_hidden_snap_folder); } diff -Nru snapd-2.45.1+20.04.2/cmd/Makefile.am snapd-2.48.3+20.04/cmd/Makefile.am --- snapd-2.45.1+20.04.2/cmd/Makefile.am 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/cmd/Makefile.am 2021-02-02 08:21:12.000000000 +0000 @@ -55,8 +55,6 @@ endif new_format = \ - libsnap-confine-private/cgroup-pids-support.c \ - libsnap-confine-private/cgroup-pids-support.h \ libsnap-confine-private/cgroup-support.c \ libsnap-confine-private/cgroup-support.h \ libsnap-confine-private/infofile-test.c \ @@ -72,7 +70,9 @@ snap-confine/snap-confine-invocation-test.c \ snap-confine/snap-confine-invocation.c \ snap-confine/snap-confine-invocation.h \ - snap-discard-ns/snap-discard-ns.c + snap-discard-ns/snap-discard-ns.c \ + snap-gdb-shim/snap-gdb-shim.c \ + snap-gdb-shim/snap-gdbserver-shim.c # NOTE: clang-format is using project-wide .clang-format file. .PHONY: fmt @@ -112,8 +112,6 @@ libsnap-confine-private/apparmor-support.h \ libsnap-confine-private/cgroup-freezer-support.c \ libsnap-confine-private/cgroup-freezer-support.h \ - libsnap-confine-private/cgroup-pids-support.c \ - libsnap-confine-private/cgroup-pids-support.h \ libsnap-confine-private/cgroup-support.c \ libsnap-confine-private/cgroup-support.h \ libsnap-confine-private/classic.c \ @@ -491,6 +489,17 @@ snap_gdb_shim_snap_gdb_shim_LDADD = libsnap-confine-private.a ## +## snap-gdbserver-shim +## + +libexec_PROGRAMS += snap-gdb-shim/snap-gdbserver-shim + +snap_gdb_shim_snap_gdbserver_shim_SOURCES = \ + snap-gdb-shim/snap-gdbserver-shim.c + +snap_gdb_shim_snap_gdbserver_shim_LDADD = libsnap-confine-private.a + +## ## snapd-generator ## diff -Nru snapd-2.45.1+20.04.2/cmd/snap/cmd_auto_import.go snapd-2.48.3+20.04/cmd/snap/cmd_auto_import.go --- snapd-2.45.1+20.04.2/cmd/snap/cmd_auto_import.go 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/cmd/snap/cmd_auto_import.go 2021-02-02 08:21:12.000000000 +0000 @@ -61,6 +61,9 @@ isTesting := snapdenv.Testing() + // TODO: re-write this to use osutil.LoadMountInfo instead of doing the + // parsing ourselves + scanner := bufio.NewScanner(f) for scanner.Scan() { l := strings.Fields(scanner.Text()) @@ -94,7 +97,24 @@ continue } + // TODO: should the following 2 checks try to be more smart like + // `snap-bootstrap initramfs-mounts` and try to find the boot disk + // and determine what partitions to skip using the disks package? + + // skip all initramfs mounted disks on uc20 mountPoint := l[4] + if strings.HasPrefix(mountPoint, boot.InitramfsRunMntDir) { + continue + } + + // skip all seed dir mount points too, as these are bind mounts to the + // initramfs dirs on uc20, this can show up as + // /writable/system-data/var/lib/snapd/seed as well as + // /var/lib/snapd/seed + if strings.HasSuffix(mountPoint, dirs.SnapSeedDir) { + continue + } + cand := filepath.Join(mountPoint, autoImportsName) if osutil.FileExists(cand) { cands = append(cands, cand) @@ -102,7 +122,6 @@ } return cands, scanner.Err() - } func queueFile(src string) error { @@ -260,12 +279,15 @@ } func (x *cmdAutoImport) autoAddUsers() error { - cmd := cmdCreateUser{ - clientMixin: x.clientMixin, - Known: true, - Sudoer: true, + options := client.CreateUserOptions{ + Automatic: true, } - return cmd.Execute(nil) + results, err := x.client.CreateUsers([]*client.CreateUserOptions{&options}) + for _, result := range results { + fmt.Fprintf(Stdout, i18n.G("created user %q\n"), result.Username) + } + + return err } func removableBlockDevices() (removableDevices []string) { diff -Nru snapd-2.45.1+20.04.2/cmd/snap/cmd_auto_import_test.go snapd-2.48.3+20.04/cmd/snap/cmd_auto_import_test.go --- snapd-2.45.1+20.04.2/cmd/snap/cmd_auto_import_test.go 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/cmd/snap/cmd_auto_import_test.go 2021-02-02 08:21:12.000000000 +0000 @@ -28,7 +28,6 @@ . "gopkg.in/check.v1" - "github.com/snapcore/snapd/boot" snap "github.com/snapcore/snapd/cmd/snap" "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/logger" @@ -68,7 +67,7 @@ c.Check(r.URL.Path, Equals, "/v2/users") postData, err := ioutil.ReadAll(r.Body) c.Assert(err, IsNil) - c.Check(string(postData), Equals, `{"action":"create","sudoer":true,"known":true}`) + c.Check(string(postData), Equals, `{"action":"create","automatic":true}`) fmt.Fprintln(w, `{"type": "sync", "result": [{"username": "foo"}]}`) n++ @@ -245,7 +244,7 @@ c.Check(r.URL.Path, Equals, "/v2/users") postData, err := ioutil.ReadAll(r.Body) c.Assert(err, IsNil) - c.Check(string(postData), Equals, `{"action":"create","sudoer":true,"known":true}`) + c.Check(string(postData), Equals, `{"action":"create","automatic":true}`) fmt.Fprintln(w, `{"type": "sync", "result": [{"username": "foo"}]}`) n++ @@ -315,7 +314,7 @@ err := ioutil.WriteFile(mockProcCmdlinePath, []byte("foo=bar snapd_recovery_mode=install snapd_recovery_system=20191118"), 0644) c.Assert(err, IsNil) - restore = boot.MockProcCmdline(mockProcCmdlinePath) + restore = osutil.MockProcCmdline(mockProcCmdlinePath) defer restore() _, err = snap.Parser(snap.Client()).ParseArgs([]string{"auto-import"}) @@ -463,3 +462,98 @@ filepath.Join(rootdir, "/tmp/snapd-auto-import-mount-1"), }) } + +func (s *SnapSuite) TestAutoImportUC20CandidatesIgnoresSystemPartitions(c *C) { + + mountDirs := []string{ + "/writable/system-data/var/lib/snapd/seed", + "/var/lib/snapd/seed", + "/run/mnt/ubuntu-boot", + "/run/mnt/ubuntu-seed", + "/run/mnt/ubuntu-data", + "/mnt/real-device", + } + + rootDir := c.MkDir() + dirs.SetRootDir(rootDir) + defer func() { dirs.SetRootDir("") }() + + args := make([]interface{}, 0, len(mountDirs)+1) + args = append(args, dirs.GlobalRootDir) + // pretend there are auto-import.asserts on all of them + for _, dir := range mountDirs { + args = append(args, dir) + file := filepath.Join(rootDir, dir, "auto-import.assert") + c.Assert(os.MkdirAll(filepath.Dir(file), 0755), IsNil) + c.Assert(ioutil.WriteFile(file, nil, 0644), IsNil) + } + + mockMountInfoFmtWithLoop := ` +24 0 8:18 / %[1]s%[2]s rw,relatime foo ext3 /dev/meep2 no,separator +24 0 8:18 / %[1]s%[3]s rw,relatime - ext3 /dev/meep2 rw,errors=remount-ro,data=ordered +24 0 8:18 / %[1]s%[4]s rw,relatime opt:1 - ext4 /dev/meep3 rw,errors=remount-ro,data=ordered +24 0 8:18 / %[1]s%[5]s rw,relatime opt:1 opt:2 - ext2 /dev/meep4 rw,errors=remount-ro,data=ordered +24 0 8:18 / %[1]s%[6]s rw,relatime opt:1 opt:2 - ext2 /dev/meep5 rw,errors=remount-ro,data=ordered +24 0 8:18 / %[1]s%[7]s rw,relatime opt:1 opt:2 - ext2 /dev/meep78 rw,errors=remount-ro,data=ordered +` + + content := fmt.Sprintf(mockMountInfoFmtWithLoop, args...) + restore := snap.MockMountInfoPath(makeMockMountInfo(c, content)) + defer restore() + + l, err := snap.AutoImportCandidates() + c.Check(err, IsNil) + + // only device should be the /mnt/real-device one + c.Check(l, DeepEquals, []string{filepath.Join(rootDir, "/mnt/real-device", "auto-import.assert")}) +} + +func (s *SnapSuite) TestAutoImportAssertsManagedEmptyReply(c *C) { + restore := release.MockOnClassic(false) + defer restore() + + fakeAssertData := []byte("my-assertion") + + n := 0 + total := 2 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch n { + case 0: + c.Check(r.Method, Equals, "POST") + c.Check(r.URL.Path, Equals, "/v2/assertions") + postData, err := ioutil.ReadAll(r.Body) + c.Assert(err, IsNil) + c.Check(postData, DeepEquals, fakeAssertData) + fmt.Fprintln(w, `{"type": "sync", "result": {"ready": true, "status": "Done"}}`) + n++ + case 1: + c.Check(r.Method, Equals, "POST") + c.Check(r.URL.Path, Equals, "/v2/users") + postData, err := ioutil.ReadAll(r.Body) + c.Assert(err, IsNil) + c.Check(string(postData), Equals, `{"action":"create","automatic":true}`) + + fmt.Fprintln(w, `{"type": "sync", "result": []}`) + n++ + default: + c.Fatalf("unexpected request: %v (expected %d got %d)", r, total, n) + } + + }) + + fakeAssertsFn := filepath.Join(c.MkDir(), "auto-import.assert") + err := ioutil.WriteFile(fakeAssertsFn, fakeAssertData, 0644) + c.Assert(err, IsNil) + + mockMountInfoFmt := ` +24 0 8:18 / %s rw,relatime shared:1 - ext4 /dev/sdb2 rw,errors=remount-ro,data=ordered` + content := fmt.Sprintf(mockMountInfoFmt, filepath.Dir(fakeAssertsFn)) + restore = snap.MockMountInfoPath(makeMockMountInfo(c, content)) + defer restore() + + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"auto-import"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + c.Check(s.Stdout(), Equals, ``) + c.Check(n, Equals, total) +} diff -Nru snapd-2.45.1+20.04.2/cmd/snap/cmd_create_cohort.go snapd-2.48.3+20.04/cmd/snap/cmd_create_cohort.go --- snapd-2.45.1+20.04.2/cmd/snap/cmd_create_cohort.go 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/cmd/snap/cmd_create_cohort.go 2021-02-02 08:21:12.000000000 +0000 @@ -26,7 +26,7 @@ "github.com/snapcore/snapd/i18n" ) -var shortCreateCohortHelp = i18n.G("Create cohort keys for a series of snaps") +var shortCreateCohortHelp = i18n.G("Create cohort keys for a set of snaps") var longCreateCohortHelp = i18n.G(` The create-cohort command creates a set of cohort keys for a given set of snaps. diff -Nru snapd-2.45.1+20.04.2/cmd/snap/cmd_debug_seeding.go snapd-2.48.3+20.04/cmd/snap/cmd_debug_seeding.go --- snapd-2.45.1+20.04.2/cmd/snap/cmd_debug_seeding.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48.3+20.04/cmd/snap/cmd_debug_seeding.go 2021-02-02 08:21:12.000000000 +0000 @@ -0,0 +1,163 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2020 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 ( + "bytes" + "encoding/json" + "fmt" + "time" + + "github.com/jessevdk/go-flags" + + "github.com/snapcore/snapd/interfaces" +) + +type cmdSeeding struct { + clientMixin + unicodeMixin +} + +func init() { + cmd := addDebugCommand("seeding", + "(internal) obtain seeding and preseeding details", + "(internal) obtain seeding and preseeding details", + func() flags.Commander { + return &cmdSeeding{} + }, nil, nil) + cmd.hidden = true +} + +func (x *cmdSeeding) Execute(args []string) error { + esc := x.getEscapes() + + if len(args) > 0 { + return ErrExtraArgs + } + var resp struct { + Seeded bool `json:"seeded,omitempty"` + Preseeded bool `json:"preseeded,omitempty"` + PreseedStartTime *time.Time `json:"preseed-start-time,omitempty"` + PreseedTime *time.Time `json:"preseed-time,omitempty"` + SeedStartTime *time.Time `json:"seed-start-time,omitempty"` + SeedRestartTime *time.Time `json:"seed-restart-time,omitempty"` + SeedTime *time.Time `json:"seed-time,omitempty"` + // use json.RawMessage to delay unmarshal'ing to the interfaces pkg + PreseedSystemKey *json.RawMessage `json:"preseed-system-key,omitempty"` + SeedRestartSystemKey *json.RawMessage `json:"seed-restart-system-key,omitempty"` + + SeedError string `json:"seed-error,omitempty"` + } + if err := x.client.DebugGet("seeding", &resp, nil); err != nil { + return err + } + + w := tabWriter() + + // show seeded and preseeded keys + fmt.Fprintf(w, "seeded:\t%v\n", resp.Seeded) + if resp.SeedError != "" { + // print seed-error + termWidth, _ := termSize() + termWidth -= 3 + if termWidth > 100 { + // any wider than this and it gets hard to read + termWidth = 100 + } + fmt.Fprintln(w, "seed-error: |") + // XXX: reuse/abuse + printDescr(w, resp.SeedError, termWidth) + } + + fmt.Fprintf(w, "preseeded:\t%v\n", resp.Preseeded) + + // calculate the time spent preseeding (if preseeded) and seeding + // for the preseeded case, we use the seed-restart-time as the start time + // to show how long we spent only after booting the preseeded image + + // if we are missing time values, we will default to showing "-" for the + // duration + seedDuration := esc.dash + if resp.Preseeded { + if resp.PreseedTime != nil && resp.PreseedStartTime != nil { + preseedDuration := resp.PreseedTime.Sub(*resp.PreseedStartTime).Round(time.Millisecond) + fmt.Fprintf(w, "image-preseeding:\t%v\n", preseedDuration) + } else { + fmt.Fprintf(w, "image-preseeding:\t%s\n", esc.dash) + } + + if resp.SeedTime != nil && resp.SeedRestartTime != nil { + seedDuration = fmt.Sprintf("%v", resp.SeedTime.Sub(*resp.SeedRestartTime).Round(time.Millisecond)) + } + } else if resp.SeedTime != nil && resp.SeedStartTime != nil { + seedDuration = fmt.Sprintf("%v", resp.SeedTime.Sub(*resp.SeedStartTime).Round(time.Millisecond)) + } + fmt.Fprintf(w, "seed-completion:\t%s\n", seedDuration) + + // we flush the tabwriter now because if we have more output, it will be + // the system keys, which are JSON and thus will never display cleanly in + // line with the other keys we did above + w.Flush() + + // only compare system-keys if preseeded and the system-keys exist + // they might not exist if this command is used on a system that was + // preseeded with an older version of snapd, i.e. while this feature is + // being rolled out, we may be preseeding images via old snapd deb, but with + // new snapd snap + if resp.Preseeded && resp.SeedRestartSystemKey != nil && resp.PreseedSystemKey != nil { + // only show them if they don't match, so first unmarshal them so we can + // properly compare them + + // we use raw json messages here so that the interfaces pkg can do the + // real unmarshalling to a real systemKey interface{} that can be + // compared with SystemKeysMatch, if we had instead unmarshalled here, + // we would have to remarshal the map[string]interface{} we got above + // and then pass those bytes back to the interfaces pkg which is awkward + seedSk, err := interfaces.UnmarshalJSONSystemKey(bytes.NewReader(*resp.SeedRestartSystemKey)) + if err != nil { + return err + } + + preseedSk, err := interfaces.UnmarshalJSONSystemKey(bytes.NewReader(*resp.PreseedSystemKey)) + if err != nil { + return err + } + + match, err := interfaces.SystemKeysMatch(preseedSk, seedSk) + if err != nil { + return err + } + if !match { + // mismatch, display the different keys + var preseedSkJSON, seedRestartSkJSON bytes.Buffer + json.Indent(&preseedSkJSON, *resp.PreseedSystemKey, "", " ") + fmt.Fprintf(Stdout, "preseed-system-key: ") + preseedSkJSON.WriteTo(Stdout) + fmt.Fprintln(Stdout, "") + + json.Indent(&seedRestartSkJSON, *resp.SeedRestartSystemKey, "", " ") + fmt.Fprintf(Stdout, "seed-restart-system-key: ") + seedRestartSkJSON.WriteTo(Stdout) + fmt.Fprintln(Stdout, "") + } + } + + return nil +} diff -Nru snapd-2.45.1+20.04.2/cmd/snap/cmd_debug_seeding_test.go snapd-2.48.3+20.04/cmd/snap/cmd_debug_seeding_test.go --- snapd-2.45.1+20.04.2/cmd/snap/cmd_debug_seeding_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48.3+20.04/cmd/snap/cmd_debug_seeding_test.go 2021-02-02 08:21:12.000000000 +0000 @@ -0,0 +1,493 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2020 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" + "io/ioutil" + "net/http" + + . "gopkg.in/check.v1" + + snap "github.com/snapcore/snapd/cmd/snap" +) + +var newPreseedNewSnapdSameSysKey = ` +{ + "result": { + "preseed-start-time": "2020-07-24T21:41:33.838194712Z", + "preseed-system-key": { + "apparmor-features": [ + "caps", + "dbus", + "domain", + "file", + "mount", + "namespaces", + "network", + "network_v8", + "policy", + "ptrace", + "query", + "rlimit", + "signal" + ], + "apparmor-parser-features": [ + "unsafe" + ], + "apparmor-parser-mtime": 1589907589, + "build-id": "cb94e5eeee4cf7ecda53f8308a984cb155b55732", + "cgroup-version": "1", + "nfs-home": false, + "overlay-root": "", + "seccomp-compiler-version": "e6e309ad8aee052e5aa695dfaa040328ae1559c5 2.4.3 9b218ef9a4e508dd8a7f848095cb8875d10a4bf28428ad81fdc3f8dac89108f7 bpf-actlog", + "seccomp-features": [ + "allow", + "errno", + "kill_process", + "kill_thread", + "log", + "trace", + "trap", + "user_notif" + ], + "version": 10 + }, + "preseed-time": "2020-07-24T21:41:43.156401424Z", + "preseeded": true, + "seed-restart-system-key": { + "apparmor-features": [ + "caps", + "dbus", + "domain", + "file", + "mount", + "namespaces", + "network", + "network_v8", + "policy", + "ptrace", + "query", + "rlimit", + "signal" + ], + "apparmor-parser-features": [ + "unsafe" + ], + "apparmor-parser-mtime": 1589907589, + "build-id": "cb94e5eeee4cf7ecda53f8308a984cb155b55732", + "cgroup-version": "1", + "nfs-home": false, + "overlay-root": "", + "seccomp-compiler-version": "e6e309ad8aee052e5aa695dfaa040328ae1559c5 2.4.3 9b218ef9a4e508dd8a7f848095cb8875d10a4bf28428ad81fdc3f8dac89108f7 bpf-actlog", + "seccomp-features": [ + "allow", + "errno", + "kill_process", + "kill_thread", + "log", + "trace", + "trap", + "user_notif" + ], + "version": 10 + }, + "seed-restart-time": "2020-07-24T21:42:16.646098923Z", + "seed-start-time": "0001-01-01T00:00:00Z", + "seed-time": "2020-07-24T21:42:20.518607Z", + "seeded": true + }, + "status": "OK", + "status-code": 200, + "type": "sync" +}` + +var newPreseedNewSnapdDiffSysKey = ` +{ + "result": { + "preseed-start-time": "2020-07-24T21:41:33.838194712Z", + "preseed-system-key": { + "apparmor-features": [ + "caps", + "dbus", + "domain", + "file", + "mount", + "namespaces", + "network", + "network_v8", + "policy", + "ptrace", + "query", + "rlimit", + "signal" + ], + "apparmor-parser-features": [ + "unsafe" + ], + "apparmor-parser-mtime": 1589907589, + "build-id": "cb94e5eeee4cf7ecda53f8308a984cb155b55732", + "cgroup-version": "1", + "nfs-home": false, + "overlay-root": "", + "seccomp-compiler-version": "e6e309ad8aee052e5aa695dfaa040328ae1559c5 2.4.3 9b218ef9a4e508dd8a7f848095cb8875d10a4bf28428ad81fdc3f8dac89108f7 bpf-actlog", + "seccomp-features": [ + "allow", + "errno", + "kill_process", + "kill_thread", + "log", + "trace", + "trap", + "user_notif" + ], + "version": 10 + }, + "preseed-time": "2020-07-24T21:41:43.156401424Z", + "preseeded": true, + "seed-restart-system-key": { + "apparmor-features": [ + "caps", + "dbus", + "domain", + "file", + "mount", + "namespaces", + "network", + "policy", + "ptrace", + "query", + "rlimit", + "signal" + ], + "apparmor-parser-features": [ + "unsafe" + ], + "apparmor-parser-mtime": 1589907589, + "build-id": "cb94e5eeee4cf7ecda53f8308a984cb155b55732", + "cgroup-version": "1", + "nfs-home": false, + "overlay-root": "", + "seccomp-compiler-version": "e6e309ad8aee052e5aa695dfaa040328ae1559c5 2.4.3 9b218ef9a4e508dd8a7f848095cb8875d10a4bf28428ad81fdc3f8dac89108f7 bpf-actlog", + "seccomp-features": [ + "allow", + "errno", + "kill", + "log", + "trace", + "trap", + "user_notif" + ], + "version": 10 + }, + "seed-restart-time": "2020-07-24T21:42:16.646098923Z", + "seed-start-time": "0001-01-01T00:00:00Z", + "seed-time": "2020-07-24T21:42:20.518607Z", + "seeded": true + }, + "status": "OK", + "status-code": 200, + "type": "sync" +}` + +// a system that was not preseeded at all +var noPreseedingJSON = ` +{ + "result": { + "seed-time": "2019-07-04T19:16:10.548793375-05:00", + "seeded": true + }, + "status": "OK", + "status-code": 200, + "type": "sync" +}` + +var seedingError = `{ + "result": { + "preseed-start-time": "2020-07-24T21:41:33.838194712Z", + "preseed-time": "2020-07-24T21:41:43.156401424Z", + "preseeded": true, + "seed-error": "cannot perform the following tasks:\n- xxx" + }, + "status": "OK", + "status-code": 200, + "type": "sync" +}` + +// a system that was preseeded, but didn't record the new keys +// this is the case for a system that was preseeded and then seeded with an old +// snapd, but then is refreshed to a version of snapd that supports snap debug +// seeding, where we want to still have sensible output +var oldPreseedingJSON = `{ + "result": { + "preseed-start-time": "0001-01-01T00:00:00Z", + "preseed-time": "0001-01-01T00:00:00Z", + "seed-restart-time": "2019-07-04T19:14:10.548793375-05:00", + "seed-start-time": "0001-01-01T00:00:00Z", + "seed-time": "2019-07-04T19:16:10.548793375-05:00", + "seeded": true, + "preseeded": true + }, + "status": "OK", + "status-code": 200, + "type": "sync" +}` + +var stillSeeding = `{ + "result": { + "preseed-start-time": "2020-07-24T21:41:33.838194712Z", + "preseed-time": "2020-07-24T21:41:43.156401424Z", + "preseeded": true + }, + "status": "OK", + "status-code": 200, + "type": "sync" +}` + +var stillSeedingNoPreseed = `{ + "result": {}, + "status": "OK", + "status-code": 200, + "type": "sync" +}` + +func (s *SnapSuite) TestDebugSeeding(c *C) { + tt := []struct { + jsonResp string + expStdout string + expStderr string + expErr string + comment string + hasUnicode bool + }{ + { + jsonResp: newPreseedNewSnapdSameSysKey, + expStdout: ` +seeded: true +preseeded: true +image-preseeding: 9.318s +seed-completion: 3.873s +`[1:], + comment: "new preseed keys, same system-key", + }, + { + jsonResp: newPreseedNewSnapdDiffSysKey, + expStdout: ` +seeded: true +preseeded: true +image-preseeding: 9.318s +seed-completion: 3.873s +preseed-system-key: { + "apparmor-features": [ + "caps", + "dbus", + "domain", + "file", + "mount", + "namespaces", + "network", + "network_v8", + "policy", + "ptrace", + "query", + "rlimit", + "signal" + ], + "apparmor-parser-features": [ + "unsafe" + ], + "apparmor-parser-mtime": 1589907589, + "build-id": "cb94e5eeee4cf7ecda53f8308a984cb155b55732", + "cgroup-version": "1", + "nfs-home": false, + "overlay-root": "", + "seccomp-compiler-version": "e6e309ad8aee052e5aa695dfaa040328ae1559c5 2.4.3 9b218ef9a4e508dd8a7f848095cb8875d10a4bf28428ad81fdc3f8dac89108f7 bpf-actlog", + "seccomp-features": [ + "allow", + "errno", + "kill_process", + "kill_thread", + "log", + "trace", + "trap", + "user_notif" + ], + "version": 10 +} +seed-restart-system-key: { + "apparmor-features": [ + "caps", + "dbus", + "domain", + "file", + "mount", + "namespaces", + "network", + "policy", + "ptrace", + "query", + "rlimit", + "signal" + ], + "apparmor-parser-features": [ + "unsafe" + ], + "apparmor-parser-mtime": 1589907589, + "build-id": "cb94e5eeee4cf7ecda53f8308a984cb155b55732", + "cgroup-version": "1", + "nfs-home": false, + "overlay-root": "", + "seccomp-compiler-version": "e6e309ad8aee052e5aa695dfaa040328ae1559c5 2.4.3 9b218ef9a4e508dd8a7f848095cb8875d10a4bf28428ad81fdc3f8dac89108f7 bpf-actlog", + "seccomp-features": [ + "allow", + "errno", + "kill", + "log", + "trace", + "trap", + "user_notif" + ], + "version": 10 +} +`[1:], + comment: "new preseed keys, different system-key", + }, + { + jsonResp: noPreseedingJSON, + expStdout: ` +seeded: true +preseeded: false +seed-completion: -- +`[1:], + comment: "not preseeded no unicode", + }, + { + jsonResp: noPreseedingJSON, + expStdout: ` +seeded: true +preseeded: false +seed-completion: – +`[1:], + comment: "not preseeded", + hasUnicode: true, + }, + { + jsonResp: oldPreseedingJSON, + expStdout: ` +seeded: true +preseeded: true +image-preseeding: 0s +seed-completion: 2m0s +`[1:], + comment: "old preseeded json", + }, + { + jsonResp: stillSeeding, + expStdout: ` +seeded: false +preseeded: true +image-preseeding: 9.318s +seed-completion: -- +`[1:], + comment: "preseeded, still seeding no unicode", + }, + { + jsonResp: stillSeeding, + expStdout: ` +seeded: false +preseeded: true +image-preseeding: 9.318s +seed-completion: – +`[1:], + hasUnicode: true, + comment: "preseeded, still seeding", + }, + { + jsonResp: stillSeedingNoPreseed, + expStdout: ` +seeded: false +preseeded: false +seed-completion: -- +`[1:], + comment: "not preseeded, still seeding no unicode", + }, + { + jsonResp: stillSeedingNoPreseed, + expStdout: ` +seeded: false +preseeded: false +seed-completion: – +`[1:], + hasUnicode: true, + comment: "not preseeded, still seeding", + }, + { + jsonResp: seedingError, + expStdout: ` +seeded: false +seed-error: | + cannot perform the following tasks: + - xxx +preseeded: true +image-preseeding: 9.318s +seed-completion: -- +`[1:], + comment: "preseeded, error during seeding", + }, + } + + for _, t := range tt { + comment := Commentf(t.comment) + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + n++ + switch n { + case 1: + c.Assert(r.Method, Equals, "GET", comment) + c.Assert(r.URL.Path, Equals, "/v2/debug", comment) + c.Assert(r.URL.RawQuery, Equals, "aspect=seeding", comment) + data, err := ioutil.ReadAll(r.Body) + c.Assert(err, IsNil, comment) + c.Assert(string(data), Equals, "", comment) + fmt.Fprintln(w, t.jsonResp) + default: + c.Fatalf("expected to get 1 request, now on %d", n) + } + }) + args := []string{"debug", "seeding"} + if t.hasUnicode { + args = append(args, "--unicode=always") + } + rest, err := snap.Parser(snap.Client()).ParseArgs(args) + if t.expErr != "" { + c.Assert(err, ErrorMatches, t.expErr, comment) + c.Assert(s.Stdout(), Equals, "", comment) + c.Assert(s.Stderr(), Equals, t.expStderr, comment) + continue + } + c.Assert(err, IsNil, comment) + c.Assert(rest, DeepEquals, []string{}, comment) + c.Assert(s.Stdout(), Equals, t.expStdout, comment) + c.Assert(s.Stderr(), Equals, "", comment) + c.Assert(n, Equals, 1, comment) + + s.ResetStdStreams() + } +} diff -Nru snapd-2.45.1+20.04.2/cmd/snap/cmd_download.go snapd-2.48.3+20.04/cmd/snap/cmd_download.go --- snapd-2.45.1+20.04.2/cmd/snap/cmd_download.go 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/cmd/snap/cmd_download.go 2021-02-02 08:21:12.000000000 +0000 @@ -71,7 +71,7 @@ }}) } -func fetchSnapAssertions(tsto *image.ToolingStore, snapPath string, snapInfo *snap.Info) (string, error) { +func fetchSnapAssertionsDirect(tsto *image.ToolingStore, snapPath string, snapInfo *snap.Info) (string, error) { db, err := asserts.OpenDatabase(&asserts.DatabaseConfig{ Backstore: asserts.NewMemoryBackstore(), Trusted: sysdb.Trusted(), @@ -113,6 +113,43 @@ `), assertPath, snapPath) } +// for testing +var downloadDirect = downloadDirectImpl + +func downloadDirectImpl(snapName string, revision snap.Revision, dlOpts image.DownloadOptions) error { + tsto, err := image.NewToolingStore() + if err != nil { + return err + } + + fmt.Fprintf(Stdout, i18n.G("Fetching snap %q\n"), snapName) + snapPath, snapInfo, _, err := tsto.DownloadSnap(snapName, dlOpts) + if err != nil { + return err + } + + fmt.Fprintf(Stdout, i18n.G("Fetching assertions for %q\n"), snapName) + assertPath, err := fetchSnapAssertionsDirect(tsto, snapPath, snapInfo) + if err != nil { + return err + } + printInstallHint(assertPath, snapPath) + return nil +} + +func (x *cmdDownload) downloadFromStore(snapName string, revision snap.Revision) error { + dlOpts := image.DownloadOptions{ + TargetDir: x.TargetDir, + Basename: x.Basename, + Channel: x.Channel, + CohortKey: x.CohortKey, + Revision: revision, + // if something goes wrong, don't force it to start over again + LeavePartialOnError: true, + } + return downloadDirect(snapName, revision, dlOpts) +} + func (x *cmdDownload) Execute(args []string) error { if strings.ContainsRune(x.Basename, filepath.Separator) { return fmt.Errorf(i18n.G("cannot specify a path in basename (use --target-dir for that)")) @@ -143,33 +180,5 @@ } snapName := string(x.Positional.Snap) - - tsto, err := image.NewToolingStore() - if err != nil { - return err - } - - fmt.Fprintf(Stdout, i18n.G("Fetching snap %q\n"), snapName) - dlOpts := image.DownloadOptions{ - TargetDir: x.TargetDir, - Basename: x.Basename, - Channel: x.Channel, - CohortKey: x.CohortKey, - Revision: revision, - // if something goes wrong, don't force it to start over again - LeavePartialOnError: true, - } - snapPath, snapInfo, _, err := tsto.DownloadSnap(snapName, dlOpts) - if err != nil { - return err - } - - fmt.Fprintf(Stdout, i18n.G("Fetching assertions for %q\n"), snapName) - assertPath, err := fetchSnapAssertions(tsto, snapPath, snapInfo) - if err != nil { - return err - } - printInstallHint(assertPath, snapPath) - - return nil + return x.downloadFromStore(snapName, revision) } diff -Nru snapd-2.45.1+20.04.2/cmd/snap/cmd_download_test.go snapd-2.48.3+20.04/cmd/snap/cmd_download_test.go --- snapd-2.45.1+20.04.2/cmd/snap/cmd_download_test.go 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/cmd/snap/cmd_download_test.go 2021-02-02 08:21:12.000000000 +0000 @@ -20,19 +20,22 @@ package main_test import ( + "fmt" "os" "path/filepath" "gopkg.in/check.v1" - snap "github.com/snapcore/snapd/cmd/snap" + snapCmd "github.com/snapcore/snapd/cmd/snap" + "github.com/snapcore/snapd/image" + "github.com/snapcore/snapd/snap" ) // these only cover errors that happen before hitting the network, // because we're not (yet!) mocking the tooling store func (s *SnapSuite) TestDownloadBadBasename(c *check.C) { - _, err := snap.Parser(snap.Client()).ParseArgs([]string{ + _, err := snapCmd.Parser(snapCmd.Client()).ParseArgs([]string{ "download", "--basename=/foo", "a-snap", }) @@ -40,7 +43,7 @@ } func (s *SnapSuite) TestDownloadBadChannelCombo(c *check.C) { - _, err := snap.Parser(snap.Client()).ParseArgs([]string{ + _, err := snapCmd.Parser(snapCmd.Client()).ParseArgs([]string{ "download", "--beta", "--channel=foo", "a-snap", }) @@ -48,7 +51,7 @@ } func (s *SnapSuite) TestDownloadCohortAndRevision(c *check.C) { - _, err := snap.Parser(snap.Client()).ParseArgs([]string{ + _, err := snapCmd.Parser(snapCmd.Client()).ParseArgs([]string{ "download", "--cohort=what", "--revision=1234", "a-snap", }) @@ -56,7 +59,7 @@ } func (s *SnapSuite) TestDownloadChannelAndRevision(c *check.C) { - _, err := snap.Parser(snap.Client()).ParseArgs([]string{ + _, err := snapCmd.Parser(snapCmd.Client()).ParseArgs([]string{ "download", "--beta", "--revision=1234", "a-snap", }) @@ -64,7 +67,7 @@ } func (s *SnapSuite) TestPrintInstalHint(c *check.C) { - snap.PrintInstallHint("foo_1.assert", "foo_1.snap") + snapCmd.PrintInstallHint("foo_1.assert", "foo_1.snap") c.Check(s.Stdout(), check.Equals, `Install the snap with: snap ack foo_1.assert snap install foo_1.snap @@ -75,10 +78,53 @@ c.Assert(err, check.IsNil) as := filepath.Join(cwd, "some-dir/foo_1.assert") sn := filepath.Join(cwd, "some-dir/foo_1.snap") - snap.PrintInstallHint(as, sn) + snapCmd.PrintInstallHint(as, sn) c.Check(s.Stdout(), check.Equals, `Install the snap with: snap ack some-dir/foo_1.assert snap install some-dir/foo_1.snap `) +} + +func (s *SnapSuite) TestDownloadDirect(c *check.C) { + var n int + restore := snapCmd.MockDownloadDirect(func(snapName string, revision snap.Revision, dlOpts image.DownloadOptions) 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") + c.Check(dlOpts.TargetDir, check.Equals, "some-target-dir") + c.Check(dlOpts.Channel, check.Equals, "some-channel") + c.Check(dlOpts.CohortKey, check.Equals, "some-cohort") + n++ + return nil + }) + defer restore() + + // check that a direct download got issued + _, err := snapCmd.Parser(snapCmd.Client()).ParseArgs([]string{ + "download", + "--target-directory=some-target-dir", + "--basename=some-base-name", + "--channel=some-channel", + "--cohort=some-cohort", + "a-snap"}, + ) + c.Assert(err, check.IsNil) + c.Check(n, check.Equals, 1) +} + +func (s *SnapSuite) TestDownloadDirectErrors(c *check.C) { + var n int + restore := snapCmd.MockDownloadDirect(func(snapName string, revision snap.Revision, dlOpts image.DownloadOptions) error { + n++ + return fmt.Errorf("some-error") + }) + defer restore() + // check that a direct download got issued + _, err := snapCmd.Parser(snapCmd.Client()).ParseArgs([]string{ + "download", + "a-snap"}, + ) + c.Assert(err, check.ErrorMatches, "some-error") + c.Check(n, check.Equals, 1) } diff -Nru snapd-2.45.1+20.04.2/cmd/snap/cmd_help.go snapd-2.48.3+20.04/cmd/snap/cmd_help.go --- snapd-2.45.1+20.04.2/cmd/snap/cmd_help.go 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/cmd/snap/cmd_help.go 2021-02-02 08:21:12.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2016 Canonical Ltd + * Copyright (C) 2016-2020 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 @@ -170,9 +170,17 @@ } type helpCategory struct { - Label string + Label string + // Other is set if the category Commands should be listed + // together under "... Other" in the `snap help` list. + Other bool Description string - Commands []string + // Commands list commands belonging to the category that should + // be listed under both `snap help` and "snap help --all`. + Commands []string + // AllOnlyCommands list commands belonging to the category that should + // be listed only under "snap help --all`. + AllOnlyCommands []string } // helpCategories helps us by grouping commands @@ -180,11 +188,11 @@ { Label: i18n.G("Basics"), Description: i18n.G("basic snap management"), - Commands: []string{"find", "info", "install", "list", "remove"}, + Commands: []string{"find", "info", "install", "remove", "list"}, }, { Label: i18n.G("...more"), Description: i18n.G("slightly more advanced snap management"), - Commands: []string{"refresh", "revert", "switch", "disable", "enable"}, + Commands: []string{"refresh", "revert", "switch", "disable", "enable", "create-cohort"}, }, { Label: i18n.G("History"), Description: i18n.G("manage system change transactions"), @@ -194,33 +202,52 @@ Description: i18n.G("manage services"), Commands: []string{"services", "start", "stop", "restart", "logs"}, }, { - Label: i18n.G("Commands"), - Description: i18n.G("manage aliases"), - Commands: []string{"alias", "aliases", "unalias", "prefer"}, + Label: i18n.G("Permissions"), + Description: i18n.G("manage permissions"), + Commands: []string{"connections", "interface", "connect", "disconnect"}, }, { Label: i18n.G("Configuration"), Description: i18n.G("system administration and configuration"), Commands: []string{"get", "set", "unset", "wait"}, }, { + Label: i18n.G("App Aliases"), + Description: i18n.G("manage aliases"), + Commands: []string{"alias", "aliases", "unalias", "prefer"}, + }, { Label: i18n.G("Account"), Description: i18n.G("authentication to snapd and the snap store"), Commands: []string{"login", "logout", "whoami"}, }, { - Label: i18n.G("Permissions"), - Description: i18n.G("manage permissions"), - Commands: []string{"connections", "interface", "connect", "disconnect"}, + Label: i18n.G("Snapshots"), + Description: i18n.G("archives of snap data"), + Commands: []string{"saved", "save", "check-snapshot", "restore", "forget"}, + AllOnlyCommands: []string{"export-snapshot", "import-snapshot"}, + }, { + Label: i18n.G("Device"), + Description: i18n.G("manage device"), + Commands: []string{"model", "reboot", "recovery"}, + }, { + Label: i18n.G("Warnings"), + Other: true, + Description: i18n.G("manage warnings"), + Commands: []string{"warnings", "okay"}, + }, { + Label: i18n.G("Assertions"), + Other: true, + Description: i18n.G("manage assertions"), + Commands: []string{"known", "ack"}, }, { - Label: i18n.G("Snapshots"), - Description: i18n.G("archives of snap data"), - Commands: []string{"saved", "save", "check-snapshot", "restore", "forget"}, - }, { - Label: i18n.G("Other"), - Description: i18n.G("miscellanea"), - Commands: []string{"version", "warnings", "okay", "ack", "known", "model", "create-cohort"}, - }, { - Label: i18n.G("Development"), - Description: i18n.G("developer-oriented features"), - Commands: []string{"run", "pack", "try", "download", "prepare-image"}, + Label: i18n.G("Introspection"), + Other: true, + Description: i18n.G("introspection and debugging of snapd"), + Commands: []string{"version"}, + AllOnlyCommands: []string{"debug"}, + }, + { + Label: i18n.G("Development"), + Description: i18n.G("developer-oriented features"), + Commands: []string{"download", "pack", "run", "try"}, + AllOnlyCommands: []string{"prepare-image"}, }, } @@ -231,18 +258,18 @@ enabling secure delivery and operation of the latest apps and utilities. `)) snapUsage = i18n.G("Usage: snap [...]") - snapHelpCategoriesIntro = i18n.G("Commands can be classified as follows:") + snapHelpCategoriesIntro = i18n.G("Commonly used commands can be classified as follows:") + snapHelpAllIntro = i18n.G("Commands can be classified as follows:") snapHelpAllFooter = i18n.G("For more information about a command, run 'snap help '.") snapHelpFooter = i18n.G("For a short summary of all commands, run 'snap help --all'.") ) -func printHelpHeader() { +func printHelpHeader(cmdsIntro string) { fmt.Fprintln(Stdout, longSnapDescription) fmt.Fprintln(Stdout) fmt.Fprintln(Stdout, snapUsage) fmt.Fprintln(Stdout) - fmt.Fprintln(Stdout, snapHelpCategoriesIntro) - fmt.Fprintln(Stdout) + fmt.Fprintln(Stdout, cmdsIntro) } func printHelpAllFooter() { @@ -257,22 +284,45 @@ // this is called when the Execute returns a flags.Error with ErrCommandRequired func printShortHelp() { - printHelpHeader() - maxLen := 0 + printHelpHeader(snapHelpCategoriesIntro) + maxLen := utf8.RuneCountInString("... Other") + var otherCommands []string + var develCateg *helpCategory for _, categ := range helpCategories { + if categ.Other { + otherCommands = append(otherCommands, categ.Commands...) + continue + } + if categ.Label == "Development" { + develCateg = &categ + } if l := utf8.RuneCountInString(categ.Label); l > maxLen { maxLen = l } } + + fmt.Fprintln(Stdout) for _, categ := range helpCategories { + // Other and Development will come last + if categ.Other || categ.Label == "Development" || len(categ.Commands) == 0 { + continue + } fmt.Fprintf(Stdout, "%*s: %s\n", maxLen+2, categ.Label, strings.Join(categ.Commands, ", ")) } + // ... Other + if len(otherCommands) > 0 { + fmt.Fprintf(Stdout, "%*s: %s\n", maxLen+2, "... Other", strings.Join(otherCommands, ", ")) + } + // Development last + if develCateg != nil && len(develCateg.Commands) > 0 { + fmt.Fprintf(Stdout, "%*s: %s\n", maxLen+2, "Development", strings.Join(develCateg.Commands, ", ")) + } printHelpFooter() } // this is "snap help --all" func printLongHelp(parser *flags.Parser) { - printHelpHeader() + printHelpHeader(snapHelpAllIntro) maxLen := 0 for _, categ := range helpCategories { for _, command := range categ.Commands { @@ -280,6 +330,11 @@ maxLen = l } } + for _, command := range categ.AllOnlyCommands { + if l := len(command); l > maxLen { + maxLen = l + } + } } // flags doesn't have a LookupCommand? @@ -289,10 +344,8 @@ cmdLookup[cmd.Name] = cmd } - for _, categ := range helpCategories { - fmt.Fprintln(Stdout) - fmt.Fprintf(Stdout, " %s (%s):\n", categ.Label, categ.Description) - for _, name := range categ.Commands { + listCmds := func(cmds []string) { + for _, name := range cmds { cmd := cmdLookup[name] if cmd == nil { fmt.Fprintf(Stderr, "??? Cannot find command %q mentioned in help categories, please report!\n", name) @@ -301,5 +354,12 @@ } } } + + for _, categ := range helpCategories { + fmt.Fprintln(Stdout) + fmt.Fprintf(Stdout, " %s (%s):\n", categ.Label, categ.Description) + listCmds(categ.Commands) + listCmds(categ.AllOnlyCommands) + } printHelpAllFooter() } diff -Nru snapd-2.45.1+20.04.2/cmd/snap/cmd_help_test.go snapd-2.48.3+20.04/cmd/snap/cmd_help_test.go --- snapd-2.45.1+20.04.2/cmd/snap/cmd_help_test.go 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/cmd/snap/cmd_help_test.go 2021-02-02 08:21:12.000000000 +0000 @@ -54,7 +54,9 @@ snap.LongSnapDescription, "", regexp.QuoteMeta(snap.SnapUsage), - "", ".*", "", + "", + snap.SnapHelpCategoriesIntro, + ".*", "", snap.SnapHelpAllFooter, snap.SnapHelpFooter, }, "\n")+`\s*`, comment) @@ -75,7 +77,7 @@ "", regexp.QuoteMeta(snap.SnapUsage), "", - snap.SnapHelpCategoriesIntro, + snap.SnapHelpAllIntro, "", ".*", "", snap.SnapHelpAllFooter, }, "\n")+`\s*`) @@ -135,15 +137,19 @@ categorised[cmd] = true } seen := make(map[string]string, len(all)) - for _, categ := range snap.HelpCategories { - for _, cmd := range categ.Commands { + seenCmds := func(cmds []string, label string) { + for _, cmd := range cmds { categorised[cmd] = true if seen[cmd] != "" { - c.Errorf("duplicated: %q in %q and %q", cmd, seen[cmd], categ.Label) + c.Errorf("duplicated: %q in %q and %q", cmd, seen[cmd], label) } - seen[cmd] = categ.Label + seen[cmd] = label } } + for _, categ := range snap.HelpCategories { + seenCmds(categ.Commands, categ.Label) + seenCmds(categ.AllOnlyCommands, categ.Label) + } for cmd := range all { if !categorised[cmd] { c.Errorf("uncategorised: %q", cmd) diff -Nru snapd-2.45.1+20.04.2/cmd/snap/cmd_info.go snapd-2.48.3+20.04/cmd/snap/cmd_info.go --- snapd-2.45.1+20.04.2/cmd/snap/cmd_info.go 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/cmd/snap/cmd_info.go 2021-02-02 08:21:12.000000000 +0000 @@ -35,7 +35,7 @@ "github.com/snapcore/snapd/asserts" "github.com/snapcore/snapd/client" - "github.com/snapcore/snapd/cmd" + "github.com/snapcore/snapd/client/clientutil" "github.com/snapcore/snapd/i18n" "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/snap" @@ -122,7 +122,7 @@ return nil, err } - direct, err := cmd.ClientSnapFromSnapInfo(info) + direct, err := clientutil.ClientSnapFromSnapInfo(info, nil) if err != nil { return nil, err } diff -Nru snapd-2.45.1+20.04.2/cmd/snap/cmd_interfaces_test.go snapd-2.48.3+20.04/cmd/snap/cmd_interfaces_test.go --- snapd-2.45.1+20.04.2/cmd/snap/cmd_interfaces_test.go 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/cmd/snap/cmd_interfaces_test.go 2021-02-02 08:21:12.000000000 +0000 @@ -142,15 +142,14 @@ c.Assert(s.Stdout(), Equals, expectedStdout) c.Assert(s.Stderr(), testutil.EqualsWrapped, InterfacesDeprecationNotice) - s.SetUpTest(c) + s.ResetStdStreams() // should be the same rest, err = Parser(Client()).ParseArgs([]string{"interfaces", "canonical-pi2"}) c.Assert(err, IsNil) c.Assert(rest, DeepEquals, []string{}) c.Assert(s.Stdout(), Equals, expectedStdout) c.Assert(s.Stderr(), testutil.EqualsWrapped, InterfacesDeprecationNotice) - - s.SetUpTest(c) + s.ResetStdStreams() // and the same again rest, err = Parser(Client()).ParseArgs([]string{"interfaces", "keyboard-lights"}) c.Assert(err, IsNil) diff -Nru snapd-2.45.1+20.04.2/cmd/snap/cmd_model.go snapd-2.48.3+20.04/cmd/snap/cmd_model.go --- snapd-2.45.1+20.04.2/cmd/snap/cmd_model.go 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/cmd/snap/cmd_model.go 2021-02-02 08:21:12.000000000 +0000 @@ -54,8 +54,8 @@ 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 lexographical order with meta headers and primary key - // headers removed, and big nasty keys such as device-key-sha3-384 and + // 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 @@ -68,6 +68,8 @@ "gadget", "kernel", "revision", + "store", + "system-user-authority", "timestamp", "required-snaps", "device-key-sha3-384", @@ -222,6 +224,14 @@ 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) + } + // serial is same for all variants fmt.Fprintf(w, "serial%s\t%s\n", separator, serial) @@ -241,7 +251,7 @@ // switch on which header it is to handle some special cases switch headerName { // list of scalars - case "required-snaps": + case "required-snaps", "system-user-authority": headerIfaceList, ok := headerValue.([]interface{}) if !ok { return invalidTypeErr diff -Nru snapd-2.45.1+20.04.2/cmd/snap/cmd_model_test.go snapd-2.48.3+20.04/cmd/snap/cmd_model_test.go --- snapd-2.45.1+20.04.2/cmd/snap/cmd_model_test.go 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/cmd/snap/cmd_model_test.go 2021-02-02 08:21:12.000000000 +0000 @@ -37,6 +37,10 @@ base: core18 gadget: pc=18 kernel: pc-kernel=18 +store: mememestore +system-user-authority: + - youyouyou + - mememe required-snaps: - core - hello-world @@ -55,6 +59,50 @@ 85yK2e/NQ/fxSwQJMhBF74jM1z9arq6RMiE/KOleFAOraKn2hcROKnEeinABW+sOn6vNuMVv ` +const happyUC20ModelAssertionResponse = `type: model +authority-id: testrootorg +series: 16 +brand-id: testrootorg +model: test-snapd-core-20-amd64 +architecture: amd64 +base: core20 +grade: secured +snaps: + - + default-channel: 20/edge + id: UqFziVZDHLSyO3TqSWgNBoAdHbLI4dAH + name: pc + type: gadget + - + default-channel: 20/edge + id: pYVQrBcKmBa0mZ4CCN7ExT6jH8rY1hza + name: pc-kernel + type: kernel + - + default-channel: latest/stable + id: DLqre5XGLbDqg9jPtiAhRRjDuPVa5X1q + name: core20 + type: base + - + default-channel: latest/stable + id: PMrrV4ml8uWuEUDBT8dSGnKUYbevVhc4 + name: snapd + type: snapd +timestamp: 2018-09-11T22:00:00+00:00 +sign-key-sha3-384: hIedp1AvrWlcDI4uS_qjoFLzjKl5enu4G2FYJpgB3Pj-tUzGlTQBxMBsBmi-tnJR + +AcLBUgQAAQoABgUCX06qogAAv10QAFaqQ0NDDvIB7LqM0xNIz+5Y6PB5wJaRk0HqVsg2LlNgS0PQ +uJf0uFMV4GjQMraL7ZYv9BGyUoA+cz8Nbiz85m1g2ADt0ugqR/x2bAojii9lbFLmWpDMJcZhrtB1 +3k32lEUwqTMvzYTGiZ6TVug0KYbdmf2+5IGxsayAS3EwdrfbuGRHZOv6XGV7bmm1GEwCRAFvgHCk +BHKoLZ+rfbNclF4l6G+biWJTdyc5jCMpMQ6X/INnx2hXaMRf9Jfrpl6s2bGCfsxW6HVf7AWZ8qHK +jtWPQqJ6NFu2Kw1lYIA202ReK8DC3gfAlOeNzUG5dTPor3KwAoDJJI8ZaQypOazEhO9SHERIutbP +eqPxPmEoB2+E0/o0+g0o5jK4qww3Yd7b8FTDkqm2xfuuldWAiAA4x6ZOQb2So9OLT6ovqHnD3D2r +pLW/lhqwfKp3xzIVUrLi0sjGOVXu5xFDDRyFICZ6kwC7JynRGfHoa5E2y7rv8ehnOZQJ+esz9sgY +lCJcyJ8vhabDlVHg0msSeNKMVBwhQnOSakEwlcfVyaSnapArkF+OCAMl8cuGpMTKO7vJLIJo2c2P +jcVE0ftsTGs9eBi2HmdDhu3e3fmxHt9VcC4uRSOnYNVcJnMh0yVmG8RGS/Dqcz04II7llww6JJYG +KKjQ3RU/TduXa8VJsoWiRRUYAv3H +` + const happyModelWithDisplayNameAssertionResponse = `type: model authority-id: mememe series: 16 @@ -65,6 +113,10 @@ base: core18 gadget: pc=18 kernel: pc-kernel=18 +store: mememestore +system-user-authority: + - youyouyou + - mememe required-snaps: - core - hello-world @@ -131,6 +183,38 @@ eg== ` +const happySerialUC20AssertionResponse = `type: serial +authority-id: testrootorg +brand-id: testrootorg +model: test-snapd-core-20-amd64 +serial: 7777 +device-key: + AcbBTQRWhcGAARAAuKf9n7WvZDI7u3NzMkD8WN+dxCYrb0UE9XIaHcbrj0i2zJpxCtUtpzoEo7Uk + Cvxuhr2uBpzAa8fScwzOd77MGHIZQDpS7sFSkhYsSSN0m4sy8vRevsj0roN31fugCjRnhtLTkgxo + KSoAsK87vYnC+m5V5AHaRER7q1KgpUoVD7eLOJZyrd/tWecsLL9OK87yAQHdF/cVlQupOP6OU3fK + DllER6V2TD4jADK2Gyj2lDhy3F0+rE0a+zsGpmQQBorvzbozUHgBE3z/XjTTMrHYP4m+4V5HeWdn + rHt/x1LZ8wMTCMT1eeruclC82UPRgF0zWI+P7WgBqogJpCbfadhAj1zvKW+5vJ385n0BU7PoAZtA + KddBbsmEnfK/gWIxgFemIrYcYGhIBxYY6iNcygTYRFo4R9xm3bELHLG+viHggih4Lrjnb4sLHOdC + h3C4/45bY+6hSno8GQGlp4kYQQM8mrF9st51jIM6oyB84NtoySLYYE1wMeGNzDHSuI+1IiRmaTgy + Q2ImXTuqOhclhNA1sOi3R4H+oOBxe6GmoM5ATBPBqJeqUEvK8GpSRCig0QH4qMNF/abNKwvKhGMZ + LqtpFp5LNx7xYuAwoVkcq0nxQTsXctl3gJqY+lRx7mIeoXLZPKZyJees+5v96oa9lMdNX3f5UUpX + zq0cNhdgHrXZfcsAEQEAAQ== +device-key-sha3-384: CZeO_5nJm_Rg0izosNfcQRoQj9nFtAmK2Y_tz4YjlKlvS93b_9gTDHuby5HHwi7d +timestamp: 2020-09-03T14:42:47-05:00 +sign-key-sha3-384: hIedp1AvrWlcDI4uS_qjoFLzjKl5enu4G2FYJpgB3Pj-tUzGlTQBxMBsBmi-tnJR + +AcLBUgQAAQoABgUCX1FHNwAAqFoQABFiyzipoTYAuYN0Wd7cXuPPD7z+z+E+LoZZ+j4vUKqvnGX8 +tksb2nEEOQhjSvVof5pPOswWgq8Nj52dtYA20R5Zgfy0MZHHcCCfgxaRj6EiFyrG5h9l5wWMnzdb +pXo9SJ3hxw6lKdj3n9RAAY0mACvw6f/trcyLeSxQ7EBm6X9c4ohJSjlHkKj0TlKkNTrFflko5aQH +uJUk/YgsvMTZUHbgj6QKHlODUH8iRvOHxzn/Y9BlnzBsb/SyzvNTPeQyzFtd9QkESI2sWghviys2 +fGeEZPeXU6xts6Ht+xhr3mj5npZwkkL/6YxSzm9owQ0zGrfaFTswN+xoDKZ5498qRtSY3mCK/5xx +kvWpOTHHhfvuS3GGyvRZOih7IAffDEwQsUNh8V9IjQNNTIkCYTPZz4WBM42mI8UgeDsnDImmcoc0 +GlqBeCxUigszJlEdUAHQklwW7Sgp13mceR3zB7BHgp4Sk7n0RyPuTQUA94ys6SeesK5YphwmhVed +V02lkdeqRbGt3yZ/T5Zg8CIUIM0RKDSqoHgvoCMZh98dRGv6LPRj/P0RSWmjYWotjdK+lXK1fySM +RXMNJIInZoC0x8qEwGLXVl5V3z8motLG71ie7PQ677W0dE9XM5LRnZHEKXP41jfaOO9vu12TtBsh +pe/pnYDfIzU6OyOsdmkGWaWD+nbD +` + const noModelAssertionYetResponse = ` { "type": "error", @@ -266,6 +350,17 @@ `[1:], }, { + comment: "normal uc20 serial and model asserts", + modelF: simpleHappyResponder(happyUC20ModelAssertionResponse), + serialF: simpleHappyResponder(happySerialUC20AssertionResponse), + outText: ` +brand MeMeMe (meuser*) +model test-snapd-core-20-amd64 +grade secured +serial 7777 +`[1:], + }, + { comment: "model assert has display-name", modelF: simpleHappyResponder(happyModelWithDisplayNameAssertionResponse), serialF: simpleHappyResponder(happySerialAssertionResponse), @@ -314,13 +409,17 @@ c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{}) c.Check(s.Stdout(), check.Equals, ` -brand-id: mememe -model: test-model -serial: serialserial -architecture: amd64 -base: core18 -gadget: pc=18 -kernel: pc-kernel=18 +brand-id: mememe +model: test-model +serial: serialserial +architecture: amd64 +base: core18 +gadget: pc=18 +kernel: pc-kernel=18 +store: mememestore +system-user-authority: + - youyouyou + - mememe timestamp: 2017-07-27T00:00:00Z required-snaps: - core @@ -341,14 +440,18 @@ c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{}) c.Check(s.Stdout(), check.Equals, ` -brand-id: mememe -model: test-model -serial: serialserial -architecture: amd64 -base: core18 -display-name: Model Name -gadget: pc=18 -kernel: pc-kernel=18 +brand-id: mememe +model: test-model +serial: serialserial +architecture: amd64 +base: core18 +display-name: Model Name +gadget: pc=18 +kernel: pc-kernel=18 +store: mememestore +system-user-authority: + - youyouyou + - mememe timestamp: 2017-07-27T00:00:00Z required-snaps: - core @@ -369,13 +472,17 @@ c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{}) c.Check(s.Stdout(), check.Equals, ` -brand-id: mememe -model: test-model -serial: -- (device not registered yet) -architecture: amd64 -base: core18 -gadget: pc=18 -kernel: pc-kernel=18 +brand-id: mememe +model: test-model +serial: -- (device not registered yet) +architecture: amd64 +base: core18 +gadget: pc=18 +kernel: pc-kernel=18 +store: mememestore +system-user-authority: + - youyouyou + - mememe timestamp: 2017-07-27T00:00:00Z required-snaps: - core diff -Nru snapd-2.45.1+20.04.2/cmd/snap/cmd_pack.go snapd-2.48.3+20.04/cmd/snap/cmd_pack.go --- snapd-2.45.1+20.04.2/cmd/snap/cmd_pack.go 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/cmd/snap/cmd_pack.go 2021-02-02 08:21:12.000000000 +0000 @@ -23,6 +23,8 @@ "fmt" "path/filepath" + "golang.org/x/xerrors" + "github.com/jessevdk/go-flags" "github.com/snapcore/snapd/i18n" @@ -114,7 +116,7 @@ if err != nil { // TRANSLATORS: the %q is the snap-dir (the first positional // argument to the command); the %v is an error - return fmt.Errorf(i18n.G("cannot pack %q: %v"), x.Positional.SnapDir, err) + return xerrors.Errorf(i18n.G("cannot pack %q: %w"), x.Positional.SnapDir, err) } // TRANSLATORS: %s is the path to the built snap file diff -Nru snapd-2.45.1+20.04.2/cmd/snap/cmd_prepare_image.go snapd-2.48.3+20.04/cmd/snap/cmd_prepare_image.go --- snapd-2.45.1+20.04.2/cmd/snap/cmd_prepare_image.go 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/cmd/snap/cmd_prepare_image.go 2021-02-02 08:21:12.000000000 +0000 @@ -20,6 +20,7 @@ package main import ( + "os" "strings" "github.com/jessevdk/go-flags" @@ -110,6 +111,9 @@ opts.SnapChannels = snapChannels } + // store-wide cohort key via env, see image/options.go + opts.WideCohortKey = os.Getenv("UBUNTU_STORE_COHORT_KEY") + opts.PrepareDir = x.Positional.TargetDir opts.Classic = x.Classic diff -Nru snapd-2.45.1+20.04.2/cmd/snap/cmd_prepare_image_test.go snapd-2.48.3+20.04/cmd/snap/cmd_prepare_image_test.go --- snapd-2.45.1+20.04.2/cmd/snap/cmd_prepare_image_test.go 2020-06-05 13:13:49.000000000 +0000 +++ snapd-2.48.3+20.04/cmd/snap/cmd_prepare_image_test.go 2021-02-02 08:21:12.000000000 +0000 @@ -21,6 +21,7 @@ import ( . "gopkg.in/check.v1" + "os" snap "github.com/snapcore/snapd/cmd/snap" "github.com/snapcore/snapd/image" @@ -92,6 +93,31 @@ }) } +func (s *SnapPrepareImageSuite) TestPrepareImageClassicWideCohort(c *C) { + var opts *image.Options + prep := func(o *image.Options) error { + opts = o + return nil + } + r := snap.MockImagePrepare(prep) + defer r() + + os.Setenv("UBUNTU_STORE_COHORT_KEY", "is-six-centuries") + + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"prepare-image", "--classic", "model", "prepare-dir"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + + c.Check(opts, DeepEquals, &image.Options{ + Classic: true, + WideCohortKey: "is-six-centuries", + ModelFile: "model", + PrepareDir: "prepare-dir", + }) + + os.Unsetenv("UBUNTU_STORE_COHORT_KEY") +} + func (s *SnapPrepareImageSuite) TestPrepareImageExtraSnaps(c *C) { var opts *image.Options prep := func(o *image.Options) error { diff -Nru snapd-2.45.1+20.04.2/cmd/snap/cmd_reboot.go snapd-2.48.3+20.04/cmd/snap/cmd_reboot.go --- snapd-2.45.1+20.04.2/cmd/snap/cmd_reboot.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48.3+20.04/cmd/snap/cmd_reboot.go 2021-02-02 08:21:12.000000000 +0000 @@ -0,0 +1,125 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2020 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" + + "github.com/jessevdk/go-flags" + + "github.com/snapcore/snapd/i18n" +) + +type cmdReboot struct { + clientMixin + Positional struct { + Label string + } `positional-args:"true"` + + RunMode bool `long:"run"` + InstallMode bool `long:"install"` + RecoverMode bool `long:"recover"` +} + +var shortRebootHelp = i18n.G("Reboot into selected system and mode") +var longRebootHelp = i18n.G(` +The reboot command reboots the system into a particular mode of the selected +recovery system. + +When called without a system label and without a mode it will just +trigger a regular reboot. + +When called without a system label but with a mode it will use the +current system to enter the given mode. + +Note that "recover" and "run" modes are only available for the +current system. +`) + +func init() { + addCommand("reboot", shortRebootHelp, longRebootHelp, func() flags.Commander { + return &cmdReboot{} + }, map[string]string{ + // TRANSLATORS: This should not start with a lowercase letter. + "run": i18n.G("Boot into run mode"), + // TRANSLATORS: This should not start with a lowercase letter. + "install": i18n.G("Boot into install mode"), + // TRANSLATORS: This should not start with a lowercase letter. + "recover": i18n.G("Boot into recover mode"), + }, []argDesc{ + { + // TRANSLATORS: This needs to begin with < and end with > + name: i18n.G("