diff -Nru snapd-2.47.1+20.10.1build1/asserts/gpgkeypairmgr.go snapd-2.48+21.04/asserts/gpgkeypairmgr.go --- snapd-2.47.1+20.10.1build1/asserts/gpgkeypairmgr.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/asserts/gpgkeypairmgr.go 2020-11-19 16:51:02.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.47.1+20.10.1build1/asserts/internal/grouping.go snapd-2.48+21.04/asserts/internal/grouping.go --- snapd-2.47.1+20.10.1build1/asserts/internal/grouping.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/asserts/internal/grouping.go 2020-11-19 16:51:02.000000000 +0000 @@ -52,6 +52,12 @@ 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 { diff -Nru snapd-2.47.1+20.10.1build1/asserts/internal/grouping_test.go snapd-2.48+21.04/asserts/internal/grouping_test.go --- snapd-2.47.1+20.10.1build1/asserts/internal/grouping_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/asserts/internal/grouping_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -57,6 +57,7 @@ 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) diff -Nru snapd-2.47.1+20.10.1build1/asserts/pool.go snapd-2.48+21.04/asserts/pool.go --- snapd-2.47.1+20.10.1build1/asserts/pool.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/asserts/pool.go 2020-11-19 16:51:02.000000000 +0000 @@ -74,7 +74,9 @@ unresolved map[string]*unresolvedRec prerequisites map[string]*unresolvedRec - bs Backstore + bs Backstore + unchanged map[string]bool + groups map[uint16]*groupRec curPhase poolPhase @@ -96,6 +98,7 @@ unresolved: make(map[string]*unresolvedRec), prerequisites: make(map[string]*unresolvedRec), bs: NewMemoryBackstore(), + unchanged: make(map[string]bool), groups: make(map[uint16]*groupRec), } } @@ -118,8 +121,9 @@ return 0, err } if gRec := p.groups[gnum]; gRec == nil { - gRec = new(groupRec) - p.groups[gnum] = gRec + p.groups[gnum] = &groupRec{ + name: group, + } } return gnum, nil } @@ -169,6 +173,7 @@ // 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 } @@ -224,6 +229,9 @@ } 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 @@ -415,7 +423,7 @@ return nil } -func (p *Pool) resolveWith(unresolved map[string]*unresolvedRec, uniq string, u *unresolvedRec, a Assertion, extrag *internal.Grouping) error { +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 @@ -436,11 +444,11 @@ delete(unresolved, uniq) if err := p.add(a, extrag); err != nil { p.setErr(extrag, err) - return err + return false, err } } } - return nil + return true, nil } // Add adds the given assertion associated with the given grouping to the @@ -452,15 +460,24 @@ // requiring the assertion plus the groups in grouping will be considered. // The latter is mostly relevant in scenarios where the server is pushing // assertions. -func (p *Pool) Add(a Assertion, grouping Grouping) error { +// 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 err + return false, err } if !a.SupportedFormat() { - return &UnsupportedFormatError{Ref: a.Ref(), Format: a.Format()} + 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 @@ -472,11 +489,11 @@ } else { ok, err := p.isPredefined(a.Ref()) if err != nil { - return err + return false, err } if ok { // nothing to do - return nil + return true, nil } // a is not tracked as unresolved in any way so far, // this is an atypical scenario where something gets @@ -492,16 +509,55 @@ if u.serializedLabel != grouping { var err error - extrag, err = p.groupings.Deserialize(string(grouping)) + extrag, err = deserializeGrouping(string(grouping)) if err != nil { - return err + return false, err } } return p.resolveWith(unresolved, uniq, u, a, extrag) } -// TODO: AddBatch +// 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") @@ -528,6 +584,9 @@ if e == nil { if u.at.Revision == RevisionNotKnown { e = ErrUnresolved + } else { + // unchanged + p.unchanged[uniq] = true } } if e != nil { @@ -570,6 +629,20 @@ 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. @@ -630,7 +703,8 @@ // CommitTo adds the assertions from groups without errors to the // given assertion database. Commit errors can be retrieved via Err -// per group. +// 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") @@ -677,3 +751,25 @@ 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.47.1+20.10.1build1/asserts/pool_test.go snapd-2.48+21.04/asserts/pool_test.go --- snapd-2.47.1+20.10.1build1/asserts/pool_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/asserts/pool_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -212,8 +212,9 @@ asserts.MakePoolGrouping(0): {at1111}, }) - err = pool.Add(s.rev1_1111, asserts.MakePoolGrouping(0)) + 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) @@ -247,8 +248,9 @@ asserts.MakePoolGrouping(0): {at1111}, }) - err = pool.Add(s.rev1_1111, asserts.MakePoolGrouping(0)) + 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) @@ -264,14 +266,17 @@ asserts.MakePoolGrouping(0): {storeKeyAt, dev1AcctAt, decl1At}, }) - err = pool.Add(s.decl1, asserts.MakePoolGrouping(0)) + b := asserts.NewBatch(nil) + err = b.Add(s.decl1) c.Assert(err, IsNil) - - err = pool.Add(storeKey, asserts.MakePoolGrouping(0)) + err = b.Add(storeKey) + c.Assert(err, IsNil) + err = b.Add(s.dev1Acct) c.Assert(err, IsNil) - err = pool.Add(s.dev1Acct, asserts.MakePoolGrouping(0)) + 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) @@ -306,12 +311,14 @@ asserts.MakePoolGrouping(0): {at1111}, }) - err = pool.Add(s.rev1_1111, asserts.MakePoolGrouping(0)) + ok, err := pool.Add(s.rev1_1111, asserts.MakePoolGrouping(0)) c.Assert(err, IsNil) + c.Assert(ok, Equals, true) // push prerequisite suggestion - err = pool.Add(s.decl1, asserts.MakePoolGrouping(0)) + 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) @@ -325,8 +332,9 @@ c.Check(pool.Err("for_one"), IsNil) - err = pool.Add(s.dev1Acct, asserts.MakePoolGrouping(0)) + 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) @@ -361,13 +369,78 @@ asserts.MakePoolGrouping(0): {atOne}, }) - err = pool.Add(s.decl1, asserts.MakePoolGrouping(0)) + ok, err := pool.Add(s.decl1, asserts.MakePoolGrouping(0)) c.Assert(err, IsNil) + c.Assert(ok, Equals, true) // new push suggestion - err = pool.Add(s.rev1_1111, asserts.MakePoolGrouping(0)) + 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) @@ -380,8 +453,9 @@ c.Check(pool.Err("for_one"), IsNil) - err = pool.Add(s.dev1Acct, asserts.MakePoolGrouping(0)) + 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) @@ -396,6 +470,10 @@ 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) { @@ -443,8 +521,10 @@ gSuggestion, err := pool.Singleton("suggestion") c.Assert(err, IsNil) - err = pool.Add(a, gSuggestion) - c.Assert(err, ErrorMatches, `proposed "test-only-decl" assertion has format 2 but 0 is latest supported`) + 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) { @@ -456,11 +536,13 @@ gSuggestion, err := pool.Singleton("suggestion") c.Assert(err, IsNil) - err = pool.Add(s.decl1_1, gSuggestion) + ok, err := pool.Add(s.decl1_1, gSuggestion) c.Assert(err, IsNil) + c.Assert(ok, Equals, true) - err = pool.Add(s.decl1, gSuggestion) + ok, err = pool.Add(s.decl1, gSuggestion) c.Assert(err, IsNil) + c.Assert(ok, Equals, true) toResolve, err := pool.ToResolve() c.Assert(err, IsNil) @@ -512,15 +594,18 @@ // re-adding of current revisions, is not what we expect // but needs not to produce unneeded roundtrips - err = pool.Add(s.hub.StoreAccountKey(""), asserts.MakePoolGrouping(0)) + 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 - err = pool.Add(s.dev1Acct, asserts.MakePoolGrouping(0)) + ok, err = pool.Add(s.dev1Acct, asserts.MakePoolGrouping(0)) c.Assert(err, IsNil) + c.Assert(ok, Equals, true) - err = pool.Add(s.decl1_1, asserts.MakePoolGrouping(0)) + 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) @@ -552,8 +637,9 @@ asserts.MakePoolGrouping(1): {s.dev2Acct.At(), s.decl2.At()}, }) - err = pool.Add(s.decl1_1, asserts.MakePoolGrouping(0)) + 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) @@ -607,8 +693,9 @@ err = pool.AddError(errBoom, storeKey.Ref()) c.Assert(err, IsNil) - err = pool.Add(s.rev1_1111, asserts.MakePoolGrouping(1)) + 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) @@ -641,8 +728,9 @@ asserts.MakePoolGrouping(1): {at1111}, }) - err = pool.Add(s.rev1_1111, asserts.MakePoolGrouping(1)) + 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) @@ -681,8 +769,9 @@ gSuggestion, err := pool.Singleton("suggestion") c.Assert(err, IsNil) - err = pool.Add(s.rev1_3333, gSuggestion) + 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) @@ -779,8 +868,9 @@ asserts.MakePoolGrouping(1): {at1111}, }) - err = pool.Add(s.rev1_1111, asserts.MakePoolGrouping(1)) + 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) @@ -813,8 +903,9 @@ asserts.MakePoolGrouping(0): {atOne}, }) - err = pool.Add(s.decl1, asserts.MakePoolGrouping(0)) + 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) @@ -848,3 +939,81 @@ 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.47.1+20.10.1build1/asserts/snapasserts/snapasserts.go snapd-2.48+21.04/asserts/snapasserts/snapasserts.go --- snapd-2.47.1+20.10.1build1/asserts/snapasserts/snapasserts.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/asserts/snapasserts/snapasserts.go 2020-11-19 16:51:02.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.47.1+20.10.1build1/asserts/snapasserts/validation_sets.go snapd-2.48+21.04/asserts/snapasserts/validation_sets.go --- snapd-2.47.1+20.10.1build1/asserts/snapasserts/validation_sets.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48+21.04/asserts/snapasserts/validation_sets.go 2020-11-19 16:51:02.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.47.1+20.10.1build1/asserts/snapasserts/validation_sets_test.go snapd-2.48+21.04/asserts/snapasserts/validation_sets_test.go --- snapd-2.47.1+20.10.1build1/asserts/snapasserts/validation_sets_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48+21.04/asserts/snapasserts/validation_sets_test.go 2020-11-19 16:51:02.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.47.1+20.10.1build1/asserts/sysdb/sysdb.go snapd-2.48+21.04/asserts/sysdb/sysdb.go --- snapd-2.47.1+20.10.1build1/asserts/sysdb/sysdb.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/asserts/sysdb/sysdb.go 2020-11-19 16:51:02.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.47.1+20.10.1build1/asserts/systestkeys/trusted.go snapd-2.48+21.04/asserts/systestkeys/trusted.go --- snapd-2.47.1+20.10.1build1/asserts/systestkeys/trusted.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/asserts/systestkeys/trusted.go 2020-11-19 16:51:02.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.47.1+20.10.1build1/asserts/validation_set.go snapd-2.48+21.04/asserts/validation_set.go --- snapd-2.47.1+20.10.1build1/asserts/validation_set.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/asserts/validation_set.go 2020-11-19 16:51:02.000000000 +0000 @@ -32,7 +32,7 @@ // Presence represents a presence constraint. type Presence string -var ( +const ( PresenceRequired Presence = "required" PresenceOptional Presence = "optional" PresenceInvalid Presence = "invalid" diff -Nru snapd-2.47.1+20.10.1build1/boot/assets.go snapd-2.48+21.04/boot/assets.go --- snapd-2.47.1+20.10.1build1/boot/assets.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/boot/assets.go 2020-11-19 16:51:02.000000000 +0000 @@ -171,10 +171,10 @@ var ErrObserverNotApplicable = errors.New("observer not applicable") // TrustedAssetsInstallObserverForModel returns a new trusted assets observer -// for use during installation of the run mode system, provided the device model -// supports secure boot. Otherwise, nil and ErrObserverNotApplicable is -// returned. -func TrustedAssetsInstallObserverForModel(model *asserts.Model, gadgetDir string) (*TrustedAssetsInstallObserver, error) { +// 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 @@ -182,11 +182,49 @@ 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 } @@ -209,54 +247,49 @@ return strutil.ListContains(hashes, assetHash) } -// TrustedAssetsInstallObserver tracks the installation of trusted boot assets. +// TrustedAssetsInstallObserver tracks the installation of trusted or managed +// boot assets. type TrustedAssetsInstallObserver struct { - model *asserts.Model - gadgetDir string - cache *trustedAssetsCache - blName string - trustedAssets []string - trackedAssets bootAssetsMap + model *asserts.Model + gadgetDir string + cache *trustedAssetsCache + + blName string + managedAssets []string + trustedAssets []string + trackedAssets bootAssetsMap + + recoveryBlName string + trustedRecoveryAssets []string trackedRecoveryAssets bootAssetsMap - encryptionKey secboot.EncryptionKey + + 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 -// managed boot assets, such as the bootloader binary which is measured as part -// of the secure boot. +// 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) (bool, error) { +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 true, nil + return gadget.ChangeApply, nil } - if o.blName == "" { - // we have no information about the bootloader yet - bl, err := bootloader.ForGadget(o.gadgetDir, root, &bootloader.Options{Role: bootloader.RoleRunMode, NoSlashBoot: true}) - if err != nil { - return false, fmt.Errorf("cannot find bootloader: %v", err) - } - o.blName = bl.Name() - tbl, ok := bl.(bootloader.TrustedAssetsBootloader) - if !ok { - return true, nil - } - trustedAssets, err := tbl.TrustedAssets() - if err != nil { - return false, fmt.Errorf("cannot list %q bootloader trusted assets: %v", bl.Name(), err) - } - o.trustedAssets = trustedAssets + 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 true, nil + return gadget.ChangeApply, nil } ta, err := o.cache.Add(data.After, o.blName, filepath.Base(relativeTarget)) if err != nil { - return false, err + 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 @@ -266,33 +299,22 @@ o.trackedAssets = bootAssetsMap{} } if len(o.trackedAssets[ta.name]) > 0 { - return false, fmt.Errorf("cannot reuse asset name %q", ta.name) + return gadget.ChangeAbort, fmt.Errorf("cannot reuse asset name %q", ta.name) } o.trackedAssets[ta.name] = append(o.trackedAssets[ta.name], ta.hash) } - return true, nil + 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 { - bl, err := bootloader.Find(recoveryRootDir, &bootloader.Options{ - Role: bootloader.RoleRecovery, - }) - if err != nil { - return fmt.Errorf("cannot identify recovery system bootloader: %v", err) - } - tbl, ok := bl.(bootloader.TrustedAssetsBootloader) - if !ok { - // not a trusted assets bootloader + if len(o.trustedRecoveryAssets) == 0 { + // not a trusted assets bootloader or has no trusted assets return nil } - trustedAssets, err := tbl.TrustedAssets() - if err != nil { - return fmt.Errorf("cannot list %q recovery bootloader trusted assets: %v", bl.Name(), err) - } - for _, trustedAsset := range trustedAssets { - ta, err := o.cache.Add(filepath.Join(recoveryRootDir, trustedAsset), bl.Name(), filepath.Base(trustedAsset)) + for _, trustedAsset := range o.trustedRecoveryAssets { + ta, err := o.cache.Add(filepath.Join(recoveryRootDir, trustedAsset), o.recoveryBlName, filepath.Base(trustedAsset)) if err != nil { return err } @@ -317,113 +339,171 @@ return o.trackedRecoveryAssets } -func (o *TrustedAssetsInstallObserver) ChosenEncryptionKey(key secboot.EncryptionKey) { - o.encryptionKey = key +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 measured boot assets during gadget updates, provided -// the device model supports secure boot. Otherwise, nil and ErrObserverNotApplicable is -// returned. -func TrustedAssetsUpdateObserverForModel(model *asserts.Model) (*TrustedAssetsUpdateObserver, error) { +// 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 } - // there is no need to track assets if we did not seal any keys - if !hasSealedKeys(dirs.GlobalRootDir) { - return nil, ErrObserverNotApplicable + // trusted assets need tracking only when the system is using encryption + // for its data partitions + trackTrustedAssets := hasSealedKeys(dirs.GlobalRootDir) + + // 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 } - return &TrustedAssetsUpdateObserver{ + // 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, - }, nil + + 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. +// 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 findMaybeTrustedAssetsBootloader(root string, opts *bootloader.Options) (foundBl bootloader.Bootloader, trustedAssets []string, err error) { - foundBl, err = bootloader.Find(root, opts) +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) } - tbl, ok := foundBl.(bootloader.TrustedAssetsBootloader) - if !ok { - return foundBl, nil, nil - } - trustedAssets, err = tbl.TrustedAssets() + 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, fmt.Errorf("cannot list %q bootloader trusted assets: %v", foundBl.Name(), err) + return nil, nil, nil, fmt.Errorf("cannot find bootloader: %v", err) } - return foundBl, trustedAssets, nil + 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 managed boot assets, such as -// the bootloader binary which is measured as part of the secure boot. +// 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) (bool, error) { +func (o *TrustedAssetsUpdateObserver) Observe(op gadget.ContentOperation, affectedStruct *gadget.LaidOutStructure, root, relativeTarget string, data *gadget.ContentChange) (gadget.ContentChangeAction, error) { var whichBootloader bootloader.Bootloader - var whichAssets []string + var whichTrustedAssets []string + var whichManagedAssets []string var err error var isRecovery bool switch affectedStruct.Role { case gadget.SystemBoot: - if o.bootBootloader == nil { - o.bootBootloader, o.bootTrustedAssets, err = findMaybeTrustedAssetsBootloader(root, &bootloader.Options{ - Role: bootloader.RoleRunMode, - NoSlashBoot: true, - }) - if err != nil { - return false, err - } - } whichBootloader = o.bootBootloader - whichAssets = o.bootTrustedAssets + whichTrustedAssets = o.bootTrustedAssets + whichManagedAssets = o.bootManagedAssets case gadget.SystemSeed: - if o.seedBootloader == nil { - o.seedBootloader, o.seedTrustedAssets, err = findMaybeTrustedAssetsBootloader(root, &bootloader.Options{ - Role: bootloader.RoleRecovery, - }) - if err != nil { - return false, err - } - } whichBootloader = o.seedBootloader - whichAssets = o.seedTrustedAssets + whichTrustedAssets = o.seedTrustedAssets + whichManagedAssets = o.seedManagedAssets isRecovery = true default: // only system-seed and system-boot are of interest - return true, nil + 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(whichAssets) == 0 || !strutil.ListContains(whichAssets, relativeTarget) { + + 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 true, nil + 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 false, fmt.Errorf("cannot load modeenv: %v", err) + return gadget.ChangeAbort, fmt.Errorf("cannot load modeenv: %v", err) } } switch op { @@ -433,14 +513,14 @@ return o.observeRollback(whichBootloader, isRecovery, root, relativeTarget, data) default: // we only care about update and rollback actions - return false, nil + return gadget.ChangeApply, nil } } -func (o *TrustedAssetsUpdateObserver) observeUpdate(bl bootloader.Bootloader, recovery bool, root, relativeTarget string, change *gadget.ContentChange) (bool, error) { +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 false, fmt.Errorf("cannot copy modeenv: %v", err) + return gadget.ChangeAbort, fmt.Errorf("cannot copy modeenv: %v", err) } // we may be running after a mid-update reboot, where a successful boot @@ -453,13 +533,13 @@ // it existed taBefore, err = o.cache.Add(change.Before, bl.Name(), filepath.Base(relativeTarget)) if err != nil { - return false, err + return gadget.ChangeAbort, err } } ta, err := o.cache.Add(change.After, bl.Name(), filepath.Base(relativeTarget)) if err != nil { - return false, err + return gadget.ChangeAbort, err } trustedAssets := &o.modeenv.CurrentTrustedBootAssets @@ -490,21 +570,21 @@ // during an update; more entries indicates that the // same asset name is used multiple times with different // content - return false, fmt.Errorf("cannot reuse asset name %q", ta.name) + 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 true, nil + return gadget.ChangeApply, nil } if err := o.modeenv.Write(); err != nil { - return false, fmt.Errorf("cannot write modeeenv: %v", err) + return gadget.ChangeAbort, fmt.Errorf("cannot write modeeenv: %v", err) } - return true, nil + return gadget.ChangeApply, nil } -func (o *TrustedAssetsUpdateObserver) observeRollback(bl bootloader.Bootloader, recovery bool, root, relativeTarget string, data *gadget.ContentChange) (bool, error) { +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 { @@ -516,7 +596,7 @@ hashList, ok := (*trustedAssets)[assetName] if !ok || len(hashList) == 0 { // asset not tracked in modeenv - return true, nil + return gadget.ChangeApply, nil } // new assets are appended to the list @@ -527,20 +607,20 @@ if err != nil { // file may not exist if it was added by the update, that's ok if !os.IsNotExist(err) { - return false, fmt.Errorf("cannot calculate the digest of current asset: %v", 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 false, fmt.Errorf("tracked asset %q is unexpectedly missing from disk", + 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 false, fmt.Errorf("unexpected content of existing asset %q", relativeTarget) + return gadget.ChangeAbort, fmt.Errorf("unexpected content of existing asset %q", relativeTarget) } } @@ -556,7 +636,7 @@ // 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 false, fmt.Errorf("cannot remove unused boot asset %v:%v: %v", assetName, newHash, err) + return gadget.ChangeAbort, fmt.Errorf("cannot remove unused boot asset %v:%v: %v", assetName, newHash, err) } } @@ -568,10 +648,10 @@ } if err := o.modeenv.Write(); err != nil { - return false, fmt.Errorf("cannot write modeeenv: %v", err) + return gadget.ChangeAbort, fmt.Errorf("cannot write modeeenv: %v", err) } - return false, nil + return gadget.ChangeApply, nil } // BeforeWrite is called when the update process has been staged for execution. @@ -671,7 +751,7 @@ } // let's find the bootloader first - bl, trustedAssets, err := findMaybeTrustedAssetsBootloader(root, opts) + bl, trustedAssets, err := findMaybeTrustedBootloaderAndAssets(root, opts) if err != nil { return nil, err } diff -Nru snapd-2.47.1+20.10.1build1/boot/assets_test.go snapd-2.48+21.04/boot/assets_test.go --- snapd-2.47.1+20.10.1build1/boot/assets_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/boot/assets_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -36,7 +36,6 @@ "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/seed" "github.com/snapcore/snapd/snap" @@ -55,7 +54,7 @@ c.Assert(os.MkdirAll(boot.InitramfsUbuntuBootDir, 0755), IsNil) c.Assert(os.MkdirAll(boot.InitramfsUbuntuSeedDir, 0755), IsNil) - restore := boot.MockSecbootResealKey(func(params *secboot.ResealKeyParams) error { return nil }) + restore := boot.MockSecbootResealKeys(func(params *secboot.ResealKeysParams) error { return nil }) s.AddCleanup(restore) } @@ -65,12 +64,16 @@ c.Check(l, DeepEquals, expected) } -func (s *assetsSuite) uc20UpdateObserver(c *C) (*boot.TrustedAssetsUpdateObserver, *asserts.Model) { - uc20Model := boottest.MakeMockUC20Model() +func (s *assetsSuite) uc20UpdateObserverEncryptedSystemMockedBootloader(c *C) (*boot.TrustedAssetsUpdateObserver, *asserts.Model) { // checked by TrustedAssetsUpdateObserverForModel and // resealKeyToModeenv s.stampSealedKeys(c, dirs.GlobalRootDir) - obs, err := boot.TrustedAssetsUpdateObserverForModel(uc20Model) + 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 @@ -173,12 +176,14 @@ err := os.Chmod(cacheDir, 0000) c.Assert(err, IsNil) - 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) + 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) @@ -187,13 +192,15 @@ _, 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") - blDir := filepath.Join(cacheDir, "grub") - defer os.Chmod(blDir, 0755) - err = os.Chmod(blDir, 0000) - c.Assert(err, IsNil) + 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`) + _, 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) { @@ -222,17 +229,45 @@ func (s *assetsSuite) TestInstallObserverNew(c *C) { d := c.MkDir() - // we get an observer for UC20 + // bootloader in gadget cannot be identified uc20Model := boottest.MakeMockUC20Model() - obs, err := boot.TrustedAssetsInstallObserverForModel(uc20Model, d) - c.Assert(err, IsNil) - c.Assert(obs, NotNil) + 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) + 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 ( @@ -257,7 +292,8 @@ // we get an observer for UC20 uc20Model := boottest.MakeMockUC20Model() - obs, err := boot.TrustedAssetsInstallObserverForModel(uc20Model, d) + useEncryption := true + obs, err := boot.TrustedAssetsInstallObserverForModel(uc20Model, d, useEncryption) c.Assert(err, IsNil) c.Assert(obs, NotNil) @@ -278,19 +314,28 @@ Before: "", } // only grubx64.efi gets installed to system-boot - _, err = obs.Observe(gadget.ContentWrite, mockRunBootStruct, boot.InitramfsUbuntuBootDir, + 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 - _, err = obs.Observe(gadget.ContentWrite, mockRunBootStruct, boot.InitramfsUbuntuBootDir, + 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 - _, err = obs.Observe(gadget.ContentWrite, mockRunBootStruct, boot.InitramfsUbuntuBootDir, + 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)), @@ -305,9 +350,10 @@ otherWriteChange := &gadget.ContentChange{ After: filepath.Join(d, "other-foobar"), } - _, err = obs.Observe(gadget.ContentWrite, systemSeedStruct, boot.InitramfsUbuntuBootDir, + 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)), @@ -323,19 +369,19 @@ func (s *assetsSuite) TestInstallObserverObserveSystemBootMocked(c *C) { d := c.MkDir() - tab := bootloadertest.Mock("trusted-assets", "").WithTrustedAssets() - bootloader.Force(tab) - defer bootloader.Force(nil) - tab.TrustedAssetsList = []string{ + tab := s.bootloaderWithTrustedAssets(c, []string{ "asset", "nested/other-asset", - } + }) // we get an observer for UC20 uc20Model := boottest.MakeMockUC20Model() - obs, err := boot.TrustedAssetsInstallObserverForModel(uc20Model, d) + 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 @@ -349,28 +395,30 @@ // there is no original file in place Before: "", } - _, err = obs.Observe(gadget.ContentWrite, mockRunBootStruct, boot.InitramfsUbuntuBootDir, + 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 - _, err = obs.Observe(gadget.ContentWrite, mockRunBootStruct, boot.InitramfsUbuntuBootDir, + res, err = obs.Observe(gadget.ContentWrite, mockRunBootStruct, boot.InitramfsUbuntuBootDir, "asset", writeChange) c.Assert(err, IsNil) + c.Check(res, Equals, gadget.ChangeApply) // different one - _, err = obs.Observe(gadget.ContentWrite, mockRunBootStruct, boot.InitramfsUbuntuBootDir, + 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 - _, err = obs.Observe(gadget.ContentWrite, mockRunBootStruct, boot.InitramfsUbuntuBootDir, + 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-assets", "*"), []string{ - filepath.Join(dirs.SnapBootAssetsDir, "trusted-assets", fmt.Sprintf("asset-%s", dataHash)), - filepath.Join(dirs.SnapBootAssetsDir, "trusted-assets", fmt.Sprintf("other-asset-%s", dataHash)), + 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)), }) - // the list of trusted assets was asked for just once - c.Check(tab.TrustedAssetsCalls, Equals, 1) // let's see what the observer has tracked tracked := obs.CurrentTrustedBootAssetsMap() c.Check(tracked, DeepEquals, boot.BootAssetsMap{ @@ -379,7 +427,43 @@ }) } +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 @@ -388,22 +472,19 @@ // we get an observer for UC20 uc20Model := boottest.MakeMockUC20Model() - obs, err := boot.TrustedAssetsInstallObserverForModel(uc20Model, d) + useEncryption := true + obs, err := boot.TrustedAssetsInstallObserverForModel(uc20Model, d, useEncryption) c.Assert(err, IsNil) c.Assert(obs, NotNil) - - err = ioutil.WriteFile(filepath.Join(d, "foobar"), []byte("foobar"), 0644) - c.Assert(err, IsNil) - // bootloder is found, but ignored because it does not support trusted assets - _, err = obs.Observe(gadget.ContentWrite, mockRunBootStruct, boot.InitramfsUbuntuBootDir, - "asset", &gadget.ContentChange{After: filepath.Join(d, "foobar")}) - c.Assert(err, IsNil) - c.Check(osutil.IsDirectory(dirs.SnapBootAssetsDir), Equals, false, - Commentf("%q exists while it should not", dirs.SnapBootAssetsDir)) - c.Check(obs.CurrentTrustedBootAssetsMap(), IsNil) + 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() @@ -412,103 +493,64 @@ // we get an observer for UC20 uc20Model := boottest.MakeMockUC20Model() - obs, err := boot.TrustedAssetsInstallObserverForModel(uc20Model, d) + useEncryption := true + obs, err := boot.TrustedAssetsInstallObserverForModel(uc20Model, d, useEncryption) c.Assert(err, IsNil) c.Assert(obs, NotNil) - - err = ioutil.WriteFile(filepath.Join(d, "foobar"), []byte("foobar"), 0644) - c.Assert(err, IsNil) - // bootloder is found, but ignored because it does not support trusted assets - _, err = obs.Observe(gadget.ContentWrite, mockRunBootStruct, boot.InitramfsUbuntuBootDir, - "asset", &gadget.ContentChange{After: filepath.Join(d, "foobar")}) - c.Assert(err, IsNil) - _, err = obs.Observe(gadget.ContentWrite, mockRunBootStruct, boot.InitramfsUbuntuBootDir, - "other-asset", &gadget.ContentChange{After: filepath.Join(d, "foobar")}) - c.Assert(err, IsNil) - // the list of trusted assets was asked for just once - c.Check(tab.TrustedAssetsCalls, Equals, 1) - c.Check(obs.CurrentTrustedBootAssetsMap(), IsNil) + 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 := bootloadertest.Mock("trusted-assets", "").WithTrustedAssets() - bootloader.Force(tab) - defer bootloader.Force(nil) - - tab.TrustedAssetsList = []string{ + tab := s.bootloaderWithTrustedAssets(c, []string{ "asset", "nested/asset", - } + }) // we get an observer for UC20 uc20Model := boottest.MakeMockUC20Model() - obs, err := boot.TrustedAssetsInstallObserverForModel(uc20Model, d) + 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) - _, err = obs.Observe(gadget.ContentWrite, mockRunBootStruct, boot.InitramfsUbuntuBootDir, "asset", + 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 - _, err = obs.Observe(gadget.ContentWrite, mockRunBootStruct, boot.InitramfsUbuntuBootDir, "nested/asset", + 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"`) - // the list of trusted assets was asked for just once - c.Check(tab.TrustedAssetsCalls, Equals, 1) -} - -func (s *assetsSuite) TestInstallObserverObserveErr(c *C) { - d := c.MkDir() - - tab := bootloadertest.Mock("trusted-assets", "").WithTrustedAssets() - tab.TrustedAssetsErr = fmt.Errorf("mocked trusted assets error") - - bootloader.ForceError(fmt.Errorf("mocked bootloader error")) - // we get an observer for UC20 - obs, _ := s.uc20UpdateObserver(c) - - err := ioutil.WriteFile(filepath.Join(d, "foobar"), []byte("data"), 0644) - c.Assert(err, IsNil) - - // there is no known bootloader in gadget - _, err = obs.Observe(gadget.ContentWrite, mockRunBootStruct, boot.InitramfsUbuntuBootDir, - "asset", &gadget.ContentChange{After: filepath.Join(d, "foobar")}) - c.Assert(err, ErrorMatches, "cannot find bootloader: mocked bootloader error") - - // force a bootloader now - bootloader.ForceError(nil) - bootloader.Force(tab) - defer bootloader.Force(nil) - - _, err = obs.Observe(gadget.ContentWrite, mockRunBootStruct, boot.InitramfsUbuntuBootDir, - "asset", &gadget.ContentChange{After: filepath.Join(d, "foobar")}) - c.Assert(err, ErrorMatches, `cannot list "trusted-assets" bootloader trusted assets: mocked trusted assets error`) + c.Check(res, Equals, gadget.ChangeAbort) } func (s *assetsSuite) TestInstallObserverObserveExistingRecoveryMocked(c *C) { d := c.MkDir() - tab := bootloadertest.Mock("recovery-bootloader", "").WithTrustedAssets() - // MockBootloader does not implement trusted assets - bootloader.Force(tab) - defer bootloader.Force(nil) - tab.TrustedAssetsList = []string{ + tab := s.bootloaderWithTrustedAssets(c, []string{ "asset", "nested/other-asset", "shim", - } + }) // we get an observer for UC20 uc20Model := boottest.MakeMockUC20Model() - obs, err := boot.TrustedAssetsInstallObserverForModel(uc20Model, d) + 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 @@ -526,14 +568,13 @@ err = obs.ObserveExistingTrustedRecoveryAssets(d) c.Assert(err, IsNil) - // a single file in cache - checkContentGlob(c, filepath.Join(dirs.SnapBootAssetsDir, "recovery-bootloader", "*"), []string{ - filepath.Join(dirs.SnapBootAssetsDir, "recovery-bootloader", fmt.Sprintf("asset-%s", dataHash)), - filepath.Join(dirs.SnapBootAssetsDir, "recovery-bootloader", fmt.Sprintf("other-asset-%s", dataHash)), - filepath.Join(dirs.SnapBootAssetsDir, "recovery-bootloader", fmt.Sprintf("shim-%s", shimHash)), + 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 was asked for just once - c.Check(tab.TrustedAssetsCalls, Equals, 1) + // 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{ @@ -543,36 +584,6 @@ }) } -func (s *assetsSuite) TestInstallObserverObserveExistingRecoveryNoAssets(c *C) { - d := c.MkDir() - - tab := bootloadertest.Mock("recovery-bootloader", "").WithTrustedAssets() - // MockBootloader does not implement trusted assets - bootloader.Force(tab) - defer bootloader.Force(nil) - - uc20Model := boottest.MakeMockUC20Model() - obs, err := boot.TrustedAssetsInstallObserverForModel(uc20Model, d) - c.Assert(err, IsNil) - c.Assert(obs, NotNil) - - // does not fail when the bootloader has no trusted assets - err = obs.ObserveExistingTrustedRecoveryAssets(d) - c.Assert(err, IsNil) - // asked for the list of trusted assets - c.Check(tab.TrustedAssetsCalls, Equals, 1) - // nothing was tracked - tracked := obs.CurrentTrustedRecoveryBootAssetsMap() - c.Check(tracked, IsNil) - - // force a non trusted bootloader - bl := bootloadertest.Mock("non-trusted-bootloader", "") - bootloader.Force(bl) - // happy with non trusted bootloader too - err = obs.ObserveExistingTrustedRecoveryAssets(d) - c.Assert(err, IsNil) -} - func (s *assetsSuite) TestInstallObserverObserveExistingRecoveryReuseNameErr(c *C) { d := c.MkDir() @@ -582,9 +593,12 @@ }) // we get an observer for UC20 uc20Model := boottest.MakeMockUC20Model() - obs, err := boot.TrustedAssetsInstallObserverForModel(uc20Model, d) + 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) @@ -596,53 +610,65 @@ err = obs.ObserveExistingTrustedRecoveryAssets(d) // same asset name but different content c.Assert(err, ErrorMatches, `cannot reuse recovery asset name "asset"`) - // the list of trusted assets was asked for just once - c.Check(tab.TrustedAssetsCalls, Equals, 1) + // got the list of trusted assets for recovery bootloader + c.Check(tab.TrustedAssetsCalls, Equals, 2) } -func (s *assetsSuite) TestInstallObserverObserveExistingRecoveryErr(c *C) { +func (s *assetsSuite) TestInstallObserverObserveExistingRecoveryButMissingErr(c *C) { d := c.MkDir() - uc20Model := boottest.MakeMockUC20Model() - obs, err := boot.TrustedAssetsInstallObserverForModel(uc20Model, d) - c.Assert(err, IsNil) - c.Assert(obs, NotNil) - tab := s.bootloaderWithTrustedAssets(c, []string{ "asset", }) - // no trusted asset - err = obs.ObserveExistingTrustedRecoveryAssets(d) - c.Assert(err, ErrorMatches, "cannot open asset file: .*/asset: no such file or directory") - c.Check(tab.TrustedAssetsCalls, Equals, 1) - - tab.TrustedAssetsErr = fmt.Errorf("fail") - err = obs.ObserveExistingTrustedRecoveryAssets(d) - c.Assert(err, ErrorMatches, `cannot list "trusted" recovery bootloader trusted assets: fail`) + 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) - // force an error - bootloader.ForceError(fmt.Errorf("fail bootloader")) + // trusted asset is missing err = obs.ObserveExistingTrustedRecoveryAssets(d) - c.Assert(err, ErrorMatches, `cannot identify recovery system bootloader: fail bootloader`) + c.Assert(err, ErrorMatches, "cannot open asset file: .*/asset: no such file or directory") } func (s *assetsSuite) TestUpdateObserverNew(c *C) { - // we get an observer for UC20, but only if we sealed keys + tab := s.bootloaderWithTrustedAssets(c, nil) + uc20Model := boottest.MakeMockUC20Model() - obs, err := boot.TrustedAssetsUpdateObserverForModel(uc20Model) + + gadgetDir := c.MkDir() + + // no trusted or managed assets + obs, err := boot.TrustedAssetsUpdateObserverForModel(uc20Model, gadgetDir) c.Assert(err, Equals, boot.ErrObserverNotApplicable) c.Check(obs, IsNil) - // sealed keys stamp + + // 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) - obs, err = boot.TrustedAssetsUpdateObserverForModel(uc20Model) + 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) + nonUC20obs, err := boot.TrustedAssetsUpdateObserverForModel(nonUC20Model, gadgetDir) c.Assert(err, Equals, boot.ErrObserverNotApplicable) c.Assert(nonUC20obs, IsNil) } @@ -689,38 +715,44 @@ "nested/other-asset", "shim", }) + tab.ManagedAssetsList = []string{ + "managed-asset", + } // we get an observer for UC20 - obs, _ := s.uc20UpdateObserver(c) + obs, _ := s.uc20UpdateObserverEncryptedSystemMockedBootloader(c) + // the list of trusted assets is obtained upfront + c.Check(tab.TrustedAssetsCalls, Equals, 2) - _, err = obs.Observe(gadget.ContentUpdate, mockRunBootStruct, root, "asset", + 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) - _, err = obs.Observe(gadget.ContentUpdate, mockRunBootStruct, root, "shim", + 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) - // the list of trusted assets was asked once for the boot bootloader - c.Check(tab.TrustedAssetsCalls, Equals, 1) + c.Check(res, Equals, gadget.ChangeApply) // observe the recovery struct - _, err = obs.Observe(gadget.ContentUpdate, mockSeedStruct, root, "shim", + res, err = obs.Observe(gadget.ContentUpdate, mockSeedStruct, root, "shim", &gadget.ContentChange{After: filepath.Join(d, "shim")}) c.Assert(err, IsNil) - _, err = obs.Observe(gadget.ContentUpdate, mockSeedStruct, root, "asset", + 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) - _, err = obs.Observe(gadget.ContentUpdate, mockSeedStruct, root, "nested/other-asset", + 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) - // and once again for the recovery bootloader - c.Check(tab.TrustedAssetsCalls, Equals, 2) + 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)), @@ -741,9 +773,15 @@ "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.MockSecbootResealKey(func(params *secboot.ResealKeyParams) error { + restore := boot.MockSecbootResealKeys(func(params *secboot.ResealKeysParams) error { resealCalls++ return nil }) @@ -762,6 +800,10 @@ "asset", "shim", }) + tab.ManagedAssetsList = []string{ + "managed-asset", + "nested/managed-asset", + } data := []byte("foobar") // SHA3-384 @@ -798,18 +840,21 @@ c.Assert(err, IsNil) // we get an observer for UC20 - obs, _ := s.uc20UpdateObserver(c) + obs, _ := s.uc20UpdateObserverEncryptedSystemMockedBootloader(c) // observe the updates - _, err = obs.Observe(gadget.ContentUpdate, mockRunBootStruct, root, "asset", + res, err := obs.Observe(gadget.ContentUpdate, mockRunBootStruct, root, "asset", &gadget.ContentChange{After: filepath.Join(d, "foobar")}) c.Assert(err, IsNil) - _, err = obs.Observe(gadget.ContentUpdate, mockSeedStruct, root, "asset", + 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) - _, err = obs.Observe(gadget.ContentUpdate, mockSeedStruct, root, "shim", + 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 @@ -829,14 +874,25 @@ "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.MockSecbootResealKey(func(params *secboot.ResealKeyParams) error { + 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) @@ -864,15 +920,17 @@ c.Assert(err, IsNil) // we get an observer for UC20 - obs, _ := s.uc20UpdateObserver(c) + obs, _ := s.uc20UpdateObserverEncryptedSystemMockedBootloader(c) // observe the updates - _, err = obs.Observe(gadget.ContentUpdate, mockRunBootStruct, root, "asset", + res, err := obs.Observe(gadget.ContentUpdate, mockRunBootStruct, root, "asset", &gadget.ContentChange{After: filepath.Join(d, "foobar")}) c.Assert(err, IsNil) - _, err = obs.Observe(gadget.ContentUpdate, mockSeedStruct, root, "asset", + 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 @@ -907,7 +965,9 @@ // modeenv is not set up, but the observer should not care // we get an observer for UC20 - obs, _ := s.uc20UpdateObserver(c) + 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{ @@ -917,60 +977,28 @@ } // observe the updates - _, err := obs.Observe(gadget.ContentUpdate, mockVolumeStruct, root, "asset", + res, err := obs.Observe(gadget.ContentUpdate, mockVolumeStruct, root, "asset", &gadget.ContentChange{After: filepath.Join(d, "foobar")}) c.Assert(err, IsNil) - // trusted assets were asked for - c.Check(tab.TrustedAssetsCalls, Equals, 0) -} - -func (s *assetsSuite) TestUpdateObserverUpdateNotTrustedMocked(c *C) { - d := c.MkDir() - root := c.MkDir() - - // mot a non trusted assets bootloader - bl := bootloadertest.Mock("not-trusted", "") - bootloader.Force(bl) - defer bootloader.Force(nil) - - err := ioutil.WriteFile(filepath.Join(d, "foobar"), nil, 0644) - c.Assert(err, IsNil) - - // no need to mock modeenv, the bootloader has no trusted assets - - // we get an observer for UC20 - obs, _ := s.uc20UpdateObserver(c) - - // observe the updates - _, err = obs.Observe(gadget.ContentUpdate, mockRunBootStruct, root, "asset", - &gadget.ContentChange{After: filepath.Join(d, "foobar")}) - c.Assert(err, IsNil) - _, err = obs.Observe(gadget.ContentUpdate, mockSeedStruct, root, "asset", - &gadget.ContentChange{After: filepath.Join(d, "foobar")}) - c.Assert(err, IsNil) - - // reseal is a noop - err = obs.BeforeWrite() - 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() - obs, _ := s.uc20UpdateObserver(c) + uc20Model := boottest.MakeMockUC20Model() // first no bootloader bootloader.ForceError(fmt.Errorf("bootloader fail")) - _, err := obs.Observe(gadget.ContentUpdate, mockRunBootStruct, root, "asset", - &gadget.ContentChange{After: filepath.Join(d, "foobar")}) - - c.Assert(err, ErrorMatches, "cannot find bootloader: bootloader fail") - _, err = obs.Observe(gadget.ContentUpdate, mockSeedStruct, root, "asset", - &gadget.ContentChange{After: filepath.Join(d, "foobar")}) + obs, err := boot.TrustedAssetsUpdateObserverForModel(uc20Model, gadgetDir) + c.Assert(obs, IsNil) c.Assert(err, ErrorMatches, "cannot find bootloader: bootloader fail") bootloader.ForceError(nil) @@ -978,26 +1006,32 @@ bootloader.Force(bl) defer bootloader.Force(nil) - bl.TrustedAssetsList = []string{"asset"} bl.TrustedAssetsErr = fmt.Errorf("fail") - - // listing trusted assets fails - _, err = obs.Observe(gadget.ContentUpdate, mockRunBootStruct, root, "asset", - &gadget.ContentChange{After: filepath.Join(d, "foobar")}) - c.Assert(err, ErrorMatches, `cannot list "trusted" bootloader trusted assets: fail`) - _, err = obs.Observe(gadget.ContentUpdate, mockSeedStruct, root, "asset", - &gadget.ContentChange{After: filepath.Join(d, "foobar")}) + 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) - bl.TrustedAssetsErr = nil + // 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 - _, err = obs.Observe(gadget.ContentUpdate, mockRunBootStruct, root, "asset", + 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`) - _, err = obs.Observe(gadget.ContentUpdate, mockSeedStruct, root, "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 load modeenv: .* no such file or directory`) + c.Check(res, Equals, gadget.ChangeAbort) m := boot.Modeenv{ Mode: "run", @@ -1006,15 +1040,18 @@ c.Assert(err, IsNil) // no source file, hash will fail - _, err = obs.Observe(gadget.ContentUpdate, mockRunBootStruct, root, "asset", + 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`) - _, err = obs.Observe(gadget.ContentUpdate, mockRunBootStruct, root, "asset", + 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`) - _, err = obs.Observe(gadget.ContentUpdate, mockSeedStruct, root, "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 open asset file: .*/foobar: no such file or directory`) + c.Check(res, Equals, gadget.ChangeAbort) } func (s *assetsSuite) TestUpdateObserverUpdateRepeatedAssetErr(c *C) { @@ -1026,7 +1063,7 @@ defer bootloader.Force(nil) bl.TrustedAssetsList = []string{"asset"} - obs, _ := s.uc20UpdateObserver(c) + obs, _ := s.uc20UpdateObserverEncryptedSystemMockedBootloader(c) // we are already tracking 2 assets, this is an unexpected state for observing content updates m := boot.Modeenv{ @@ -1045,12 +1082,14 @@ err = ioutil.WriteFile(filepath.Join(d, "foobar"), nil, 0644) c.Assert(err, IsNil) - _, err = obs.Observe(gadget.ContentUpdate, mockRunBootStruct, root, "asset", + 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"`) - _, err = obs.Observe(gadget.ContentUpdate, mockSeedStruct, root, "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) { @@ -1101,22 +1140,24 @@ }) // we get an observer for UC20 - obs, _ := s.uc20UpdateObserver(c) + obs, _ := s.uc20UpdateObserverEncryptedSystemMockedBootloader(c) - _, err = obs.Observe(gadget.ContentUpdate, mockRunBootStruct, root, "asset", + 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) - _, err = obs.Observe(gadget.ContentUpdate, mockSeedStruct, root, "asset", + 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{ @@ -1182,7 +1223,9 @@ } // we get an observer for UC20 - obs, _ := s.uc20UpdateObserver(c) + obs, _ := s.uc20UpdateObserverEncryptedSystemMockedBootloader(c) + // the list of trusted assets is obtained upfront + c.Check(tab.TrustedAssetsCalls, Equals, 2) m := boot.Modeenv{ Mode: "run", @@ -1202,40 +1245,41 @@ err := m.WriteTo("") c.Assert(err, IsNil) - _, err = obs.Observe(gadget.ContentRollback, mockRunBootStruct, root, "asset", + 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) - _, err = obs.Observe(gadget.ContentRollback, mockRunBootStruct, root, "shim", + 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) - // the list of trusted assets was asked once for the boot bootloader - c.Check(tab.TrustedAssetsCalls, Equals, 1) + c.Check(res, Equals, gadget.ChangeApply) // observe the recovery struct - _, err = obs.Observe(gadget.ContentRollback, mockSeedStruct, rootSeed, "shim", + 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) - _, err = obs.Observe(gadget.ContentRollback, mockSeedStruct, rootSeed, "asset", + 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) - _, err = obs.Observe(gadget.ContentRollback, mockSeedStruct, rootSeed, "nested/other-asset", + 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) - // and once again for the recovery bootloader - c.Check(tab.TrustedAssetsCalls, Equals, 2) + 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)), @@ -1259,7 +1303,9 @@ tab := s.bootloaderWithTrustedAssets(c, []string{"asset"}) // we get an observer for UC20 - obs, _ := s.uc20UpdateObserver(c) + 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{ @@ -1276,17 +1322,15 @@ err := m.WriteTo("") c.Assert(err, IsNil) // file does not exist on disk - _, err = obs.Observe(gadget.ContentRollback, mockRunBootStruct, root, "asset", + res, err := obs.Observe(gadget.ContentRollback, mockRunBootStruct, root, "asset", &gadget.ContentChange{}) c.Assert(err, IsNil) - // the list of trusted assets was asked once for the boot bootloader - c.Check(tab.TrustedAssetsCalls, Equals, 1) + c.Check(res, Equals, gadget.ChangeApply) // observe the recovery struct - _, err = obs.Observe(gadget.ContentRollback, mockSeedStruct, root, "asset", + res, err = obs.Observe(gadget.ContentRollback, mockSeedStruct, root, "asset", &gadget.ContentChange{}) c.Assert(err, IsNil) - // and once again for the recovery bootloader - c.Check(tab.TrustedAssetsCalls, Equals, 2) + c.Check(res, Equals, gadget.ChangeApply) // check modeenv newM, err := boot.ReadModeenv("") c.Assert(err, IsNil) @@ -1294,7 +1338,7 @@ c.Check(newM.CurrentTrustedRecoveryBootAssets, HasLen, 0) // new observer - obs, _ = s.uc20UpdateObserver(c) + obs, _ = s.uc20UpdateObserverEncryptedSystemMockedBootloader(c) m = boot.Modeenv{ Mode: "run", CurrentTrustedBootAssets: boot.BootAssetsMap{ @@ -1309,23 +1353,27 @@ err = m.WriteTo("") c.Assert(err, IsNil) // again, file does not exist on disk, but we expected it to be there - _, err = obs.Observe(gadget.ContentRollback, mockRunBootStruct, root, "asset", + res, err = obs.Observe(gadget.ContentRollback, mockRunBootStruct, root, "asset", &gadget.ContentChange{}) c.Assert(err, ErrorMatches, `tracked asset "asset" is unexpectedly missing from disk`) - _, err = obs.Observe(gadget.ContentRollback, mockSeedStruct, root, "asset", + 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 - _, err = obs.Observe(gadget.ContentRollback, mockRunBootStruct, root, "asset", + res, err = obs.Observe(gadget.ContentRollback, mockRunBootStruct, root, "asset", &gadget.ContentChange{}) c.Assert(err, ErrorMatches, `unexpected content of existing asset "asset"`) - _, err = obs.Observe(gadget.ContentRollback, mockSeedStruct, root, "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) { @@ -1335,8 +1383,12 @@ 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 - obs, _ := s.uc20UpdateObserver(c) + s.stampSealedKeys(c, dirs.GlobalRootDir) + obs, _ := s.uc20UpdateObserver(c, gadgetDir) cache := boot.NewTrustedAssetsCache(dirs.SnapBootAssetsDir) @@ -1371,6 +1423,7 @@ {"grubx64.efi", "new grub efi"}, // SHA3-384: cc0663cc7e6c7ada990261c3ff1d72da001dc02451558716422d3d2443b8789463363c9ff0cd1b853c6ced3e8e7dc39d {"bootx64.efi", "new recovery shim efi"}, + {"grub.conf", "grub from gadget"}, }, }, // just the markers @@ -1422,15 +1475,28 @@ c.Assert(err, IsNil) // updates first - _, err = obs.Observe(gadget.ContentUpdate, mockRunBootStruct, bootDir, "EFI/boot/grubx64.efi", + res, err := obs.Observe(gadget.ContentUpdate, mockRunBootStruct, bootDir, "EFI/boot/grubx64.efi", &gadget.ContentChange{After: filepath.Join(gadgetDir, "grubx64.efi")}) c.Assert(err, IsNil) - _, err = obs.Observe(gadget.ContentUpdate, mockSeedStruct, seedDir, "EFI/boot/grubx64.efi", + 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) - _, err = obs.Observe(gadget.ContentUpdate, mockSeedStruct, seedDir, "EFI/boot/bootx64.efi", + 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 @@ -1474,15 +1540,18 @@ // hiya, update failed, pretend we do a rollback, files on disk are as // if they were restored - _, err = obs.Observe(gadget.ContentRollback, mockRunBootStruct, bootDir, "EFI/boot/grubx64.efi", + res, err = obs.Observe(gadget.ContentRollback, mockRunBootStruct, bootDir, "EFI/boot/grubx64.efi", &gadget.ContentChange{}) c.Assert(err, IsNil) - _, err = obs.Observe(gadget.ContentRollback, mockSeedStruct, seedDir, "EFI/boot/grubx64.efi", + c.Check(res, Equals, gadget.ChangeApply) + res, err = obs.Observe(gadget.ContentRollback, mockSeedStruct, seedDir, "EFI/boot/grubx64.efi", &gadget.ContentChange{}) c.Assert(err, IsNil) - _, err = obs.Observe(gadget.ContentRollback, mockSeedStruct, seedDir, "EFI/boot/bootx64.efi", + 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("") @@ -1524,7 +1593,7 @@ s.bootloaderWithTrustedAssets(c, []string{"asset", "shim"}) // we get an observer for UC20 - obs, _ := s.uc20UpdateObserver(c) + obs, _ := s.uc20UpdateObserverEncryptedSystemMockedBootloader(c) data := []byte("foobar") // SHA3-384 @@ -1536,19 +1605,23 @@ err = ioutil.WriteFile(filepath.Join(d, "shim"), shim, 0644) c.Assert(err, IsNil) - _, err = obs.Observe(gadget.ContentUpdate, mockRunBootStruct, root, "asset", + res, err := obs.Observe(gadget.ContentUpdate, mockRunBootStruct, root, "asset", &gadget.ContentChange{After: filepath.Join(d, "foobar")}) c.Assert(err, IsNil) - _, err = obs.Observe(gadget.ContentUpdate, mockRunBootStruct, root, "shim", + 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 - _, err = obs.Observe(gadget.ContentUpdate, mockSeedStruct, root, "shim", + res, err = obs.Observe(gadget.ContentUpdate, mockSeedStruct, root, "shim", &gadget.ContentChange{After: filepath.Join(d, "shim")}) c.Assert(err, IsNil) - _, err = obs.Observe(gadget.ContentUpdate, mockSeedStruct, root, "asset", + 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)), @@ -1569,7 +1642,7 @@ "shim": {shimHash}, }) resealCalls := 0 - restore := boot.MockSecbootResealKey(func(params *secboot.ResealKeyParams) error { + restore := boot.MockSecbootResealKeys(func(params *secboot.ResealKeysParams) error { resealCalls++ return nil }) @@ -1623,7 +1696,7 @@ } // we get an observer for UC20 - obs, _ := s.uc20UpdateObserver(c) + obs, _ := s.uc20UpdateObserverEncryptedSystemMockedBootloader(c) m := boot.Modeenv{ Mode: "run", @@ -1638,17 +1711,20 @@ err = m.WriteTo("") c.Assert(err, IsNil) - _, err = obs.Observe(gadget.ContentUpdate, mockRunBootStruct, root, "asset", + res, err := obs.Observe(gadget.ContentUpdate, mockRunBootStruct, root, "asset", &gadget.ContentChange{After: filepath.Join(d, "foobar")}) c.Assert(err, IsNil) - _, err = obs.Observe(gadget.ContentUpdate, mockRunBootStruct, root, "shim", + 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 - _, err = obs.Observe(gadget.ContentUpdate, mockSeedStruct, root, "asset", + 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)), @@ -1717,10 +1793,10 @@ s.bootloaderWithTrustedAssets(c, []string{"asset", "shim"}) // we get an observer for UC20 - obs, _ := s.uc20UpdateObserver(c) + obs, _ := s.uc20UpdateObserverEncryptedSystemMockedBootloader(c) resealCalls := 0 - restore := boot.MockSecbootResealKey(func(params *secboot.ResealKeyParams) error { + restore := boot.MockSecbootResealKeys(func(params *secboot.ResealKeysParams) error { resealCalls++ return nil }) @@ -1746,9 +1822,10 @@ err = ioutil.WriteFile(filepath.Join(d, "shim"), []byte("shim"), 0644) c.Assert(err, IsNil) // observe only recovery bootloader update, no action for run bootloader - _, err = obs.Observe(gadget.ContentUpdate, mockSeedStruct, root, "shim", + 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) @@ -1775,15 +1852,16 @@ s.bootloaderWithTrustedAssets(c, []string{"asset", "shim"}) // we get an observer for UC20 - obs, _ := s.uc20UpdateObserver(c) + 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 - _, err = obs.Observe(gadget.ContentUpdate, mockSeedStruct, root, "shim", + 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() @@ -1794,10 +1872,11 @@ c.Check(afterCancelM.CurrentTrustedRecoveryBootAssets, HasLen, 0) // get a new observer, and observe an update for run bootloader asset only - obs, _ = s.uc20UpdateObserver(c) - _, err = obs.Observe(gadget.ContentUpdate, mockRunBootStruct, root, "shim", + 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) @@ -1827,17 +1906,19 @@ s.bootloaderWithTrustedAssets(c, []string{"asset", "shim"}) // we get an observer for UC20 - obs, _ := s.uc20UpdateObserver(c) + obs, _ := s.uc20UpdateObserverEncryptedSystemMockedBootloader(c) // trigger loading modeenv and bootloader information err = ioutil.WriteFile(filepath.Join(d, "shim"), []byte("shim"), 0644) c.Assert(err, IsNil) - _, err = obs.Observe(gadget.ContentUpdate, mockSeedStruct, root, "shim", + res, err := obs.Observe(gadget.ContentUpdate, mockSeedStruct, root, "shim", &gadget.ContentChange{After: filepath.Join(d, "shim")}) c.Assert(err, IsNil) - _, err = obs.Observe(gadget.ContentUpdate, mockRunBootStruct, root, "shim", + 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 @@ -1861,6 +1942,10 @@ // 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() @@ -1891,18 +1976,20 @@ s.bootloaderWithTrustedAssets(c, []string{"asset", "shim"}) // we get an observer for UC20 - obs, _ := s.uc20UpdateObserver(c) + obs, _ := s.uc20UpdateObserverEncryptedSystemMockedBootloader(c) shim := []byte("shim") shimHash := "dac0063e831d4b2e7a330426720512fc50fa315042f0bb30f9d1db73e4898dcb89119cac41fdfa62137c8931a50f9d7b" err = ioutil.WriteFile(filepath.Join(d, "shim"), shim, 0644) c.Assert(err, IsNil) - _, err = obs.Observe(gadget.ContentUpdate, mockSeedStruct, root, "shim", + res, err := obs.Observe(gadget.ContentUpdate, mockSeedStruct, root, "shim", &gadget.ContentChange{After: filepath.Join(d, "shim")}) c.Assert(err, IsNil) - _, err = obs.Observe(gadget.ContentUpdate, mockRunBootStruct, root, "shim", + 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"), @@ -2191,6 +2278,10 @@ 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") @@ -2263,6 +2354,11 @@ 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") @@ -2342,29 +2438,33 @@ }) // we get an observer for UC20 - obs, uc20model := s.uc20UpdateObserver(c) + obs, uc20model := s.uc20UpdateObserverEncryptedSystemMockedBootloader(c) - _, err = obs.Observe(gadget.ContentUpdate, mockRunBootStruct, root, "asset", + 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) - _, err = obs.Observe(gadget.ContentUpdate, mockRunBootStruct, root, "shim", + 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 - _, err = obs.Observe(gadget.ContentUpdate, mockSeedStruct, root, "shim", + res, err = obs.Observe(gadget.ContentUpdate, mockSeedStruct, root, "shim", &gadget.ContentChange{After: filepath.Join(d, "shim")}) c.Assert(err, IsNil) - _, err = obs.Observe(gadget.ContentUpdate, mockSeedStruct, root, "asset", + 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)), @@ -2403,7 +2503,7 @@ runKernelBf, } - restore = boot.MockSecbootResealKey(func(params *secboot.ResealKeyParams) error { + restore = boot.MockSecbootResealKeys(func(params *secboot.ResealKeysParams) error { resealCalls++ c.Assert(params.ModelParams, HasLen, 1) @@ -2412,25 +2512,38 @@ for _, ch := range mp.EFILoadChains { printChain(c, ch, "-") } - 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))), - }) + 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, 1) + c.Check(resealCalls, Equals, 2) } func (s *assetsSuite) TestUpdateObserverCanceledReseal(c *C) { @@ -2469,7 +2582,7 @@ tab := s.bootloaderWithTrustedAssets(c, []string{"asset", "shim"}) // we get an observer for UC20 - obs, uc20model := s.uc20UpdateObserver(c) + obs, uc20model := s.uc20UpdateObserverEncryptedSystemMockedBootloader(c) data := []byte("foobar") err = ioutil.WriteFile(filepath.Join(d, "foobar"), data, 0644) @@ -2479,19 +2592,23 @@ c.Assert(err, IsNil) // trigger a bunch of updates, so that we have things to cancel - _, err = obs.Observe(gadget.ContentUpdate, mockRunBootStruct, root, "asset", + res, err := obs.Observe(gadget.ContentUpdate, mockRunBootStruct, root, "asset", &gadget.ContentChange{After: filepath.Join(d, "foobar")}) c.Assert(err, IsNil) - _, err = obs.Observe(gadget.ContentUpdate, mockRunBootStruct, root, "shim", + 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 - _, err = obs.Observe(gadget.ContentUpdate, mockSeedStruct, root, "shim", + res, err = obs.Observe(gadget.ContentUpdate, mockSeedStruct, root, "shim", &gadget.ContentChange{After: filepath.Join(d, "shim")}) c.Assert(err, IsNil) - _, err = obs.Observe(gadget.ContentUpdate, mockSeedStruct, root, "asset", + 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{ @@ -2521,7 +2638,7 @@ } resealCalls := 0 - restore = boot.MockSecbootResealKey(func(params *secboot.ResealKeyParams) error { + restore = boot.MockSecbootResealKeys(func(params *secboot.ResealKeysParams) error { resealCalls++ c.Assert(params.ModelParams, HasLen, 1) mp := params.ModelParams[0] @@ -2529,14 +2646,25 @@ for _, ch := range mp.EFILoadChains { printChain(c, ch, "-") } - 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))), - }) + 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() @@ -2556,5 +2684,82 @@ filepath.Join(dirs.SnapBootAssetsDir, "trusted", "shim-shimhash"), }) - c.Check(resealCalls, Equals, 1) + 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.47.1+20.10.1build1/boot/bootchain_test.go snapd-2.48+21.04/boot/bootchain_test.go --- snapd-2.47.1+20.10.1build1/boot/bootchain_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/boot/bootchain_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -1146,6 +1146,10 @@ } 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", diff -Nru snapd-2.47.1+20.10.1build1/boot/boot_test.go snapd-2.48+21.04/boot/boot_test.go --- snapd-2.47.1+20.10.1build1/boot/boot_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/boot/boot_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -879,7 +879,7 @@ defer r() resealCalls := 0 - restore := boot.MockSecbootResealKey(func(params *secboot.ResealKeyParams) error { + restore := boot.MockSecbootResealKeys(func(params *secboot.ResealKeysParams) error { resealCalls++ c.Assert(params.ModelParams, HasLen, 1) @@ -991,7 +991,7 @@ defer r() resealCalls := 0 - restore := boot.MockSecbootResealKey(func(params *secboot.ResealKeyParams) error { + restore := boot.MockSecbootResealKeys(func(params *secboot.ResealKeysParams) error { resealCalls++ c.Assert(params.ModelParams, HasLen, 1) @@ -1099,9 +1099,9 @@ defer r() resealCalls := 0 - restore := boot.MockSecbootResealKey(func(params *secboot.ResealKeyParams) error { + restore := boot.MockSecbootResealKeys(func(params *secboot.ResealKeysParams) error { resealCalls++ - return nil + return fmt.Errorf("unexpected call") }) defer restore() @@ -1111,8 +1111,25 @@ c.Assert(bootKern.IsTrivial(), Equals, false) // write boot-chains for current state that will stay unchanged - bootChainsData := `{"boot-chains":[{"brand-id":"my-brand","model":"my-model-uc20","grade":"dangerous","model-sign-key-id":"Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij","asset-chain":[{"role":"run-mode","name":"asset","hashes":["0fa8abfbdaf924ad307b74dd2ed183b9a4a398891a2f6bac8fd2db7041b77f068580f9c6c66f699b496c2da1cbcc7ed8"]}],"kernel":"pc-kernel","kernel-revision":"1","kernel-cmdlines":[""]}]}` - err := ioutil.WriteFile(filepath.Join(dirs.SnapFDEDir, "boot-chains"), []byte(bootChainsData), 0600) + bootChains := []boot.BootChain{{ + BrandID: "my-brand", + Model: "my-model-uc20", + Grade: "dangerous", + ModelSignKeyID: "Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij", + AssetChain: []boot.BootAsset{ + { + Role: bootloader.RoleRunMode, + Name: "asset", + Hashes: []string{ + "0fa8abfbdaf924ad307b74dd2ed183b9a4a398891a2f6bac8fd2db7041b77f068580f9c6c66f699b496c2da1cbcc7ed8", + }, + }, + }, + Kernel: "pc-kernel", + KernelRevision: "1", + KernelCmdlines: []string{"snapd_recovery_mode=run"}, + }} + err := boot.WriteBootChains(bootChains, filepath.Join(dirs.SnapFDEDir, "boot-chains"), 0) c.Assert(err, IsNil) // make the kernel used on next boot @@ -1197,9 +1214,9 @@ defer r() resealCalls := 0 - restore := boot.MockSecbootResealKey(func(params *secboot.ResealKeyParams) error { + restore := boot.MockSecbootResealKeys(func(params *secboot.ResealKeysParams) error { resealCalls++ - return nil + return fmt.Errorf("unexpected call") }) defer restore() @@ -1209,8 +1226,25 @@ c.Assert(bootKern.IsTrivial(), Equals, false) // write boot-chains for current state that will stay unchanged - bootChainsData := `{"boot-chains":[{"brand-id":"my-brand","model":"my-model-uc20","grade":"dangerous","model-sign-key-id":"Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij","asset-chain":[{"role":"run-mode","name":"asset","hashes":["0fa8abfbdaf924ad307b74dd2ed183b9a4a398891a2f6bac8fd2db7041b77f068580f9c6c66f699b496c2da1cbcc7ed8"]}],"kernel":"pc-kernel","kernel-revision":"","kernel-cmdlines":[""]}]}` - err := ioutil.WriteFile(filepath.Join(dirs.SnapFDEDir, "boot-chains"), []byte(bootChainsData), 0600) + bootChains := []boot.BootChain{{ + BrandID: "my-brand", + Model: "my-model-uc20", + Grade: "dangerous", + ModelSignKeyID: "Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij", + AssetChain: []boot.BootAsset{ + { + Role: bootloader.RoleRunMode, + Name: "asset", + Hashes: []string{ + "0fa8abfbdaf924ad307b74dd2ed183b9a4a398891a2f6bac8fd2db7041b77f068580f9c6c66f699b496c2da1cbcc7ed8", + }, + }, + }, + Kernel: "pc-kernel", + KernelRevision: "", + KernelCmdlines: []string{"snapd_recovery_mode=run"}, + }} + err := boot.WriteBootChains(bootChains, filepath.Join(dirs.SnapFDEDir, "boot-chains"), 0) c.Assert(err, IsNil) // make the kernel used on next boot @@ -1506,7 +1540,7 @@ c.Assert(coreDev.HasModeenv(), Equals, true) resealCalls := 0 - restore := boot.MockSecbootResealKey(func(params *secboot.ResealKeyParams) error { + restore := boot.MockSecbootResealKeys(func(params *secboot.ResealKeysParams) error { resealCalls++ return nil }) @@ -1850,7 +1884,7 @@ c.Assert(coreDev.HasModeenv(), Equals, true) resealCalls := 0 - restore := boot.MockSecbootResealKey(func(params *secboot.ResealKeyParams) error { + restore := boot.MockSecbootResealKeys(func(params *secboot.ResealKeysParams) error { resealCalls++ c.Assert(params.ModelParams, HasLen, 1) @@ -2081,7 +2115,7 @@ c.Assert(coreDev.HasModeenv(), Equals, true) resealCalls := 0 - restore = boot.MockSecbootResealKey(func(params *secboot.ResealKeyParams) error { + restore = boot.MockSecbootResealKeys(func(params *secboot.ResealKeysParams) error { resealCalls++ c.Assert(params.ModelParams, HasLen, 1) @@ -2090,14 +2124,25 @@ for _, ch := range mp.EFILoadChains { printChain(c, ch, "-") } - 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))), - }) + 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() @@ -2122,7 +2167,7 @@ filepath.Join(dirs.SnapBootAssetsDir, "trusted", "asset-"+dataHash), filepath.Join(dirs.SnapBootAssetsDir, "trusted", "shim-"+shimHash), }) - c.Check(resealCalls, Equals, 1) + c.Check(resealCalls, Equals, 2) } func (s *bootenv20Suite) TestMarkBootSuccessful20BootAssetsStableStateHappy(c *C) { @@ -2172,10 +2217,10 @@ 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", + Path: "/var/lib/snapd/seed/snaps/pc-kernel-recovery_1.snap", SideInfo: &snap.SideInfo{ Revision: snap.Revision{N: 1}, - RealName: "pc-linux", + RealName: "pc-kernel-recovery", }, } return uc20Model, []*seed.Snap{kernelSnap}, nil @@ -2211,15 +2256,57 @@ c.Assert(coreDev.HasModeenv(), Equals, true) resealCalls := 0 - restore = boot.MockSecbootResealKey(func(params *secboot.ResealKeyParams) error { + restore = boot.MockSecbootResealKeys(func(params *secboot.ResealKeysParams) error { resealCalls++ return nil }) defer restore() // write boot-chains for current state that will stay unchanged - bootChainsData := `{"boot-chains":[{"brand-id":"my-brand","model":"my-model-uc20","grade":"dangerous","model-sign-key-id":"Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij","asset-chain":[{"role":"recovery","name":"shim","hashes":["dac0063e831d4b2e7a330426720512fc50fa315042f0bb30f9d1db73e4898dcb89119cac41fdfa62137c8931a50f9d7b"]},{"role":"recovery","name":"asset","hashes":["0fa8abfbdaf924ad307b74dd2ed183b9a4a398891a2f6bac8fd2db7041b77f068580f9c6c66f699b496c2da1cbcc7ed8"]}],"kernel":"pc-kernel","kernel-revision":"1","kernel-cmdlines":[""]},{"brand-id":"my-brand","model":"my-model-uc20","grade":"dangerous","model-sign-key-id":"Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij","asset-chain":[{"role":"recovery","name":"shim","hashes":["dac0063e831d4b2e7a330426720512fc50fa315042f0bb30f9d1db73e4898dcb89119cac41fdfa62137c8931a50f9d7b"]},{"role":"recovery","name":"asset","hashes":["0fa8abfbdaf924ad307b74dd2ed183b9a4a398891a2f6bac8fd2db7041b77f068580f9c6c66f699b496c2da1cbcc7ed8"]}],"kernel":"pc-linux","kernel-revision":"1","kernel-cmdlines":[""]}]}` - err := ioutil.WriteFile(filepath.Join(dirs.SnapFDEDir, "boot-chains"), []byte(bootChainsData), 0600) + 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 @@ -2292,10 +2379,10 @@ 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", + Path: "/var/lib/snapd/seed/snaps/pc-kernel-recovery_1.snap", SideInfo: &snap.SideInfo{ Revision: snap.Revision{N: 1}, - RealName: "pc-linux", + RealName: "pc-kernel-recovery", }, } return uc20Model, []*seed.Snap{kernelSnap}, nil @@ -2331,15 +2418,58 @@ c.Assert(coreDev.HasModeenv(), Equals, true) resealCalls := 0 - restore = boot.MockSecbootResealKey(func(params *secboot.ResealKeyParams) error { + restore = boot.MockSecbootResealKeys(func(params *secboot.ResealKeysParams) error { resealCalls++ return nil }) defer restore() // write boot-chains for current state that will stay unchanged - bootChainsData := `{"boot-chains":[{"brand-id":"my-brand","model":"my-model-uc20","grade":"dangerous","model-sign-key-id":"Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij","asset-chain":[{"role":"recovery","name":"shim","hashes":["dac0063e831d4b2e7a330426720512fc50fa315042f0bb30f9d1db73e4898dcb89119cac41fdfa62137c8931a50f9d7b"]},{"role":"recovery","name":"asset","hashes":["0fa8abfbdaf924ad307b74dd2ed183b9a4a398891a2f6bac8fd2db7041b77f068580f9c6c66f699b496c2da1cbcc7ed8"]}],"kernel":"pc-kernel","kernel-revision":"","kernel-cmdlines":[""]},{"brand-id":"my-brand","model":"my-model-uc20","grade":"dangerous","model-sign-key-id":"Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij","asset-chain":[{"role":"recovery","name":"shim","hashes":["dac0063e831d4b2e7a330426720512fc50fa315042f0bb30f9d1db73e4898dcb89119cac41fdfa62137c8931a50f9d7b"]},{"role":"recovery","name":"asset","hashes":["0fa8abfbdaf924ad307b74dd2ed183b9a4a398891a2f6bac8fd2db7041b77f068580f9c6c66f699b496c2da1cbcc7ed8"]}],"kernel":"pc-linux","kernel-revision":"1","kernel-cmdlines":[""]}]}` - err := ioutil.WriteFile(filepath.Join(dirs.SnapFDEDir, "boot-chains"), []byte(bootChainsData), 0600) + 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 diff -Nru snapd-2.47.1+20.10.1build1/boot/cmdline.go snapd-2.48+21.04/boot/cmdline.go --- snapd-2.47.1+20.10.1build1/boot/cmdline.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/boot/cmdline.go 2020-11-19 16:51:02.000000000 +0000 @@ -20,16 +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" ) @@ -45,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 != "": @@ -92,34 +82,14 @@ 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) - if err != nil { - return "", "", err - } - return whichModeAndRecoverySystem(cmdline) -} - -// 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 - } -} - var errBootConfigNotManaged = errors.New("boot config is not managed") -func getBootloaderManagingItsAssets(where string, opts *bootloader.Options) (bootloader.ManagedAssetsBootloader, error) { +func getBootloaderManagingItsAssets(where string, opts *bootloader.Options) (bootloader.TrustedAssetsBootloader, error) { bl, err := bootloader.Find(where, opts) if err != nil { - return nil, fmt.Errorf("internal error: cannot find managed assets bootloader under %q: %v", where, err) + return nil, fmt.Errorf("internal error: cannot find trusted assets bootloader under %q: %v", where, err) } - mbl, ok := bl.(bootloader.ManagedAssetsBootloader) + mbl, ok := bl.(bootloader.TrustedAssetsBootloader) if !ok { // the bootloader cannot manage its scripts return nil, errBootConfigNotManaged diff -Nru snapd-2.47.1+20.10.1build1/boot/cmdline_test.go snapd-2.48+21.04/boot/cmdline_test.go --- snapd-2.47.1+20.10.1build1/boot/cmdline_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/boot/cmdline_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -30,6 +30,7 @@ "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" ) @@ -47,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) } @@ -85,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) @@ -122,13 +129,9 @@ c.Assert(err, IsNil) c.Assert(cmdline, Equals, "") - mbl := bl.WithManagedAssets() - bootloader.Force(mbl) - mbl.IsManaged = false - - // TODO:UC20: remove is managed checks + tbl := bl.WithTrustedAssets() + bootloader.Force(tbl) - // is-managed is ignored with the right model and bootloader interface cmdline, err = boot.ComposeRecoveryCommandLine(model, "20200314") c.Assert(err, IsNil) c.Assert(cmdline, Equals, "snapd_recovery_mode=recover snapd_recovery_system=20200314") @@ -156,12 +159,11 @@ func (s *kernelCommandLineSuite) TestComposeCommandLineManagedHappy(c *C) { model := boottest.MakeMockUC20Model() - mbl := bootloadertest.Mock("btloader", c.MkDir()).WithManagedAssets() - bootloader.Force(mbl) + tbl := bootloadertest.Mock("btloader", c.MkDir()).WithTrustedAssets() + bootloader.Force(tbl) defer bootloader.Force(nil) - mbl.IsManaged = true - mbl.StaticCommandLine = "panic=-1" + tbl.StaticCommandLine = "panic=-1" cmdline, err := boot.ComposeRecoveryCommandLine(model, "20200314") c.Assert(err, IsNil) @@ -170,9 +172,6 @@ c.Assert(err, IsNil) c.Assert(cmdline, Equals, "snapd_recovery_mode=run panic=-1") - // managed status is effectively ignored - mbl.IsManaged = false - cmdline, err = boot.ComposeRecoveryCommandLine(model, "20200314") c.Assert(err, IsNil) c.Assert(cmdline, Equals, "snapd_recovery_mode=recover snapd_recovery_system=20200314 panic=-1") @@ -184,20 +183,18 @@ func (s *kernelCommandLineSuite) TestComposeCandidateCommandLineManagedHappy(c *C) { model := boottest.MakeMockUC20Model() - mbl := bootloadertest.Mock("btloader", c.MkDir()).WithManagedAssets() - bootloader.Force(mbl) + tbl := bootloadertest.Mock("btloader", c.MkDir()).WithTrustedAssets() + bootloader.Force(tbl) defer bootloader.Force(nil) - mbl.IsManaged = true - mbl.StaticCommandLine = "panic=-1" - mbl.CandidateStaticCommandLine = "candidate panic=-1" + 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 - mbl.IsManaged = false cmdline, err = boot.ComposeCandidateCommandLine(model) c.Assert(err, IsNil) c.Assert(cmdline, Equals, "snapd_recovery_mode=run candidate panic=-1") diff -Nru snapd-2.47.1+20.10.1build1/boot/debug.go snapd-2.48+21.04/boot/debug.go --- snapd-2.47.1+20.10.1build1/boot/debug.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/boot/debug.go 2020-11-19 16:51:02.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.47.1+20.10.1build1/boot/export_test.go snapd-2.48+21.04/boot/export_test.go --- snapd-2.47.1+20.10.1build1/boot/export_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/boot/export_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -88,19 +88,27 @@ return o.currentTrustedRecoveryBootAssetsMap() } -func MockSecbootSealKey(f func(key secboot.EncryptionKey, params *secboot.SealKeyParams) error) (restore func()) { - old := secbootSealKey - secbootSealKey = f +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() { - secbootSealKey = old + secbootSealKeys = old } } -func MockSecbootResealKey(f func(params *secboot.ResealKeyParams) error) (restore func()) { - old := secbootResealKey - secbootResealKey = f +func MockSecbootResealKeys(f func(params *secboot.ResealKeysParams) error) (restore func()) { + old := secbootResealKeys + secbootResealKeys = f return func() { - secbootResealKey = old + secbootResealKeys = old } } diff -Nru snapd-2.47.1+20.10.1build1/boot/initramfs20dirs.go snapd-2.48+21.04/boot/initramfs20dirs.go --- snapd-2.47.1+20.10.1build1/boot/initramfs20dirs.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/boot/initramfs20dirs.go 2020-11-19 16:51:02.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. @@ -59,21 +67,34 @@ // InstallHostFDEDataDir is the location of the FDE data during install mode. InstallHostFDEDataDir string - // InitramfsEncryptionKeyDir is the location of the encrypted partition keys - // during the initramfs. - InitramfsEncryptionKeyDir 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.47.1+20.10.1build1/boot/makebootable.go snapd-2.48+21.04/boot/makebootable.go --- snapd-2.47.1+20.10.1build1/boot/makebootable.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/boot/makebootable.go 2020-11-19 16:51:02.000000000 +0000 @@ -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) @@ -294,9 +296,13 @@ // run partition layout, no /boot mount. NoSlashBoot: 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 @@ -346,24 +352,20 @@ return fmt.Errorf("cannot set run system environment: %v", err) } - _, ok = bl.(bootloader.ManagedAssetsBootloader) + _, 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 - ok, err := bl.InstallBootConfig(bootWith.UnpackedGadgetDir, opts) - if err != nil { + if err := bl.InstallBootConfig(bootWith.UnpackedGadgetDir, opts); err != nil { return fmt.Errorf("cannot install managed bootloader assets: %v", err) } - if !ok { - return fmt.Errorf("cannot install boot config with a mismatched gadget") - } } if sealer != nil { // seal the encryption key to the parameters specified in modeenv - if err := sealKeyToModeenv(sealer.encryptionKey, model, modeenv); err != nil { + if err := sealKeyToModeenv(sealer.dataEncryptionKey, sealer.saveEncryptionKey, model, modeenv); err != nil { return err } } diff -Nru snapd-2.47.1+20.10.1build1/boot/makebootable_test.go snapd-2.48+21.04/boot/makebootable_test.go --- snapd-2.47.1+20.10.1build1/boot/makebootable_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/boot/makebootable_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -33,6 +33,7 @@ "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" @@ -79,6 +80,7 @@ } func (s *makeBootableSuite) TestMakeBootable(c *C) { + bootloader.Force(nil) model := boottest.MakeMockModel() grubCfg := []byte("#grub cfg") @@ -117,14 +119,11 @@ 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(s.rootdir), kernelInfo.Filename()) @@ -173,6 +172,7 @@ } func (s *makeBootable20Suite) TestMakeBootable20(c *C) { + bootloader.Force(nil) model := boottest.MakeMockUC20Model() unpackedGadgetDir := c.MkDir() @@ -228,7 +228,8 @@ // ensure only a single file got copied (the grub.cfg) files, err := filepath.Glob(filepath.Join(s.rootdir, "EFI/ubuntu/*")) c.Assert(err, IsNil) - c.Check(files, HasLen, 1) + // 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(s.rootdir, "EFI/ubuntu/grub.cfg"), testutil.FileEquals, grubRecoveryCfgAsset) @@ -237,13 +238,13 @@ 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) { @@ -369,7 +370,8 @@ } // set up observer state - obs, err := boot.TrustedAssetsInstallObserverForModel(model, unpackedGadgetDir) + useEncryption := true + obs, err := boot.TrustedAssetsInstallObserverForModel(model, unpackedGadgetDir, useEncryption) c.Assert(obs, NotNil) c.Assert(err, IsNil) runBootStruct := &gadget.LaidOutStructure{ @@ -389,10 +391,12 @@ // set encryption key myKey := secboot.EncryptionKey{} + myKey2 := secboot.EncryptionKey{} for i := range myKey { myKey[i] = byte(i) + myKey2[i] = byte(128 + i) } - obs.ChosenEncryptionKey(myKey) + obs.ChosenEncryptionKeys(myKey, myKey2) // set a mock recovery kernel readSystemEssentialCalls := 0 @@ -410,10 +414,20 @@ defer restore() // set mock key sealing - sealKeyCalls := 0 - restore = boot.MockSecbootSealKey(func(key secboot.EncryptionKey, params *secboot.SealKeyParams) error { - sealKeyCalls++ - c.Check(key, DeepEquals, myKey) + 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, @@ -428,14 +442,27 @@ 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", - }) + 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 @@ -517,8 +544,8 @@ c.Check(copiedRecoveryGrubBin, testutil.FileEquals, "recovery grub content") c.Check(copiedRecoveryShimBin, testutil.FileEquals, "recovery shim content") - // make sure SealKey was called - c.Check(sealKeyCalls, Equals, 1) + // 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) @@ -582,9 +609,9 @@ UnpackedGadgetDir: unpackedGadgetDir, } - // no grub cfg in gadget directory raises an error + // no grub marker in gadget directory raises an error err = boot.MakeBootable(model, s.rootdir, bootWith, nil) - c.Assert(err, ErrorMatches, "cannot install boot config with a mismatched gadget") + c.Assert(err, ErrorMatches, "internal error: cannot identify run system bootloader: cannot determine bootloader") // set up grub.cfg in gadget grubCfg := []byte("#grub cfg") @@ -685,7 +712,8 @@ } // set up observer state - obs, err := boot.TrustedAssetsInstallObserverForModel(model, unpackedGadgetDir) + useEncryption := true + obs, err := boot.TrustedAssetsInstallObserverForModel(model, unpackedGadgetDir, useEncryption) c.Assert(obs, NotNil) c.Assert(err, IsNil) runBootStruct := &gadget.LaidOutStructure{ @@ -705,10 +733,12 @@ // set encryption key myKey := secboot.EncryptionKey{} + myKey2 := secboot.EncryptionKey{} for i := range myKey { myKey[i] = byte(i) + myKey2[i] = byte(128 + i) } - obs.ChosenEncryptionKey(myKey) + obs.ChosenEncryptionKeys(myKey, myKey2) // set a mock recovery kernel readSystemEssentialCalls := 0 @@ -726,10 +756,20 @@ defer restore() // set mock key sealing - sealKeyCalls := 0 - restore = boot.MockSecbootSealKey(func(key secboot.EncryptionKey, params *secboot.SealKeyParams) error { - sealKeyCalls++ - c.Check(key, DeepEquals, myKey) + 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, @@ -759,10 +799,11 @@ defer restore() err = boot.MakeBootable(model, s.rootdir, bootWith, obs) - c.Assert(err, ErrorMatches, "cannot seal the encryption key: seal error") + 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() @@ -810,7 +851,7 @@ // TODO:UC20: enable this use case err = boot.MakeBootable(model, s.rootdir, bootWith, nil) - c.Assert(err, ErrorMatches, fmt.Sprintf("cannot find boot config in %q", unpackedGadgetDir)) + c.Assert(err, ErrorMatches, "non-empty uboot.env not supported on UC20 yet") } func (s *makeBootable20UbootSuite) TestUbootMakeBootable20BootScr(c *C) { @@ -869,6 +910,7 @@ 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 @@ -882,7 +924,7 @@ ) } -func (s *makeBootable20UbootSuite) TestUbootMakeBootable20RunModeBootScr(c *C) { +func (s *makeBootable20UbootSuite) TestUbootMakeBootable20RunModeBootSel(c *C) { bootloader.Force(nil) model := boottest.MakeMockUC20Model() @@ -898,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) @@ -906,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 @@ -934,8 +979,8 @@ KernelPath: kernelInSeed, Kernel: kernelInfo, Recovery: false, + UnpackedGadgetDir: unpackedGadgetDir, } - err = boot.MakeBootable(model, s.rootdir, bootWith, nil) c.Assert(err, IsNil) diff -Nru snapd-2.47.1+20.10.1build1/boot/seal.go snapd-2.48+21.04/boot/seal.go --- snapd-2.47.1+20.10.1build1/boot/seal.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/boot/seal.go 2020-11-19 16:51:02.000000000 +0000 @@ -20,6 +20,9 @@ package boot import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" "encoding/json" "fmt" "os" @@ -38,20 +41,63 @@ ) var ( - secbootSealKey = secboot.SealKey - secbootResealKey = secboot.ResealKey + secbootSealKeys = secboot.SealKeys + secbootResealKeys = secboot.ResealKeys seedReadSystemEssential = seed.ReadSystemEssential ) +// Hook functions setup by devicestate to support device-specific full +// disk encryption implementations. +var ( + HasFDESetupHook = func() (bool, error) { + return false, nil + } + RunFDESetupHook = func(op string, params *FdeSetupHookParams) error { + return fmt.Errorf("internal error: RunFDESetupHook not set yet") + } +) + +// FdeSetupHookParams contains the inputs for the fde-setup hook +type FdeSetupHookParams struct { + Key secboot.EncryptionKey + KeyName string + + KernelInfo *snap.Info + Model *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") } -// sealKeyToModeenv seals the supplied key to the parameters specified +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 secboot.EncryptionKey, model *asserts.Model, modeenv *Modeenv) error { +func sealKeyToModeenv(key, saveKey secboot.EncryptionKey, model *asserts.Model, modeenv *Modeenv) error { + 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 sealKeyToModeenvUsingFdeSetupHook(key, saveKey secboot.EncryptionKey, model *asserts.Model, modeenv *Modeenv) error { + return fmt.Errorf("cannot use fde-setup hook yet") +} + +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, @@ -95,20 +141,34 @@ bootloader.RoleRunMode: bl.Name(), } - // get model parameters from bootchains - modelParams, err := sealKeyModelParams(pbc, roleToBlName) + // 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 + } + } + + // 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 prepare for key sealing: %v", err) + return fmt.Errorf("cannot generate key for signing dynamic authorization policies: %v", err) + } + + if err := sealRunObjectKeys(key, pbc, authKey, roleToBlName); err != nil { + return err } - sealKeyParams := &secboot.SealKeyParams{ - ModelParams: modelParams, - KeyFile: filepath.Join(InitramfsEncryptionKeyDir, "ubuntu-data.sealed-key"), - TPMPolicyUpdateDataFile: filepath.Join(InstallHostFDEDataDir, "policy-update-data"), - TPMLockoutAuthFile: filepath.Join(InstallHostFDEDataDir, "tpm-lockout-auth"), - } - // finally, seal the key - if err := secbootSealKey(key, sealKeyParams); err != nil { - return fmt.Errorf("cannot seal the encryption key: %v", err) + + if err := sealFallbackObjectKeys(key, saveKey, rpbc, authKey, roleToBlName); err != nil { + return err } if err := stampSealedKeys(InstallHostWritableDir); err != nil { @@ -120,6 +180,74 @@ 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. + keys := []secboot.SealKeyRequest{ + { + Key: key, + KeyFile: filepath.Join(InitramfsBootEncryptionKeyDir, "ubuntu-data.sealed-key"), + }, + } + if err := secbootSealKeys(keys, 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. + keys := []secboot.SealKeyRequest{ + { + Key: key, + KeyFile: filepath.Join(InitramfsSeedEncryptionKeyDir, "ubuntu-data.recovery.sealed-key"), + }, + { + Key: saveKey, + KeyFile: filepath.Join(InitramfsSeedEncryptionKeyDir, "ubuntu-save.recovery.sealed-key"), + }, + } + if err := secbootSealKeys(keys, sealKeyParams); err != nil { + return fmt.Errorf("cannot seal the fallback encryption keys: %v", err) + } + return nil } @@ -186,14 +314,14 @@ 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, rootdir, expectReseal) + needed, nextCount, err := isResealNeeded(pbc, bootChainsFileUnder(rootdir), expectReseal) if err != nil { return err } if !needed { - // no need to actually reseal logger.Debugf("reseal not necessary") return nil } @@ -205,24 +333,87 @@ 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) } - resealKeyParams := &secboot.ResealKeyParams{ - ModelParams: modelParams, - KeyFile: filepath.Join(InitramfsEncryptionKeyDir, "ubuntu-data.sealed-key"), - TPMPolicyUpdateDataFile: filepath.Join(dirs.SnapFDEDirUnder(rootdir), "policy-update-data"), + + // 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 := secbootResealKey(resealKeyParams); err != nil { + if err := secbootResealKeys(resealKeyParams); err != nil { return fmt.Errorf("cannot reseal the encryption key: %v", err) } - logger.Debugf("resealing (%d) succeeded", nextCount) - bootChainsPath := bootChainsFileUnder(rootdir) - if err := writeBootChains(pbc, bootChainsPath, nextCount); err != nil { - return 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 @@ -386,8 +577,8 @@ // 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, rootdir string, expectReseal bool) (ok bool, nextCount int, err error) { - previousPbc, c, err := readBootChains(bootChainsFileUnder(rootdir)) +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 } diff -Nru snapd-2.47.1+20.10.1build1/boot/seal_test.go snapd-2.48+21.04/boot/seal_test.go --- snapd-2.47.1+20.10.1build1/boot/seal_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/boot/seal_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -33,6 +33,7 @@ "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" @@ -60,7 +61,7 @@ err string }{ {sealErr: nil, err: ""}, - {sealErr: errors.New("seal error"), err: "cannot seal the encryption key: seal error"}, + {sealErr: errors.New("seal error"), err: "cannot seal the encryption keys: seal error"}, } { rootdir := c.MkDir() dirs.SetRootDir(rootdir) @@ -99,8 +100,10 @@ // 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() @@ -121,11 +124,33 @@ defer restore() // set mock key sealing - sealKeyCalls := 0 - restore = boot.MockSecbootSealKey(func(key secboot.EncryptionKey, params *secboot.SealKeyParams) error { - sealKeyCalls++ - c.Check(key, DeepEquals, myKey) + 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) @@ -133,27 +158,45 @@ 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) - 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", - }) + 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, model, modeenv) - c.Assert(sealKeyCalls, Equals, 1) + 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 { @@ -219,12 +262,43 @@ }, }) + // 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 c.Check(filepath.Join(dirs.SnapFDEDirUnder(boot.InstallHostWritableDir), "sealed-keys"), testutil.FilePresent) } } +// TODO:UC20: also test fallback reseal func (s *sealSuite) TestResealKeyToModeenv(c *C) { var prevPbc boot.PredictableBootChains @@ -308,20 +382,32 @@ defer restore() // set mock key resealing - resealKeyCalls := 0 - restore = boot.MockSecbootResealKey(func(params *secboot.ResealKeyParams) error { - resealKeyCalls++ + 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") - 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) + 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) @@ -344,39 +430,42 @@ 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) - 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)), - )), - }) + 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 }) @@ -391,10 +480,14 @@ if !tc.sealedKeys || tc.prevPbc { // did nothing c.Assert(err, IsNil) - c.Assert(resealKeyCalls, Equals, 0) + c.Assert(resealKeysCalls, Equals, 0) continue } - c.Assert(resealKeyCalls, Equals, 1) + if tc.resealErr != nil { + c.Assert(resealKeysCalls, Equals, 1) + } else { + c.Assert(resealKeysCalls, Equals, 2) + } if tc.err == "" { c.Assert(err, IsNil) } else { @@ -718,6 +811,10 @@ } 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", @@ -755,12 +852,12 @@ err := boot.WriteBootChains(pbc, filepath.Join(dirs.SnapFDEDirUnder(rootdir), "boot-chains"), 2) c.Assert(err, IsNil) - needed, _, err := boot.IsResealNeeded(pbc, rootdir, false) + 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, rootdir, false) + 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) @@ -768,7 +865,7 @@ // boot-chains does not exist, we cannot compare so advise to reseal otherRootdir := c.MkDir() - needed, cnt, err = boot.IsResealNeeded(otherchain, otherRootdir, false) + 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) @@ -776,29 +873,58 @@ // 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, rootdir, false) + 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) - // unrevioned kernel chain + // unrevisioned kernel chain unrevchain := []boot.BootChain{pbc[0], pbc[1]} unrevchain[1].KernelRevision = "" // write on disk - err = boot.WriteBootChains(unrevchain, filepath.Join(dirs.SnapFDEDirUnder(rootdir), "boot-chains"), 2) + bootChainsFile := filepath.Join(dirs.SnapFDEDirUnder(rootdir), "boot-chains") + err = boot.WriteBootChains(unrevchain, bootChainsFile, 2) c.Assert(err, IsNil) - needed, cnt, err = boot.IsResealNeeded(pbc, rootdir, false) + 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, rootdir, false) + needed, _, err = boot.IsResealNeeded(unrevchain, bootChainsFile, false) c.Assert(err, IsNil) c.Check(needed, Equals, false) - needed, cnt, err = boot.IsResealNeeded(unrevchain, rootdir, true) + 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) TestSealToModeenvWithFdeHookFailsToday(c *C) { + rootdir := c.MkDir() + dirs.SetRootDir(rootdir) + defer dirs.SetRootDir("") + + oldHasFDESetupHook := boot.HasFDESetupHook + defer func() { boot.HasFDESetupHook = oldHasFDESetupHook }() + boot.HasFDESetupHook = func() (bool, error) { + return true, nil + } + oldRunFDESetupHook := boot.RunFDESetupHook + defer func() { boot.RunFDESetupHook = oldRunFDESetupHook }() + boot.RunFDESetupHook = func(string, *boot.FdeSetupHookParams) error { + c.Fatalf("hook runner should not be called yet") + return nil + } + + modeenv := &boot.Modeenv{ + RecoverySystem: "20200825", + } + myKey := secboot.EncryptionKey{} + myKey2 := secboot.EncryptionKey{} + + model := boottest.MakeMockUC20Model() + err := boot.SealKeyToModeenv(myKey, myKey2, model, modeenv) + c.Assert(err, ErrorMatches, "cannot use fde-setup hook yet") +} diff -Nru snapd-2.47.1+20.10.1build1/bootloader/androidboot.go snapd-2.48+21.04/bootloader/androidboot.go --- snapd-2.47.1+20.10.1build1/bootloader/androidboot.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/bootloader/androidboot.go 2020-11-19 16:51:02.000000000 +0000 @@ -52,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.47.1+20.10.1build1/bootloader/assets/data/grub-recovery.cfg snapd-2.48+21.04/bootloader/assets/data/grub-recovery.cfg --- snapd-2.47.1+20.10.1build1/bootloader/assets/data/grub-recovery.cfg 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/bootloader/assets/data/grub-recovery.cfg 2020-11-19 16:51:02.000000000 +0000 @@ -58,6 +58,6 @@ } done -menuentry 'System setup' --hotkey=f 'uefi-firmware' { +menuentry 'UEFI Firmware Settings' --hotkey=f 'uefi-firmware' { fwsetup } diff -Nru snapd-2.47.1+20.10.1build1/bootloader/assets/grub_recovery_cfg_asset.go snapd-2.48+21.04/bootloader/assets/grub_recovery_cfg_asset.go --- snapd-2.47.1+20.10.1build1/bootloader/assets/grub_recovery_cfg_asset.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/bootloader/assets/grub_recovery_cfg_asset.go 2020-11-19 16:51:02.000000000 +0000 @@ -148,9 +148,10 @@ 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, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x20, 0x73, 0x65, - 0x74, 0x75, 0x70, 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, + 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.47.1+20.10.1build1/bootloader/bootloader.go snapd-2.48+21.04/bootloader/bootloader.go --- snapd-2.47.1+20.10.1build1/bootloader/bootloader.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/bootloader/bootloader.go 2020-11-19 16:51:02.000000000 +0000 @@ -96,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 @@ -162,15 +161,14 @@ DisableTryKernel() error } -// ManagedAssetsBootloader has its boot assets (typically boot config) managed -// by snapd. -type ManagedAssetsBootloader interface { +// 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 - // IsCurrentlyManaged returns true when the on disk boot assets are managed. - IsCurrentlyManaged() (bool, error) // ManagedAssets returns a list of boot assets managed by the bootloader - // in the boot filesystem. + // 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 @@ -183,14 +181,10 @@ // 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) -} -// TrustedAssetsBootloader has boot assets that take part in secure boot -// process. -type TrustedAssetsBootloader interface { - // 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. + // 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. @@ -203,25 +197,22 @@ BootChain(runBl Bootloader, kernelPath string) ([]BootFile, error) } -func genericInstallBootConfig(gadgetFile, systemFile string) (bool, error) { - if !osutil.FileExists(gadgetFile) { - return false, nil - } +func genericInstallBootConfig(gadgetFile, systemFile string) error { 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.CopyFile(gadgetFile, systemFile, osutil.CopyFlagOverwrite) } -func genericSetBootConfigFromAsset(systemFile, assetName string) (bool, error) { +func genericSetBootConfigFromAsset(systemFile, assetName string) error { bootConfig := assets.Internal(assetName) if bootConfig == nil { - return true, fmt.Errorf("internal error: no boot asset for %q", assetName) + 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.AtomicWriteFile(systemFile, bootConfig, 0644, 0) + return osutil.AtomicWriteFile(systemFile, bootConfig, 0644, 0) } func genericUpdateBootConfigFromAssets(systemFile string, assetName string) error { @@ -254,16 +245,11 @@ if err := opts.validate(); err != nil { return err } - // TODO:UC20 use ForGadget() to obtain the right bootloader - for _, bl := range []installableBootloader{&grub{}, &uboot{}, &androidboot{}, &lk{}} { - bl.setRootDir(rootDir) - ok, err := bl.InstallBootConfig(gadgetDir, opts) - if ok { - return err - } + bl, err := ForGadget(gadgetDir, rootDir, opts) + if err != nil { + return fmt.Errorf("cannot find boot config in %q", gadgetDir) } - - return fmt.Errorf("cannot find boot config in %q", gadgetDir) + return bl.InstallBootConfig(gadgetDir, opts) } type bootloaderNewFunc func(rootdir string, opts *Options) Bootloader @@ -373,8 +359,9 @@ } for _, blNew := range bootloaders { bl := blNew(rootDir, opts) + markerConf := filepath.Join(gadgetDir, bl.Name()+".conf") // do we have a marker file? - if osutil.FileExists(filepath.Join(gadgetDir, bl.Name()+".conf")) { + if osutil.FileExists(markerConf) { return bl, nil } } diff -Nru snapd-2.47.1+20.10.1build1/bootloader/bootloadertest/bootloadertest.go snapd-2.48+21.04/bootloader/bootloadertest/bootloadertest.go --- snapd-2.47.1+20.10.1build1/bootloader/bootloadertest/bootloadertest.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/bootloader/bootloadertest/bootloadertest.go 2020-11-19 16:51:02.000000000 +0000 @@ -43,7 +43,6 @@ RemoveKernelAssetsCalls []snap.PlaceInfo InstallBootConfigCalled []string - InstallBootConfigResult bool InstallBootConfigErr error enabledKernel snap.PlaceInfo @@ -58,7 +57,6 @@ var _ bootloader.TrustedAssetsBootloader = (*MockTrustedAssetsBootloader)(nil) var _ bootloader.ExtractedRunKernelImageBootloader = (*MockExtractedRunKernelImageBootloader)(nil) var _ bootloader.ExtractedRecoveryKernelImageBootloader = (*MockExtractedRecoveryKernelImageBootloader)(nil) -var _ bootloader.ManagedAssetsBootloader = (*MockManagedAssetsBootloader)(nil) func Mock(name, bootdir string) *MockBootloader { return &MockBootloader{ @@ -139,9 +137,9 @@ // 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 @@ -374,36 +372,43 @@ return b.runKernelImageMockedErrs["DisableTryKernel"] } -// MockManagedAssetsBootloader mocks a bootloader implementing the -// bootloader.ManagedAssetsBootloader interface. -type MockManagedAssetsBootloader struct { +// MockTrustedAssetsBootloader mocks a bootloader implementing the +// bootloader.TrustedAssetsBootloader interface. +type MockTrustedAssetsBootloader struct { *MockBootloader - IsManaged bool - IsManagedErr error + 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 - Assets []string + ManagedAssetsList []string StaticCommandLine string CandidateStaticCommandLine string CommandLineErr error } -func (b *MockBootloader) WithManagedAssets() *MockManagedAssetsBootloader { - return &MockManagedAssetsBootloader{ +func (b *MockBootloader) WithTrustedAssets() *MockTrustedAssetsBootloader { + return &MockTrustedAssetsBootloader{ MockBootloader: b, } } -func (b *MockManagedAssetsBootloader) IsCurrentlyManaged() (bool, error) { - return b.IsManaged, b.IsManagedErr +func (b *MockTrustedAssetsBootloader) ManagedAssets() []string { + return b.ManagedAssetsList } -func (b *MockManagedAssetsBootloader) ManagedAssets() []string { - return b.Assets -} - -func (b *MockManagedAssetsBootloader) UpdateBootConfig(opts *bootloader.Options) error { +func (b *MockTrustedAssetsBootloader) UpdateBootConfig(opts *bootloader.Options) error { b.UpdateCalls++ return b.UpdateErr } @@ -419,45 +424,20 @@ return strings.TrimSpace(line) } -func (b *MockManagedAssetsBootloader) CommandLine(modeArg, systemArg, extraArgs string) (string, error) { +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 *MockManagedAssetsBootloader) CandidateCommandLine(modeArg, systemArg, extraArgs string) (string, error) { +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 } -// 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 -} - -func (b *MockBootloader) WithTrustedAssets() *MockTrustedAssetsBootloader { - return &MockTrustedAssetsBootloader{ - MockBootloader: b, - } -} - func (b *MockTrustedAssetsBootloader) TrustedAssets() ([]string, error) { b.TrustedAssetsCalls++ return b.TrustedAssetsList, b.TrustedAssetsErr @@ -473,30 +453,3 @@ b.BootChainKernelPath = append(b.BootChainKernelPath, kernelPath) return b.BootChainList, b.BootChainErr } - -// MockManagedAssetsRecoveryAwareBootloader mocks a bootloader implementing the -// bootloader.ManagedAssetsBootloader and bootloader.RecoveryAwareBootloader -// interfaces. -type MockManagedAssetsRecoveryAwareBootloader struct { - *MockManagedAssetsBootloader - - EnvVars map[string]string -} - -func (b *MockBootloader) WithManagedAssetsRecoveryAware() *MockManagedAssetsRecoveryAwareBootloader { - return &MockManagedAssetsRecoveryAwareBootloader{ - MockManagedAssetsBootloader: &MockManagedAssetsBootloader{ - MockBootloader: b, - }, - EnvVars: make(map[string]string), - } -} - -func (b *MockManagedAssetsRecoveryAwareBootloader) SetRecoverySystemEnv(systemDir string, env map[string]string) error { - b.EnvVars = env - return nil -} - -func (b *MockManagedAssetsRecoveryAwareBootloader) GetRecoverySystemEnv(systemDir, key string) (string, error) { - return b.EnvVars[key], nil -} diff -Nru snapd-2.47.1+20.10.1build1/bootloader/grub.go snapd-2.48+21.04/bootloader/grub.go --- snapd-2.47.1+20.10.1build1/bootloader/grub.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/bootloader/grub.go 2020-11-19 16:51:02.000000000 +0000 @@ -29,7 +29,6 @@ "github.com/snapcore/snapd/bootloader/grubenv" "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/snap" - "github.com/snapcore/snapd/strutil" ) // sanity - grub implements the required interfaces @@ -38,7 +37,6 @@ _ installableBootloader = (*grub)(nil) _ RecoveryAwareBootloader = (*grub)(nil) _ ExtractedRunKernelImageBootloader = (*grub)(nil) - _ ManagedAssetsBootloader = (*grub)(nil) _ TrustedAssetsBootloader = (*grub)(nil) ) @@ -88,29 +86,19 @@ return filepath.Join(g.rootdir, g.basedir) } -func (g *grub) installManagedRecoveryBootConfig(gadgetDir string) (bool, error) { - gadgetGrubCfg := filepath.Join(gadgetDir, g.Name()+".conf") - if !osutil.FileExists(gadgetGrubCfg) { - // gadget does not use grub bootloader - return false, nil - } +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) (bool, error) { - gadgetGrubCfg := filepath.Join(gadgetDir, g.Name()+".conf") - if !osutil.FileExists(gadgetGrubCfg) { - // gadget does not use grub bootloader - return false, nil - } +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) (bool, error) { +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) @@ -367,18 +355,6 @@ return genericUpdateBootConfigFromAssets(currentBootConfig, bootScriptName) } -// IsCurrentlyManaged returns true when the boot config is managed by snapd. -// -// Implements ManagedBootloader for the grub bootloader. -func (g *grub) IsCurrentlyManaged() (bool, error) { - currentBootScript := filepath.Join(g.dir(), "grub.cfg") - _, err := editionFromDiskConfigAsset(currentBootScript) - if err != nil && err != errNoEdition { - return false, err - } - return err != errNoEdition, nil -} - // ManagedAssets returns a list relative paths to boot assets inside the root // directory of the filesystem. // @@ -395,7 +371,7 @@ assetName = "grub-recovery.cfg" } staticCmdline := staticCommandLineForGrubAssetEdition(assetName, edition) - args, err := strutil.KernelCommandLineSplit(staticCmdline + " " + extraArgs) + args, err := osutil.KernelCommandLineSplit(staticCmdline + " " + extraArgs) if err != nil { return "", fmt.Errorf("cannot use badly formatted kernel command line: %v", err) } diff -Nru snapd-2.47.1+20.10.1build1/bootloader/grub_test.go snapd-2.48+21.04/bootloader/grub_test.go --- snapd-2.47.1+20.10.1build1/bootloader/grub_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/bootloader/grub_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -471,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) @@ -597,42 +601,6 @@ c.Check(exists, Equals, false) } -func (s *grubTestSuite) TestIsCurrentlyManaged(c *C) { - // native EFI/ubuntu setup, as we're using NoSlashBoot - 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) - mg, ok := g.(bootloader.ManagedAssetsBootloader) - c.Assert(ok, Equals, true) - - // native EFI/ubuntu setup, as we're using NoSlashBoot - s.makeFakeGrubEFINativeEnv(c, []byte(`this is -some random boot config`)) - - is, err := mg.IsCurrentlyManaged() - c.Assert(err, IsNil) - c.Assert(is, Equals, false) - - // try with real snap managed config - s.makeFakeGrubEFINativeEnv(c, []byte(`# Snapd-Boot-Config-Edition: 5 -managed by snapd`)) - - is, err = mg.IsCurrentlyManaged() - c.Assert(err, IsNil) - c.Assert(is, Equals, true) - - // break boot config - err = os.Chmod(s.grubEFINativeDir(), 0000) - defer os.Chmod(s.grubEFINativeDir(), 0755) - c.Assert(err, IsNil) - is, err = mg.IsCurrentlyManaged() - c.Assert(err, ErrorMatches, "cannot load existing config asset: .*/grub.cfg: permission denied") - c.Assert(is, Equals, false) -} - func (s *grubTestSuite) TestListManagedAssets(c *C) { s.makeFakeGrubEFINativeEnv(c, []byte(`this is some random boot config`)) @@ -641,23 +609,23 @@ g := bootloader.NewGrub(s.rootdir, opts) c.Assert(g, NotNil) - mg, ok := g.(bootloader.ManagedAssetsBootloader) + tg, ok := g.(bootloader.TrustedAssetsBootloader) c.Assert(ok, Equals, true) - c.Check(mg.ManagedAssets(), DeepEquals, []string{ + c.Check(tg.ManagedAssets(), DeepEquals, []string{ "EFI/ubuntu/grub.cfg", }) opts = &bootloader.Options{Role: bootloader.RoleRecovery} - mg = bootloader.NewGrub(s.rootdir, opts).(bootloader.ManagedAssetsBootloader) - c.Check(mg.ManagedAssets(), DeepEquals, []string{ + 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 - mg = bootloader.NewGrub(s.rootdir, nil).(bootloader.ManagedAssetsBootloader) - c.Check(mg.ManagedAssets(), DeepEquals, []string{ + tg = bootloader.NewGrub(s.rootdir, nil).(bootloader.TrustedAssetsBootloader) + c.Check(tg.ManagedAssets(), DeepEquals, []string{ "boot/grub/grub.cfg", }) } @@ -675,10 +643,10 @@ `)) defer restore() - eg, ok := g.(bootloader.ManagedAssetsBootloader) + tg, ok := g.(bootloader.TrustedAssetsBootloader) c.Assert(ok, Equals, true) // install the recovery boot script - err := eg.UpdateBootConfig(opts) + err := tg.UpdateBootConfig(opts) c.Assert(err, IsNil) c.Assert(filepath.Join(s.grubEFINativeDir(), "grub.cfg"), testutil.FileEquals, `recovery boot script`) @@ -701,10 +669,10 @@ this is mocked grub.conf `)) defer restore() - eg, ok := g.(bootloader.ManagedAssetsBootloader) + tg, ok := g.(bootloader.TrustedAssetsBootloader) c.Assert(ok, Equals, true) // install the recovery boot script - err := eg.UpdateBootConfig(opts) + 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 @@ -723,9 +691,9 @@ restore := assets.MockInternal("grub.cfg", []byte(newConfig)) defer restore() - eg, ok := g.(bootloader.ManagedAssetsBootloader) + tg, ok := g.(bootloader.TrustedAssetsBootloader) c.Assert(ok, Equals, true) - err := eg.UpdateBootConfig(opts) + err := tg.UpdateBootConfig(opts) c.Assert(err, IsNil) if update { c.Assert(filepath.Join(s.grubEFINativeDir(), "grub.cfg"), testutil.FileEquals, newConfig) @@ -781,6 +749,10 @@ } 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 ` @@ -796,14 +768,14 @@ opts := &bootloader.Options{NoSlashBoot: true} g := bootloader.NewGrub(s.rootdir, opts) c.Assert(g, NotNil) - eg, ok := g.(bootloader.ManagedAssetsBootloader) + 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 = eg.UpdateBootConfig(opts) + 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) @@ -813,7 +785,7 @@ // writing out new config fails err = os.Chmod(s.grubEFINativeDir(), 0111) c.Assert(err, IsNil) - err = eg.UpdateBootConfig(opts) + 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) } @@ -849,14 +821,14 @@ defer restore() opts := &bootloader.Options{NoSlashBoot: true} - mg := bootloader.NewGrub(s.rootdir, opts).(bootloader.ManagedAssetsBootloader) + 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.ManagedAssetsBootloader) + 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) @@ -886,22 +858,22 @@ optsNoSlashBoot := &bootloader.Options{NoSlashBoot: true} g := bootloader.NewGrub(s.rootdir, optsNoSlashBoot) c.Assert(g, NotNil) - mg, ok := g.(bootloader.ManagedAssetsBootloader) + tg, ok := g.(bootloader.TrustedAssetsBootloader) c.Assert(ok, Equals, true) extraArgs := `extra_arg=1 extra_foo=-1 panic=3 baz="more spaces"` - args, err := mg.CommandLine("snapd_recovery_mode=run", "", extraArgs) + 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 = mg.CommandLine("", "", extraArgs) + 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.ManagedAssetsBootloader) + 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 @@ -912,10 +884,10 @@ boot script ` s.makeFakeGrubEFINativeEnv(c, []byte(grubCfg3)) - mg = bootloader.NewGrub(s.rootdir, optsNoSlashBoot).(bootloader.ManagedAssetsBootloader) + tg = bootloader.NewGrub(s.rootdir, optsNoSlashBoot).(bootloader.TrustedAssetsBootloader) c.Assert(g, NotNil) extraArgs = `extra_arg=1` - args, err = mg.CommandLine("snapd_recovery_mode=run", "", extraArgs) + 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`) } @@ -949,9 +921,9 @@ defer restore() optsNoSlashBoot := &bootloader.Options{NoSlashBoot: true} - mg := bootloader.NewGrub(s.rootdir, optsNoSlashBoot).(bootloader.ManagedAssetsBootloader) + mg := bootloader.NewGrub(s.rootdir, optsNoSlashBoot).(bootloader.TrustedAssetsBootloader) optsRecovery := &bootloader.Options{NoSlashBoot: true, Role: bootloader.RoleRecovery} - recoverymg := bootloader.NewGrub(s.rootdir, optsRecovery).(bootloader.ManagedAssetsBootloader) + recoverymg := bootloader.NewGrub(s.rootdir, optsRecovery).(bootloader.TrustedAssetsBootloader) args, err := mg.CandidateCommandLine("snapd_recovery_mode=run", "", "extra=1") c.Assert(err, IsNil) @@ -995,17 +967,17 @@ opts := &bootloader.Options{NoSlashBoot: true} g := bootloader.NewGrub(s.rootdir, opts) c.Assert(g, NotNil) - mg, ok := g.(bootloader.ManagedAssetsBootloader) + tg, ok := g.(bootloader.TrustedAssetsBootloader) c.Assert(ok, Equals, true) extraArgs := "foo bar baz=1" - args, err := mg.CommandLine("snapd_recovery_mode=run", "", extraArgs) + 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.ManagedAssetsBootloader) + 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 diff -Nru snapd-2.47.1+20.10.1build1/bootloader/lkenv/lkenv.go snapd-2.48+21.04/bootloader/lkenv/lkenv.go --- snapd-2.47.1+20.10.1build1/bootloader/lkenv/lkenv.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/bootloader/lkenv/lkenv.go 2020-11-19 16:51:02.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, @@ -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.47.1+20.10.1build1/bootloader/lkenv/lkenv_test.go snapd-2.48+21.04/bootloader/lkenv/lkenv_test.go --- snapd-2.47.1+20.10.1build1/bootloader/lkenv/lkenv_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/bootloader/lkenv/lkenv_test.go 2020-11-19 16:51:02.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.47.1+20.10.1build1/bootloader/lk.go snapd-2.48+21.04/bootloader/lk.go --- snapd-2.47.1+20.10.1build1/bootloader/lk.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/bootloader/lk.go 2020-11-19 16:51:02.000000000 +0000 @@ -72,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) @@ -205,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.47.1+20.10.1build1/bootloader/uboot.go snapd-2.48+21.04/bootloader/uboot.go --- snapd-2.47.1+20.10.1build1/bootloader/uboot.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/bootloader/uboot.go 2020-11-19 16:51:02.000000000 +0000 @@ -84,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 @@ -95,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 @@ -105,20 +105,20 @@ 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 @@ -128,7 +128,7 @@ 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.47.1+20.10.1build1/check-pr-title.py snapd-2.48+21.04/check-pr-title.py --- snapd-2.47.1+20.10.1build1/check-pr-title.py 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/check-pr-title.py 2020-11-19 16:51:02.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.47.1+20.10.1build1/client/asserts.go snapd-2.48+21.04/client/asserts.go --- snapd-2.47.1+20.10.1build1/client/asserts.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/client/asserts.go 2020-11-19 16:51:02.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.47.1+20.10.1build1/client/client.go snapd-2.48+21.04/client/client.go --- snapd-2.47.1+20.10.1build1/client/client.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/client/client.go 2020-11-19 16:51:02.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() @@ -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 } @@ -642,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.47.1+20.10.1build1/client/client_test.go snapd-2.48+21.04/client/client_test.go --- snapd-2.47.1+20.10.1build1/client/client_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/client/client_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -20,6 +20,7 @@ package client_test import ( + "encoding/json" "errors" "fmt" "io" @@ -123,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") @@ -134,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}) @@ -145,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}) @@ -166,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, "") @@ -184,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"`) } @@ -203,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, "") } @@ -212,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") } @@ -484,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") } @@ -543,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.47.1+20.10.1build1/client/console_conf.go snapd-2.48+21.04/client/console_conf.go --- snapd-2.47.1+20.10.1build1/client/console_conf.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48+21.04/client/console_conf.go 2020-11-19 16:51:02.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.47.1+20.10.1build1/client/console_conf_test.go snapd-2.48+21.04/client/console_conf_test.go --- snapd-2.47.1+20.10.1build1/client/console_conf_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48+21.04/client/console_conf_test.go 2020-11-19 16:51:02.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.47.1+20.10.1build1/client/export_test.go snapd-2.48+21.04/client/export_test.go --- snapd-2.47.1+20.10.1build1/client/export_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/client/export_test.go 2020-11-19 16:51:02.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.47.1+20.10.1build1/client/icons.go snapd-2.48+21.04/client/icons.go --- snapd-2.47.1+20.10.1build1/client/icons.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/client/icons.go 2020-11-19 16:51:02.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.47.1+20.10.1build1/client/login.go snapd-2.48+21.04/client/login.go --- snapd-2.47.1+20.10.1build1/client/login.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/client/login.go 2020-11-19 16:51:02.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.47.1+20.10.1build1/client/model.go snapd-2.48+21.04/client/model.go --- snapd-2.47.1+20.10.1build1/client/model.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/client/model.go 2020-11-19 16:51:02.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.47.1+20.10.1build1/client/snap_op.go snapd-2.48+21.04/client/snap_op.go --- snapd-2.47.1+20.10.1build1/client/snap_op.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/client/snap_op.go 2020-11-19 16:51:02.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.47.1+20.10.1build1/client/snap_op_test.go snapd-2.48+21.04/client/snap_op_test.go --- snapd-2.47.1+20.10.1build1/client/snap_op_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/client/snap_op_test.go 2020-11-19 16:51:02.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.47.1+20.10.1build1/client/snapshot.go snapd-2.48+21.04/client/snapshot.go --- snapd-2.47.1+20.10.1build1/client/snapshot.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/client/snapshot.go 2020-11-19 16:51:02.000000000 +0000 @@ -34,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") @@ -197,6 +200,31 @@ } 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.47.1+20.10.1build1/client/snapshot_test.go snapd-2.48+21.04/client/snapshot_test.go --- snapd-2.47.1+20.10.1build1/client/snapshot_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/client/snapshot_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -21,7 +21,10 @@ import ( "io/ioutil" + "net/http" "net/url" + "strconv" + "strings" "time" "gopkg.in/check.v1" @@ -141,19 +144,22 @@ func (cs *clientSuite) TestClientExportSnapshot(c *check.C) { type tableT struct { - content string - status int + content string + contentType string + status int } table := []tableT{ - {"Hello World!", 200}, - {"", 400}, + {"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 @@ -161,11 +167,11 @@ 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) } - c.Assert(size, check.Equals, int64(len(t.content)), comm) if t.status == 200 { buf, err := ioutil.ReadAll(r) @@ -174,3 +180,40 @@ } } } + +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.47.1+20.10.1build1/client/users.go snapd-2.48+21.04/client/users.go --- snapd-2.47.1+20.10.1build1/client/users.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/client/users.go 2020-11-19 16:51:02.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.47.1+20.10.1build1/client/users_test.go snapd-2.48+21.04/client/users_test.go --- snapd-2.47.1+20.10.1build1/client/users_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/client/users_test.go 2020-11-19 16:51:02.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.47.1+20.10.1build1/cmd/autogen.sh snapd-2.48+21.04/cmd/autogen.sh --- snapd-2.47.1+20.10.1build1/cmd/autogen.sh 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/cmd/autogen.sh 2020-11-19 16:51:02.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.47.1+20.10.1build1/cmd/snap/cmd_auto_import.go snapd-2.48+21.04/cmd/snap/cmd_auto_import.go --- snapd-2.47.1+20.10.1build1/cmd/snap/cmd_auto_import.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/cmd/snap/cmd_auto_import.go 2020-11-19 16:51:02.000000000 +0000 @@ -279,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.47.1+20.10.1build1/cmd/snap/cmd_auto_import_test.go snapd-2.48+21.04/cmd/snap/cmd_auto_import_test.go --- snapd-2.47.1+20.10.1build1/cmd/snap/cmd_auto_import_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/cmd/snap/cmd_auto_import_test.go 2020-11-19 16:51:02.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"}) @@ -508,3 +507,53 @@ // 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.47.1+20.10.1build1/cmd/snap/cmd_help.go snapd-2.48+21.04/cmd/snap/cmd_help.go --- snapd-2.47.1+20.10.1build1/cmd/snap/cmd_help.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/cmd/snap/cmd_help.go 2020-11-19 16:51:02.000000000 +0000 @@ -218,9 +218,10 @@ Description: i18n.G("authentication to snapd and the snap store"), Commands: []string{"login", "logout", "whoami"}, }, { - Label: i18n.G("Snapshots"), - Description: i18n.G("archives of snap data"), - Commands: []string{"saved", "save", "check-snapshot", "restore", "forget"}, + 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"), diff -Nru snapd-2.47.1+20.10.1build1/cmd/snap/cmd_pack.go snapd-2.48+21.04/cmd/snap/cmd_pack.go --- snapd-2.47.1+20.10.1build1/cmd/snap/cmd_pack.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/cmd/snap/cmd_pack.go 2020-11-19 16:51:02.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.47.1+20.10.1build1/cmd/snap/cmd_recovery.go snapd-2.48+21.04/cmd/snap/cmd_recovery.go --- snapd-2.47.1+20.10.1build1/cmd/snap/cmd_recovery.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/cmd/snap/cmd_recovery.go 2020-11-19 16:51:02.000000000 +0000 @@ -20,30 +20,41 @@ package main import ( + "errors" "fmt" + "io" "strings" "github.com/jessevdk/go-flags" "github.com/snapcore/snapd/client" "github.com/snapcore/snapd/i18n" + "github.com/snapcore/snapd/release" ) type cmdRecovery struct { clientMixin colorMixin + + ShowKeys bool `long:"show-keys"` } var shortRecoveryHelp = i18n.G("List available recovery systems") var longRecoveryHelp = i18n.G(` The recovery command lists the available recovery systems. + +With --show-keys it displays recovery keys that can be used to unlock the encrypted partitions if the device-specific automatic unlocking does not work. `) func init() { addCommand("recovery", shortRecoveryHelp, longRecoveryHelp, func() flags.Commander { // XXX: if we want more/nicer details we can add `snap recovery ` later return &cmdRecovery{} - }, nil, nil) + }, colorDescs.also( + map[string]string{ + // TRANSLATORS: This should not start with a lowercase letter. + "show-keys": i18n.G("Show recovery keys (if available) to unlock encrypted partitions."), + }), nil) } func notesForSystem(sys *client.System) string { @@ -53,11 +64,33 @@ return "-" } +func (x *cmdRecovery) showKeys(w io.Writer) error { + if release.OnClassic { + return errors.New(`command "show-keys" is not available on classic systems`) + } + var srk *client.SystemRecoveryKeysResponse + err := x.client.SystemRecoveryKeys(&srk) + if err != nil { + return err + } + fmt.Fprintf(w, "recovery:\t%s\n", srk.RecoveryKey) + fmt.Fprintf(w, "reinstall:\t%s\n", srk.ReinstallKey) + return nil +} + func (x *cmdRecovery) Execute(args []string) error { if len(args) > 0 { return ErrExtraArgs } + esc := x.getEscapes() + w := tabWriter() + defer w.Flush() + + if x.ShowKeys { + return x.showKeys(w) + } + systems, err := x.client.ListSystems() if err != nil { return err @@ -67,9 +100,6 @@ return nil } - esc := x.getEscapes() - w := tabWriter() - defer w.Flush() fmt.Fprintf(w, i18n.G("Label\tBrand\tModel\tNotes\n")) for _, sys := range systems { // doing it this way because otherwise it's a sea of %s\t%s\t%s diff -Nru snapd-2.47.1+20.10.1build1/cmd/snap/cmd_recovery_test.go snapd-2.48+21.04/cmd/snap/cmd_recovery_test.go --- snapd-2.47.1+20.10.1build1/cmd/snap/cmd_recovery_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/cmd/snap/cmd_recovery_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -26,6 +26,7 @@ . "gopkg.in/check.v1" snap "github.com/snapcore/snapd/cmd/snap" + "github.com/snapcore/snapd/release" ) func (s *SnapSuite) TestRecoveryHelp(c *C) { @@ -34,9 +35,16 @@ The recovery command lists the available recovery systems. +With --show-keys it displays recovery keys that can be used to unlock the +encrypted partitions if the device-specific automatic unlocking does not work. + [recovery command options] - --color=[auto|never|always] - --unicode=[auto|never|always] + --color=[auto|never|always] Use a little bit of color to highlight + some things. (default: auto) + --unicode=[auto|never|always] Use a little bit of Unicode to improve + legibility. (default: auto) + --show-keys Show recovery keys (if available) to + unlock encrypted partitions. ` s.testSubCommandHelp(c, "recovery", msg) } @@ -145,3 +153,42 @@ _, err := snap.Parser(snap.Client()).ParseArgs([]string{"recovery"}) c.Check(err, ErrorMatches, `cannot list recovery systems: permission denied`) } + +func (s *SnapSuite) TestRecoveryShowRecoveryKeyOnClassicErrors(c *C) { + restore := release.MockOnClassic(true) + defer restore() + + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + c.Fatalf("unexpected server call") + }) + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"recovery", "--show-keys"}) + c.Assert(err, ErrorMatches, `command "show-keys" is not available on classic systems`) +} + +func (s *SnapSuite) TestRecoveryShowRecoveryKeyHappy(c *C) { + restore := release.MockOnClassic(false) + defer restore() + + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch n { + case 0: + c.Check(r.Method, Equals, "GET") + c.Check(r.URL.Path, Equals, "/v2/system-recovery-keys") + c.Check(r.URL.RawQuery, Equals, "") + fmt.Fprintln(w, `{"type": "sync", "result": {"recovery-key": "61665-00531-54469-09783-47273-19035-40077-28287", "reinstall-key":"1234"}}`) + default: + c.Fatalf("expected to get 1 requests, now on %d", n+1) + } + + n++ + }) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"recovery", "--show-keys"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + c.Check(s.Stdout(), Equals, `recovery: 61665-00531-54469-09783-47273-19035-40077-28287 +reinstall: 1234 +`) + c.Check(s.Stderr(), Equals, "") + c.Check(n, Equals, 1) +} diff -Nru snapd-2.47.1+20.10.1build1/cmd/snap/cmd_routine_console_conf.go snapd-2.48+21.04/cmd/snap/cmd_routine_console_conf.go --- snapd-2.47.1+20.10.1build1/cmd/snap/cmd_routine_console_conf.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48+21.04/cmd/snap/cmd_routine_console_conf.go 2020-11-19 16:51:02.000000000 +0000 @@ -0,0 +1,140 @@ +// -*- 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" + "sort" + "strings" + "sync" + "time" + + "github.com/snapcore/snapd/client" + + "github.com/jessevdk/go-flags" + "github.com/snapcore/snapd/i18n" +) + +type cmdRoutineConsoleConfStart struct { + clientMixin +} + +var shortRoutineConsoleConfStartHelp = i18n.G("Start console-conf snapd routine") +var longRoutineConsoleConfStartHelp = i18n.G(` +The console-conf-start command starts synchronization with console-conf + +This command is used by console-conf when it starts up. It delays refreshes if +there are none currently ongoing, and exits with a specific error code if there +are ongoing refreshes which console-conf should wait for before prompting the +user to begin configuring the device. +`) + +// TODO: move these to their own package for unified time constants for how +// often or long we do things like waiting for a reboot, etc. ? +var snapdAPIInterval = 2 * time.Second +var snapdWaitForFullSystemReboot = 10 * time.Minute + +func init() { + c := addRoutineCommand("console-conf-start", shortRoutineConsoleConfStartHelp, longRoutineConsoleConfStartHelp, func() flags.Commander { + return &cmdRoutineConsoleConfStart{} + }, nil, nil) + c.hidden = true +} + +func printfFunc(msg string, format ...interface{}) func() { + return func() { + fmt.Fprintf(Stderr, msg, format...) + } +} + +func (x *cmdRoutineConsoleConfStart) Execute(args []string) error { + var snapdReloadMsgOnce, systemReloadMsgOnce, snapRefreshMsgOnce sync.Once + + for { + chgs, snaps, err := x.client.InternalConsoleConfStart() + if err != nil { + // snapd may be under maintenance right now, either for base/kernel + // snap refreshes which result in a reboot, or for snapd itself + // which just results in a restart of the daemon + maybeMaintErr := x.client.Maintenance() + if maybeMaintErr == nil { + // not a maintenance error, give up + return err + } + + maintErr, ok := maybeMaintErr.(*client.Error) + if !ok { + // if cli.Maintenance() didn't return a client.Error we have very weird + // problems + return fmt.Errorf("internal error: client.Maintenance() didn't return a client.Error") + } + + if maintErr.Kind == client.ErrorKindDaemonRestart { + // then we need to wait for snapd to restart, so keep trying + // the console-conf-start endpoint until it works + snapdReloadMsgOnce.Do(printfFunc("Snapd is reloading, please wait...\n")) + + // we know that snapd isn't available because it is in + // maintenance so we don't gain anything by hitting it + // more frequently except for perhaps a quicker latency + // for the user when it comes back, but it will be busy + // doing things when it starts up anyways so it won't be + // able to respond immediately + time.Sleep(snapdAPIInterval) + continue + } else if maintErr.Kind == client.ErrorKindSystemRestart { + // system is rebooting, just wait for the reboot + systemReloadMsgOnce.Do(printfFunc("System is rebooting, please wait for reboot...\n")) + time.Sleep(snapdWaitForFullSystemReboot) + // if we didn't reboot after 10 minutes something's probably broken + return fmt.Errorf("system didn't reboot after 10 minutes even though snapd daemon is in maintenance") + } + } + + if len(chgs) == 0 { + return nil + } + + if len(snaps) == 0 { + // internal error if we have chg id's, but no snaps + return fmt.Errorf("internal error: returned changes (%v) but no snap names", chgs) + } + + snapRefreshMsgOnce.Do(func() { + sort.Strings(snaps) + + var snapNameList string + switch len(snaps) { + case 1: + snapNameList = snaps[0] + case 2: + snapNameList = fmt.Sprintf("%s and %s", snaps[0], snaps[1]) + default: + // don't forget the oxford comma! + snapNameList = fmt.Sprintf("%s, and %s", strings.Join(snaps[:len(snaps)-1], ", "), snaps[len(snaps)-1]) + } + + fmt.Fprintf(Stderr, "Snaps (%s) are refreshing, please wait...\n", snapNameList) + }) + + // don't DDOS snapd by hitting it's API too often + time.Sleep(snapdAPIInterval) + } +} diff -Nru snapd-2.47.1+20.10.1build1/cmd/snap/cmd_routine_console_conf_test.go snapd-2.48+21.04/cmd/snap/cmd_routine_console_conf_test.go --- snapd-2.47.1+20.10.1build1/cmd/snap/cmd_routine_console_conf_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48+21.04/cmd/snap/cmd_routine_console_conf_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -0,0 +1,496 @@ +// -*- 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 ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "os" + "path/filepath" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/client" + snap "github.com/snapcore/snapd/cmd/snap" + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/testutil" +) + +func (s *SnapSuite) TestRoutineConsoleConfStartTrivialCase(c *C) { + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + n++ + switch n { + case 1: + c.Check(r.Method, Equals, "POST") + c.Check(r.URL.Path, Equals, "/v2/internal/console-conf-start") + + fmt.Fprintf(w, `{"type":"sync", "status-code": 200, "result": {}}`) + default: + c.Errorf("unexpected request %v", n) + } + }) + + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"routine", "console-conf-start"}) + c.Assert(err, IsNil) + c.Check(s.Stdout(), Equals, "") + c.Check(s.Stderr(), Equals, "") + c.Assert(n, Equals, 1) +} + +func (s *SnapSuite) TestRoutineConsoleConfStartInconsistentAPIResponseError(c *C) { + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + n++ + switch n { + case 1: + c.Check(r.Method, Equals, "POST") + c.Check(r.URL.Path, Equals, "/v2/internal/console-conf-start") + + // return just refresh changes but no snap ids + fmt.Fprintf(w, `{ + "type":"sync", + "status-code": 200, + "result": { + "active-auto-refreshes": ["1"] + } + }`) + default: + c.Errorf("unexpected request %v", n) + } + }) + + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"routine", "console-conf-start"}) + c.Assert(err, ErrorMatches, `internal error: returned changes .* but no snap names`) + c.Check(s.Stdout(), Equals, "") + c.Check(s.Stderr(), Equals, "") + c.Assert(n, Equals, 1) + +} + +func (s *SnapSuite) TestRoutineConsoleConfStartNonMaintenanceErrorReturned(c *C) { + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + n++ + switch n { + case 1: + c.Check(r.Method, Equals, "POST") + c.Check(r.URL.Path, Equals, "/v2/internal/console-conf-start") + + // return internal server error + fmt.Fprintf(w, `{ + "type":"error", + "status-code": 500, + "result": { + "message": "broken server" + } + }`) + default: + c.Errorf("unexpected request %v", n) + } + }) + + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"routine", "console-conf-start"}) + c.Assert(err, ErrorMatches, "broken server") + c.Check(s.Stdout(), Equals, "") + c.Check(s.Stderr(), Equals, "") + c.Assert(n, Equals, 1) +} + +func (s *SnapSuite) TestRoutineConsoleConfStartSingleSnap(c *C) { + // make the command hit the API as fast as possible for testing + r := snap.MockSnapdAPIInterval(0) + defer r() + + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + n++ + switch n { + // first 4 times we hit the API there is a snap refresh ongoing + case 1, 2, 3, 4: + c.Check(r.Method, Equals, "POST") + c.Check(r.URL.Path, Equals, "/v2/internal/console-conf-start") + + // return just refresh changes but no snap ids + fmt.Fprintf(w, `{ + "type":"sync", + "status-code": 200, + "result": { + "active-auto-refreshes": ["1"], + "active-auto-refresh-snaps": ["pc-kernel"] + } + }`) + // 5th time we return nothing as we are done + case 5: + c.Check(r.Method, Equals, "POST") + c.Check(r.URL.Path, Equals, "/v2/internal/console-conf-start") + + fmt.Fprintf(w, `{"type":"sync", "status-code": 200, "result": {}}`) + + default: + c.Errorf("unexpected request %v", n) + } + }) + + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"routine", "console-conf-start"}) + c.Assert(err, IsNil) + c.Check(s.Stdout(), Equals, "") + c.Check(s.Stderr(), Equals, "Snaps (pc-kernel) are refreshing, please wait...\n") + c.Assert(n, Equals, 5) +} + +func (s *SnapSuite) TestRoutineConsoleConfStartTwoSnaps(c *C) { + // make the command hit the API as fast as possible for testing + r := snap.MockSnapdAPIInterval(0) + defer r() + + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + n++ + switch n { + // first 4 times we hit the API there is a snap refresh ongoing + case 1, 2, 3, 4: + c.Check(r.Method, Equals, "POST") + c.Check(r.URL.Path, Equals, "/v2/internal/console-conf-start") + + // return just refresh changes but no snap ids + fmt.Fprintf(w, `{ + "type":"sync", + "status-code": 200, + "result": { + "active-auto-refreshes": ["1"], + "active-auto-refresh-snaps": ["pc-kernel","core20"] + } + }`) + // 5th time we return nothing as we are done + case 5: + c.Check(r.Method, Equals, "POST") + c.Check(r.URL.Path, Equals, "/v2/internal/console-conf-start") + + fmt.Fprintf(w, `{"type":"sync", "status-code": 200, "result": {}}`) + + default: + c.Errorf("unexpected request %v", n) + } + }) + + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"routine", "console-conf-start"}) + c.Assert(err, IsNil) + c.Check(s.Stdout(), Equals, "") + c.Check(s.Stderr(), Equals, "Snaps (core20 and pc-kernel) are refreshing, please wait...\n") + c.Assert(n, Equals, 5) +} + +func (s *SnapSuite) TestRoutineConsoleConfStartMultipleSnaps(c *C) { + // make the command hit the API as fast as possible for testing + r := snap.MockSnapdAPIInterval(0) + defer r() + + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + n++ + switch n { + // first 4 times we hit the API there are snap refreshes ongoing + case 1, 2, 3, 4: + c.Check(r.Method, Equals, "POST") + c.Check(r.URL.Path, Equals, "/v2/internal/console-conf-start") + + fmt.Fprintf(w, `{ + "type":"sync", + "status-code": 200, + "result": { + "active-auto-refreshes": ["1"], + "active-auto-refresh-snaps": ["pc-kernel","snapd","core20","pc"] + } + }`) + // 5th time we return nothing as we are done + case 5: + c.Check(r.Method, Equals, "POST") + c.Check(r.URL.Path, Equals, "/v2/internal/console-conf-start") + + fmt.Fprintf(w, `{"type":"sync", "status-code": 200, "result": {}}`) + + default: + c.Errorf("unexpected request %v", n) + } + }) + + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"routine", "console-conf-start"}) + c.Assert(err, IsNil) + c.Check(s.Stdout(), Equals, "") + c.Check(s.Stderr(), Equals, "Snaps (core20, pc, pc-kernel, and snapd) are refreshing, please wait...\n") + c.Assert(n, Equals, 5) +} + +func (s *SnapSuite) TestRoutineConsoleConfStartSnapdRefreshMaintenanceJSON(c *C) { + // make the command hit the API as fast as possible for testing + r := snap.MockSnapdAPIInterval(0) + defer r() + + // write a maintenance.json before any requests and then the first request + // should fail and see the maintenance.json and then subsequent operations + // succeed + maintErr := client.Error{ + Kind: client.ErrorKindDaemonRestart, + Message: "daemon is restarting", + } + b, err := json.Marshal(&maintErr) + c.Assert(err, IsNil) + err = os.MkdirAll(filepath.Dir(dirs.SnapdMaintenanceFile), 0755) + c.Assert(err, IsNil) + err = ioutil.WriteFile(dirs.SnapdMaintenanceFile, b, 0644) + c.Assert(err, IsNil) + + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + n++ + switch n { + // 1st time we don't respond at all to simulate what happens if the user + // triggers console-conf to start after snapd has shut down for a + // refresh + case 1: + c.Check(r.Method, Equals, "POST") + c.Check(r.URL.Path, Equals, "/v2/internal/console-conf-start") + + // 2nd time we hit the API, return an in-progress refresh + case 2: + c.Check(r.Method, Equals, "POST") + c.Check(r.URL.Path, Equals, "/v2/internal/console-conf-start") + + fmt.Fprintf(w, `{ + "type":"sync", + "status-code": 200, + "result": { + "active-auto-refreshes": ["1"], + "active-auto-refresh-snaps": ["snapd"] + } + }`) + // 3rd time we are actually done + case 3: + c.Check(r.Method, Equals, "POST") + c.Check(r.URL.Path, Equals, "/v2/internal/console-conf-start") + + fmt.Fprintf(w, `{"type":"sync", "status-code": 200, "result": {}}`) + + default: + c.Errorf("unexpected request %v", n) + } + }) + + _, err = snap.Parser(snap.Client()).ParseArgs([]string{"routine", "console-conf-start"}) + c.Assert(err, IsNil) + c.Check(s.Stdout(), Equals, "") + c.Check(s.Stderr(), testutil.Contains, "Snapd is reloading, please wait...\n") + c.Check(s.Stderr(), testutil.Contains, "Snaps (snapd) are refreshing, please wait...\n") + c.Assert(n, Equals, 3) +} + +func (s *SnapSuite) TestRoutineConsoleConfStartSystemRebootMaintenanceJSON(c *C) { + // make the command hit the API as fast as possible for testing + r := snap.MockSnapdAPIInterval(0) + defer r() + + r = snap.MockSnapdWaitForFullSystemReboot(0) + defer r() + + // write a maintenance.json before any requests and then the first request + // should fail and see the maintenance.json and then subsequent operations + // succeed + maintErr := client.Error{ + Kind: client.ErrorKindSystemRestart, + Message: "system is restarting", + } + b, err := json.Marshal(&maintErr) + c.Assert(err, IsNil) + err = os.MkdirAll(filepath.Dir(dirs.SnapdMaintenanceFile), 0755) + c.Assert(err, IsNil) + err = ioutil.WriteFile(dirs.SnapdMaintenanceFile, b, 0644) + c.Assert(err, IsNil) + + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + n++ + switch n { + // 1st time we don't respond at all to simulate what happens if the user + // triggers console-conf to start after snapd has shut down for a + // refresh + case 1: + c.Check(r.Method, Equals, "POST") + c.Check(r.URL.Path, Equals, "/v2/internal/console-conf-start") + + default: + c.Errorf("unexpected request %v", n) + } + }) + + _, err = snap.Parser(snap.Client()).ParseArgs([]string{"routine", "console-conf-start"}) + c.Assert(err, ErrorMatches, "system didn't reboot after 10 minutes even though snapd daemon is in maintenance") + c.Check(s.Stdout(), Equals, "") + c.Check(s.Stderr(), testutil.Contains, "System is rebooting, please wait for reboot...\n") + c.Assert(n, Equals, 1) +} + +func (s *SnapSuite) TestRoutineConsoleConfStartSnapdRefreshRestart(c *C) { + // make the command hit the API as fast as possible for testing + r := snap.MockSnapdAPIInterval(0) + defer r() + + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + n++ + switch n { + + // 1st time we hit the API there is a snapd snap refresh ongoing + case 1: + c.Check(r.Method, Equals, "POST") + c.Check(r.URL.Path, Equals, "/v2/internal/console-conf-start") + + fmt.Fprintf(w, `{ + "type":"sync", + "status-code": 200, + "result": { + "active-auto-refreshes": ["1"], + "active-auto-refresh-snaps": ["snapd"] + } + }`) + + // 2nd time we hit the API, set maintenance in the response + case 2: + c.Check(r.Method, Equals, "POST") + c.Check(r.URL.Path, Equals, "/v2/internal/console-conf-start") + + fmt.Fprintf(w, `{ + "type":"sync", + "status-code": 200, + "result": { + "active-auto-refreshes": ["1"], + "active-auto-refresh-snaps": ["snapd"] + }, + "maintenance": { + "kind": "daemon-restart", + "message": "daemon is restarting" + } + }`) + + // 3rd time we return nothing as if we are down for maintenance + case 3: + c.Check(r.Method, Equals, "POST") + c.Check(r.URL.Path, Equals, "/v2/internal/console-conf-start") + + // 4th time we resume responding, but still in progress + case 4: + c.Check(r.Method, Equals, "POST") + c.Check(r.URL.Path, Equals, "/v2/internal/console-conf-start") + + fmt.Fprintf(w, `{ + "type":"sync", + "status-code": 200, + "result": { + "active-auto-refreshes": ["1"], + "active-auto-refresh-snaps": ["snapd"] + } + }`) + + // 5th time we are actually done + case 5: + c.Check(r.Method, Equals, "POST") + c.Check(r.URL.Path, Equals, "/v2/internal/console-conf-start") + + fmt.Fprintf(w, `{"type":"sync", "status-code": 200, "result": {}}`) + + default: + c.Errorf("unexpected request %v", n) + } + }) + + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"routine", "console-conf-start"}) + c.Assert(err, IsNil) + c.Check(s.Stdout(), Equals, "") + c.Check(s.Stderr(), testutil.Contains, "Snapd is reloading, please wait...\n") + c.Check(s.Stderr(), testutil.Contains, "Snaps (snapd) are refreshing, please wait...\n") + c.Assert(n, Equals, 5) +} + +func (s *SnapSuite) TestRoutineConsoleConfStartKernelRefreshReboot(c *C) { + // make the command hit the API as fast as possible for testing + r := snap.MockSnapdAPIInterval(0) + defer r() + r = snap.MockSnapdWaitForFullSystemReboot(0) + defer r() + + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + n++ + switch n { + + // 1st time we hit the API there is a snapd snap refresh ongoing + case 1: + c.Check(r.Method, Equals, "POST") + c.Check(r.URL.Path, Equals, "/v2/internal/console-conf-start") + + fmt.Fprintf(w, `{ + "type":"sync", + "status-code": 200, + "result": { + "active-auto-refreshes": ["1"], + "active-auto-refresh-snaps": ["pc-kernel"] + } + }`) + + // 2nd time we hit the API, set maintenance in the response, but still + // give a valid response (so that it reads the maintenance) + case 2: + c.Check(r.Method, Equals, "POST") + c.Check(r.URL.Path, Equals, "/v2/internal/console-conf-start") + + fmt.Fprintf(w, `{ + "type":"sync", + "status-code": 200, + "result": { + "active-auto-refreshes": ["1"], + "active-auto-refresh-snaps": ["pc-kernel"] + }, + "maintenance": { + "kind": "system-restart", + "message": "system is restarting" + } + }`) + + // 3rd time we hit the API, we need to not return anything so that the + // client will inspect the error and see there is a maintenance error + case 3: + c.Check(r.Method, Equals, "POST") + c.Check(r.URL.Path, Equals, "/v2/internal/console-conf-start") + default: + c.Errorf("unexpected %s request (number %d) to %s", r.Method, n, r.URL.Path) + } + }) + + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"routine", "console-conf-start"}) + // this is the internal error, which we will hit immediately for testing, + // in a real scenario a reboot would happen OOTB from the snap client + c.Assert(err, ErrorMatches, "system didn't reboot after 10 minutes even though snapd daemon is in maintenance") + c.Check(s.Stdout(), Equals, "") + c.Check(s.Stderr(), testutil.Contains, "System is rebooting, please wait for reboot...\n") + c.Check(s.Stderr(), testutil.Contains, "Snaps (pc-kernel) are refreshing, please wait...\n") + c.Assert(n, Equals, 3) +} diff -Nru snapd-2.47.1+20.10.1build1/cmd/snap/cmd_run.go snapd-2.48+21.04/cmd/snap/cmd_run.go --- snapd-2.47.1+20.10.1build1/cmd/snap/cmd_run.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/cmd/snap/cmd_run.go 2020-11-19 16:51:02.000000000 +0000 @@ -186,6 +186,9 @@ logger.Debugf("system key mismatch detected, waiting for snapd to start responding...") for i := 0; i < timeout; i++ { + // TODO: we could also check cli.Maintenance() here too in case snapd is + // down semi-permanently for a refresh, but what message do we show to + // the user or what do we do if we know snapd is down for maintenance? if _, err := cli.SysInfo(); err == nil { return nil } diff -Nru snapd-2.47.1+20.10.1build1/cmd/snap/cmd_snap_op.go snapd-2.48+21.04/cmd/snap/cmd_snap_op.go --- snapd-2.47.1+20.10.1build1/cmd/snap/cmd_snap_op.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/cmd/snap/cmd_snap_op.go 2020-11-19 16:51:02.000000000 +0000 @@ -58,11 +58,11 @@ With no further options, the snaps are installed tracking the stable channel, with strict security confinement. -Revision choice via the --revision override requires the the user to +Revision choice via the --revision override requires the user to have developer access to the snap, either directly or through the store's collaboration feature, and to be logged in (see 'snap help login'). -Note a later refresh will typically undo a revision override, taking the snap +Note that a later refresh will typically undo a revision override, taking the snap back to the current revision of the channel it's tracking. Use --name to set the instance name when installing from snap file. @@ -87,7 +87,7 @@ With no further options, the snaps are refreshed to the current revision of the channel they're tracking, preserving their confinement options. -Revision choice via the --revision override requires the the user to +Revision choice via the --revision override requires the user to have developer access to the snap, either directly or through the store's collaboration feature, and to be logged in (see 'snap help login'). @@ -474,8 +474,9 @@ Name string `long:"name"` - Cohort string `long:"cohort"` - Positional struct { + Cohort string `long:"cohort"` + IgnoreRunning bool `long:"ignore-running" hidden:"yes"` + Positional struct { Snaps []remoteSnapName `positional-arg-name:""` } `positional-args:"yes" required:"yes"` } @@ -591,11 +592,12 @@ dangerous := x.Dangerous || x.ForceDangerous opts := &client.SnapOptions{ - Channel: x.Channel, - Revision: x.Revision, - Dangerous: dangerous, - Unaliased: x.Unaliased, - CohortKey: x.Cohort, + Channel: x.Channel, + Revision: x.Revision, + Dangerous: dangerous, + Unaliased: x.Unaliased, + CohortKey: x.Cohort, + IgnoreRunning: x.IgnoreRunning, } x.setModes(opts) @@ -637,6 +639,7 @@ List bool `long:"list"` Time bool `long:"time"` IgnoreValidation bool `long:"ignore-validation"` + IgnoreRunning bool `long:"ignore-running" hidden:"yes"` Positional struct { Snaps []installedSnapName `positional-arg-name:""` } `positional-args:"yes"` @@ -802,6 +805,7 @@ Amend: x.Amend, Channel: x.Channel, IgnoreValidation: x.IgnoreValidation, + IgnoreRunning: x.IgnoreRunning, Revision: x.Revision, CohortKey: x.Cohort, LeaveCohort: x.LeaveCohort, @@ -817,6 +821,9 @@ if x.IgnoreValidation { return errors.New(i18n.G("a single snap name must be specified when ignoring validation")) } + if x.IgnoreRunning { + return errors.New(i18n.G("a single snap name must be specified when ignoring running apps and hooks")) + } return x.refreshMany(names, nil) } @@ -970,8 +977,9 @@ waitMixin modeMixin - Revision string `long:"revision"` - Positional struct { + Revision string `long:"revision"` + IgnoreRunning bool `long:"ignore-running" hidden:"yes"` + Positional struct { Snap installedSnapName `positional-arg-name:""` } `positional-args:"yes" required:"yes"` } @@ -996,7 +1004,10 @@ } name := string(x.Positional.Snap) - opts := &client.SnapOptions{Revision: x.Revision} + opts := &client.SnapOptions{ + Revision: x.Revision, + IgnoreRunning: x.IgnoreRunning, + } x.setModes(opts) changeID, err := x.client.Revert(name, opts) if err != nil { @@ -1095,6 +1106,8 @@ "name": i18n.G("Install the snap file under the given instance name"), // TRANSLATORS: This should not start with a lowercase letter. "cohort": i18n.G("Install the snap in the given cohort"), + // TRANSLATORS: This should not start with a lowercase letter. + "ignore-running": i18n.G("Ignore running hooks or applications blocking the installation"), }), nil) addCommand("refresh", shortRefreshHelp, longRefreshHelp, func() flags.Commander { return &cmdRefresh{} }, colorDescs.also(waitDescs).also(channelDescs).also(modeDescs).also(timeDescs).also(map[string]string{ @@ -1109,6 +1122,8 @@ // TRANSLATORS: This should not start with a lowercase letter. "ignore-validation": i18n.G("Ignore validation by other snaps blocking the refresh"), // TRANSLATORS: This should not start with a lowercase letter. + "ignore-running": i18n.G("Ignore running hooks or applications blocking the refresh"), + // TRANSLATORS: This should not start with a lowercase letter. "cohort": i18n.G("Refresh the snap into the given cohort"), // TRANSLATORS: This should not start with a lowercase letter. "leave-cohort": i18n.G("Refresh the snap out of its cohort"), @@ -1119,6 +1134,8 @@ addCommand("revert", shortRevertHelp, longRevertHelp, func() flags.Commander { return &cmdRevert{} }, waitDescs.also(modeDescs).also(map[string]string{ // TRANSLATORS: This should not start with a lowercase letter. "revision": i18n.G("Revert to the given revision"), + // TRANSLATORS: This should not start with a lowercase letter. + "ignore-running": i18n.G("Ignore running hooks or applications blocking the revert"), }), nil) addCommand("switch", shortSwitchHelp, longSwitchHelp, func() flags.Commander { return &cmdSwitch{} }, waitDescs.also(channelDescs).also(map[string]string{ // TRANSLATORS: This should not start with a lowercase letter. diff -Nru snapd-2.47.1+20.10.1build1/cmd/snap/cmd_snap_op_test.go snapd-2.48+21.04/cmd/snap/cmd_snap_op_test.go --- snapd-2.47.1+20.10.1build1/cmd/snap/cmd_snap_op_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/cmd/snap/cmd_snap_op_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -207,6 +207,25 @@ c.Check(s.srv.n, check.Equals, s.srv.total) } +func (s *SnapOpSuite) TestInstallIgnoreRunning(c *check.C) { + s.srv.checker = func(r *http.Request) { + c.Check(r.URL.Path, check.Equals, "/v2/snaps/foo") + c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{ + "action": "install", + "ignore-running": true, + }) + } + + s.RedirectClientToTestServer(s.srv.handle) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "--ignore-running", "foo"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Matches, `(?sm).*foo 1.0 from Bar installed`) + c.Check(s.Stderr(), check.Equals, "") + // ensure that the fake server api was actually hit + c.Check(s.srv.n, check.Equals, s.srv.total) +} + func (s *SnapOpSuite) TestInstallNoPATH(c *check.C) { // PATH restored by test tear down os.Setenv("PATH", "/bin:/usr/bin:/sbin:/usr/sbin") diff -Nru snapd-2.47.1+20.10.1build1/cmd/snap/cmd_snapshot.go snapd-2.48+21.04/cmd/snap/cmd_snapshot.go --- snapd-2.47.1+20.10.1build1/cmd/snap/cmd_snapshot.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/cmd/snap/cmd_snapshot.go 2020-11-19 16:51:02.000000000 +0000 @@ -44,6 +44,7 @@ shortCheckHelp = i18n.G("Check a snapshot") shortRestoreHelp = i18n.G("Restore a snapshot") shortExportSnapshotHelp = i18n.G("Export a snapshot") + shortImportSnapshotHelp = i18n.G("Import a snapshot") ) var longSavedHelp = i18n.G(` @@ -105,6 +106,11 @@ Export a snapshot to the given filename. `) +var longImportSnapshotHelp = i18n.G(` +Import an exported snapshot set to the system. The snapshot is imported +with a new snapshot ID and can be restored using the restore command. +`) + type savedCmd struct { clientMixin durationMixin @@ -132,6 +138,7 @@ fmt.Fprintln(Stdout, i18n.G("No snapshots found.")) return nil } + w := tabWriter() defer w.Flush() @@ -392,7 +399,7 @@ }, }) - cmd := addCommand("export-snapshot", + addCommand("export-snapshot", shortExportSnapshotHelp, longExportSnapshotHelp, func() flags.Commander { @@ -410,10 +417,19 @@ desc: i18n.G("The filename of the export"), }, }) - // This command is hidden because there's no corresponding - // "import-snapshot" to consume the produced data. - // TODO: implement import-snapshot and remove the hidden attribute. - cmd.hidden = true + + addCommand("import-snapshot", + shortImportSnapshotHelp, + longImportSnapshotHelp, + func() flags.Commander { + return &importSnapshotCmd{} + }, nil, []argDesc{ + { + name: "", + // TRANSLATORS: This should not start with a lowercase letter. + desc: i18n.G("Name of the snapshot export file to use"), + }, + }) } type exportSnapshotCmd struct { @@ -466,6 +482,42 @@ // TRANSLATORS: the first argument is the identifier of the snapshot, the second one is the file name. fmt.Fprintf(Stdout, i18n.G("Exported snapshot #%s into %q\n"), x.Positional.ID, x.Positional.Filename) - return nil } + +type importSnapshotCmd struct { + clientMixin + durationMixin + Positional struct { + Filename string `long:"filename"` + } `positional-args:"yes" required:"yes"` +} + +func (x *importSnapshotCmd) Execute([]string) error { + filename := x.Positional.Filename + f, err := os.Open(filename) + if err != nil { + return fmt.Errorf("error accessing file: %v", err) + } + defer f.Close() + st, err := f.Stat() + if err != nil { + return fmt.Errorf("cannot stat file: %v", err) + } + + importSet, err := x.client.SnapshotImport(f, st.Size()) + if err != nil { + return err + } + + fmt.Fprintf(Stdout, i18n.G("Imported snapshot as #%d\n"), importSet.ID) + // Now display the details about this snapshot, re-use the + // "snap saved" command for this which displays details about + // the snapshot. + y := &savedCmd{ + clientMixin: x.clientMixin, + durationMixin: x.durationMixin, + ID: snapshotID(strconv.FormatUint(importSet.ID, 10)), + } + return y.Execute(nil) +} diff -Nru snapd-2.47.1+20.10.1build1/cmd/snap/cmd_snapshot_test.go snapd-2.48+21.04/cmd/snap/cmd_snapshot_test.go --- snapd-2.47.1+20.10.1build1/cmd/snap/cmd_snapshot_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/cmd/snap/cmd_snapshot_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -21,6 +21,7 @@ import ( "fmt" + "io/ioutil" "net/http" "path/filepath" "strings" @@ -28,7 +29,9 @@ . "gopkg.in/check.v1" + "github.com/snapcore/snapd/client" main "github.com/snapcore/snapd/cmd/snap" + "github.com/snapcore/snapd/strutil/quantity" "github.com/snapcore/snapd/testutil" ) @@ -122,16 +125,45 @@ return } fmt.Fprintf(w, `{"type":"sync","status-code":200,"status":"OK","result":[{"id":1,"snapshots":[{"set":1,"time":%q,"snap":"htop","revision":"1168","snap-id":"Z","epoch":{"read":[0],"write":[0]},"summary":"","version":"2","sha3-384":{"archive.tgz":""},"size":1}]}]}`, snapshotTime) - } else { - w.WriteHeader(202) - fmt.Fprintln(w, `{"type":"async", "status-code": 202, "change": "9"}`) + } + if r.Method == "POST" { + if r.Header.Get("Content-Type") == client.SnapshotExportMediaType { + fmt.Fprintln(w, `{"type": "sync", "result": {"set-id": 42, "snaps": ["htop"]}}`) + } else { + + w.WriteHeader(202) + fmt.Fprintln(w, `{"type":"async", "status-code": 202, "change": "9"}`) + } } case "/v2/changes/9": fmt.Fprintln(w, `{"type": "sync", "result": {"ready": true, "status": "Done", "data": {}}}`) case "/v2/snapshots/1/export": + w.Header().Set("Content-Type", client.SnapshotExportMediaType) fmt.Fprint(w, "Hello World!") default: c.Errorf("unexpected path %q", r.URL.Path) } }) } + +func (s *SnapSuite) TestSnapshotImportHappy(c *C) { + // mockSnapshotServer will return set-id 42 and three snaps for all + // import calls + s.mockSnapshotsServer(c) + + // time may be crossing DST change, so the age value should not be + // hardcoded, otherwise we'll see failures for 2 montsh during the year + expectedAge := time.Since(time.Now().AddDate(0, -1, 0)) + ageStr := quantity.FormatDuration(expectedAge.Seconds()) + + exportedSnapshotPath := filepath.Join(c.MkDir(), "mocked-snapshot.snapshot") + ioutil.WriteFile(exportedSnapshotPath, []byte("this is really snapshot zip file data"), 0644) + + _, err := main.Parser(main.Client()).ParseArgs([]string{"import-snapshot", exportedSnapshotPath}) + c.Check(err, IsNil) + c.Check(s.Stderr(), testutil.EqualsWrapped, "") + c.Check(s.Stdout(), testutil.MatchesWrapped, fmt.Sprintf(`Imported snapshot as #42 +Set Snap Age Version Rev Size Notes +1 htop %-6s 2 1168 1B - +`, ageStr)) +} diff -Nru snapd-2.47.1+20.10.1build1/cmd/snap/cmd_version.go snapd-2.48+21.04/cmd/snap/cmd_version.go --- snapd-2.47.1+20.10.1build1/cmd/snap/cmd_version.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/cmd/snap/cmd_version.go 2020-11-19 16:51:02.000000000 +0000 @@ -48,8 +48,7 @@ return ErrExtraArgs } - printVersions(cmd.client) - return nil + return printVersions(cmd.client) } func printVersions(cli *client.Client) error { diff -Nru snapd-2.47.1+20.10.1build1/cmd/snap/cmd_warnings.go snapd-2.48+21.04/cmd/snap/cmd_warnings.go --- snapd-2.47.1+20.10.1build1/cmd/snap/cmd_warnings.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/cmd/snap/cmd_warnings.go 2020-11-19 16:51:02.000000000 +0000 @@ -159,7 +159,7 @@ } func writeWarningTimestamp(t time.Time) error { - user, err := osutil.RealUser() + user, err := osutil.UserMaybeSudoUser() if err != nil { return err } @@ -189,7 +189,7 @@ } func lastWarningTimestamp() (time.Time, error) { - user, err := osutil.RealUser() + user, err := osutil.UserMaybeSudoUser() if err != nil { return time.Time{}, fmt.Errorf("cannot determine real user: %v", err) } diff -Nru snapd-2.47.1+20.10.1build1/cmd/snap/export_test.go snapd-2.48+21.04/cmd/snap/export_test.go --- snapd-2.47.1+20.10.1build1/cmd/snap/export_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/cmd/snap/export_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -384,3 +384,19 @@ downloadDirect = old } } + +func MockSnapdAPIInterval(t time.Duration) (restore func()) { + old := snapdAPIInterval + snapdAPIInterval = t + return func() { + snapdAPIInterval = old + } +} + +func MockSnapdWaitForFullSystemReboot(t time.Duration) (restore func()) { + old := snapdWaitForFullSystemReboot + snapdWaitForFullSystemReboot = t + return func() { + snapdWaitForFullSystemReboot = old + } +} diff -Nru snapd-2.47.1+20.10.1build1/cmd/snap/main.go snapd-2.48+21.04/cmd/snap/main.go --- snapd-2.47.1+20.10.1build1/cmd/snap/main.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/cmd/snap/main.go 2020-11-19 16:51:02.000000000 +0000 @@ -30,6 +30,8 @@ "unicode" "unicode/utf8" + "golang.org/x/xerrors" + "github.com/jessevdk/go-flags" "golang.org/x/crypto/ssh/terminal" @@ -41,6 +43,7 @@ "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/release" "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/snap/squashfs" "github.com/snapcore/snapd/snapdenv" "github.com/snapcore/snapd/snapdtool" ) @@ -423,6 +426,28 @@ return snapApp, nil } +// exitCodeFromError takes an error and returns specific exit codes +// for some errors. Otherwise the generic exit code 1 is returned. +func exitCodeFromError(err error) int { + var mksquashfsError squashfs.MksquashfsError + var cmdlineFlagsError *flags.Error + var unknownCmdError unknownCommandError + + switch { + case err == nil: + return 0 + case client.IsRetryable(err): + return 10 + case xerrors.As(err, &mksquashfsError): + return 20 + case xerrors.As(err, &cmdlineFlagsError) || xerrors.As(err, &unknownCmdError): + // EX_USAGE, see sysexit.h + return 64 + default: + return 1 + } +} + func main() { snapdtool.ExecInSnapdOrCoreSnap() @@ -480,10 +505,7 @@ // no magic /o\ if err := run(); err != nil { fmt.Fprintf(Stderr, errorPrefix, err) - if client.IsRetryable(err) { - os.Exit(10) - } - os.Exit(1) + os.Exit(exitCodeFromError(err)) } } @@ -508,6 +530,14 @@ 0x2e3b, // three-em dash }) +type unknownCommandError struct { + msg string +} + +func (e unknownCommandError) Error() string { + return e.msg +} + func run() error { cli := mkClient() parser := Parser(cli) @@ -531,7 +561,7 @@ } } // TRANSLATORS: %q is the command the user entered; %s is 'snap help' or 'snap help ' - return fmt.Errorf(i18n.G("unknown command %q, see '%s'."), sub, sug) + return unknownCommandError{fmt.Sprintf(i18n.G("unknown command %q, see '%s'."), sub, sug)} } } diff -Nru snapd-2.47.1+20.10.1build1/cmd/snap/main_test.go snapd-2.48+21.04/cmd/snap/main_test.go --- snapd-2.47.1+20.10.1build1/cmd/snap/main_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/cmd/snap/main_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -98,6 +98,10 @@ s.AddCleanup(snap.MockIsStdinTTY(false)) s.AddCleanup(snap.MockSELinuxIsEnabled(func() (bool, error) { return false, nil })) + + // mock an empty cmdline since we check the cmdline to check whether we are + // in install mode or not and we don't want to use the host's proc/cmdline + s.AddCleanup(osutil.MockProcCmdline(filepath.Join(c.MkDir(), "proc/cmdline"))) } func (s *BaseSnapSuite) TearDownTest(c *C) { diff -Nru snapd-2.47.1+20.10.1build1/cmd/snap-bootstrap/cmd_initramfs_mounts.go snapd-2.48+21.04/cmd/snap-bootstrap/cmd_initramfs_mounts.go --- snapd-2.47.1+20.10.1build1/cmd/snap-bootstrap/cmd_initramfs_mounts.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/cmd/snap-bootstrap/cmd_initramfs_mounts.go 2020-11-19 16:51:02.000000000 +0000 @@ -20,6 +20,8 @@ package main import ( + "crypto/subtle" + "encoding/json" "fmt" "io/ioutil" "os" @@ -32,6 +34,7 @@ "github.com/snapcore/snapd/asserts" "github.com/snapcore/snapd/boot" "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/logger" "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/osutil/disks" "github.com/snapcore/snapd/overlord/state" @@ -74,9 +77,12 @@ snap.TypeSnapd: "snapd", } - secbootMeasureSnapSystemEpochWhenPossible = secboot.MeasureSnapSystemEpochWhenPossible - secbootMeasureSnapModelWhenPossible = secboot.MeasureSnapModelWhenPossible - secbootUnlockVolumeIfEncrypted = secboot.UnlockVolumeIfEncrypted + secbootMeasureSnapSystemEpochWhenPossible func() error + secbootMeasureSnapModelWhenPossible func(findModel func() (*asserts.Model, error)) error + secbootUnlockVolumeUsingSealedKeyIfEncrypted func(disk disks.Disk, name string, encryptionKeyFile string, opts *secboot.UnlockVolumeUsingSealedKeyOptions) (secboot.UnlockResult, error) + secbootUnlockEncryptedVolumeUsingKey func(disk disks.Disk, name string, key []byte) (secboot.UnlockResult, error) + + secbootLockTPMSealedKeys func() error bootFindPartitionUUIDForBootedKernelDisk = boot.FindPartitionUUIDForBootedKernelDisk ) @@ -95,9 +101,23 @@ return ioutil.WriteFile(stampFile, nil, 0644) } -func generateInitramfsMounts() error { +func generateInitramfsMounts() (err error) { + // ensure that the last thing we do is to lock access to sealed keys, + // regardless of mode or early failures. + defer func() { + if e := secbootLockTPMSealedKeys(); e != nil { + e = fmt.Errorf("error locking access to sealed keys: %v", e) + if err == nil { + err = e + } else { + // preserve err but log + logger.Noticef("%v", e) + } + } + }() + // Ensure there is a very early initial measurement - err := stampedAction("secboot-epoch-measured", func() error { + err = stampedAction("secboot-epoch-measured", func() error { return secbootMeasureSnapSystemEpochWhenPossible() }) if err != nil { @@ -130,7 +150,7 @@ // no longer generates more mount points and just returns an empty output. func generateMountsModeInstall(mst *initramfsMountsState) error { // steps 1 and 2 are shared with recover mode - if err := generateMountsCommonInstallRecover(mst); err != nil { + if _, err := generateMountsCommonInstallRecover(mst); err != nil { return err } @@ -176,7 +196,28 @@ return nil } -// copyUbuntuDataAuth copies the authenication files like +// copyUbuntuDataMisc copies miscellaneous other files from the run mode system +// to the recover system such as: +// - timesync clock to keep the same time setting in recover as in run mode +func copyUbuntuDataMisc(src, dst string) error { + for _, globEx := range []string{ + // systemd's timesync clock file so that the time in recover mode moves + // forward to what it was in run mode + // NOTE: we don't sync back the time movement from recover mode to run + // mode currently, unclear how/when we could do this, but recover mode + // isn't meant to be long lasting and as such it's probably not a big + // problem to "lose" the time spent in recover mode + "system-data/var/lib/systemd/timesync/clock", + } { + if err := copyFromGlobHelper(src, dst, globEx); err != nil { + return err + } + } + + return nil +} + +// copyUbuntuDataAuth copies the authentication files like // - extrausers passwd,shadow etc // - sshd host configuration // - user .ssh dir @@ -195,13 +236,6 @@ // so that users have proper perms, i.e. console-conf added users are // sudoers "system-data/etc/sudoers.d/*", - // so that the time in recover mode moves forward to what it was in run - // mode - // NOTE: we don't sync back the time movement from recover mode to run - // mode currently, unclear how/when we could do this, but recover mode - // isn't meant to be long lasting and as such it's probably not a big - // problem to "lose" the time spent in recover mode - "system-data/var/lib/systemd/timesync/clock", } { if err := copyFromGlobHelper(src, dst, globEx); err != nil { return err @@ -219,6 +253,18 @@ return nil } +// copySafeDefaultData will copy to the destination a "safe" set of data for +// a blank recover mode, i.e. one where we cannot copy authentication, etc. from +// the actual host ubuntu-data. Currently this is just a file to disable +// console-conf from running. +func copySafeDefaultData(dst string) error { + consoleConfCompleteFile := filepath.Join(dst, "system-data/var/lib/console-conf/complete") + if err := os.MkdirAll(filepath.Dir(consoleConfCompleteFile), 0755); err != nil { + return err + } + return ioutil.WriteFile(consoleConfCompleteFile, nil, 0644) +} + func copyFromGlobHelper(src, dst, globEx string) error { matches, err := filepath.Glob(filepath.Join(src, globEx)) if err != nil { @@ -254,56 +300,797 @@ return nil } -func generateMountsModeRecover(mst *initramfsMountsState) error { - // steps 1 and 2 are shared with install mode - if err := generateMountsCommonInstallRecover(mst); err != nil { - return err +// states for partition state +const ( + // states for LocateState + partitionFound = "found" + partitionNotFound = "not-found" + partitionErrFinding = "error-finding" + // states for MountState + partitionMounted = "mounted" + partitionErrMounting = "error-mounting" + partitionAbsentOptional = "absent-but-optional" + partitionMountedUntrusted = "mounted-untrusted" + // states for UnlockState + partitionUnlocked = "unlocked" + partitionErrUnlocking = "error-unlocking" + // keys used to unlock for UnlockKey + keyRun = "run" + keyFallback = "fallback" + keyRecovery = "recovery" +) + +// partitionState is the state of a partition after recover mode has completed +// for degraded mode. +type partitionState struct { + // MountState is whether the partition was mounted successfully or not. + MountState string `json:"mount-state,omitempty"` + // MountLocation is where the partition was mounted. + MountLocation string `json:"mount-location,omitempty"` + // Device is what device the partition corresponds to. It can be the + // physical block device if the partition is unencrypted or if it was not + // successfully unlocked, or it can be a decrypted mapper device if the + // partition was encrypted and successfully decrypted, or it can be the + // empty string (or missing) if the partition was not found at all. + Device string `json:"device,omitempty"` + // FindState indicates whether the partition was found on the disk or not. + FindState string `json:"find-state,omitempty"` + // UnlockState was whether the partition was unlocked successfully or not. + UnlockState string `json:"unlock-state,omitempty"` + // UnlockKey was what key the partition was unlocked with, either "run", + // "fallback" or "recovery". + UnlockKey string `json:"unlock-key,omitempty"` + + // unexported internal fields for tracking the device, these are used during + // state machine execution, and then combined into Device during finalize() + // for simple representation to the consumer of degraded.json + + // fsDevice is what decrypted mapper device corresponds to the + // partition, it can have the following states + // - successfully decrypted => the decrypted mapper device + // - unencrypted => the block device of the partition + // - identified as decrypted, but failed to decrypt => empty string + fsDevice string + // partDevice is always the physical block device of the partition, in the + // encrypted case this is the physical encrypted partition. + partDevice string +} + +type recoverDegradedState struct { + // UbuntuData is the state of the ubuntu-data (or ubuntu-data-enc) + // partition. + UbuntuData partitionState `json:"ubuntu-data,omitempty"` + // UbuntuBoot is the state of the ubuntu-boot partition. + UbuntuBoot partitionState `json:"ubuntu-boot,omitempty"` + // UbuntuSave is the state of the ubuntu-save (or ubuntu-save-enc) + // partition. + UbuntuSave partitionState `json:"ubuntu-save,omitempty"` + // ErrorLog is the log of error messages encountered during recover mode + // setting up degraded mode. + ErrorLog []string `json:"error-log"` +} + +func (r *recoverDegradedState) partition(part string) *partitionState { + switch part { + case "ubuntu-data": + return &r.UbuntuData + case "ubuntu-boot": + return &r.UbuntuBoot + case "ubuntu-save": + return &r.UbuntuSave } + panic(fmt.Sprintf("unknown partition %s", part)) +} - // get the disk that we mounted the ubuntu-seed partition from as a - // reference point for future mounts - disk, err := disks.DiskFromMountPoint(boot.InitramfsUbuntuSeedDir, nil) +func (r *recoverDegradedState) LogErrorf(format string, v ...interface{}) { + msg := fmt.Sprintf(format, v...) + r.ErrorLog = append(r.ErrorLog, msg) + logger.Noticef(msg) +} + +// stateFunc is a function which executes a state action, returns the next +// function (for the next) state or nil if it is the final state. +type stateFunc func() (stateFunc, error) + +// recoverModeStateMachine is a state machine implementing the logic for degraded recover +// mode. the following state diagram shows the logic for the various states and +// transitions: +/** + + +TODO: the state diagram is out-of-date, locate save unencrypted is being +taken care via unlock save w/ fallback key which also works for +unencrypted save + + + +---------+ +----------+ + | start | | mount | fail + | +------------------->+ boot +------------------------+ + | | | | | + +---------+ +----+-----+ | + | | + success | | + | | + v v + fail or +-------------------+ fail, +----+------+ fail, +--------+-------+ + not needed | locate save | unencrypt |unlock data| encrypted | unlock data w/ | + +--------------+ unencrypted +<-----------+w/ run key +--------------+ fallback key +-------+ + | | | | | | | | + | +--------+----------+ +-----+-----+ +--------+-------+ | + | | | | | + | |success |success | | + | | | success | fail | + v v v | | ++---+---+ +-------+----+ +-------+----+ | | +| | | mount | success | mount data | | | +| done +<----------+ save | +---------+ +<---------------------------+ | +| | | | | | | | ++--+----+ +----+-------+ | +----------+-+ | + ^ ^ | | | + | | success v | | + | | +--------+----+ fail |fail | + | | | unlock save +--------+ | | + | +-----+ w/ run key | v v | + | ^ +-------------+ +----+------+-----+ | + | | | unlock save | | + | | | w/ fallback key +----------------------------------------+ + | +-----------------------+ | + | success +-------+---------+ + | | + | | + | | + +-----------------------------------------------------+ + fail + +*/ + +type recoverModeStateMachine struct { + // the current state is the one that is about to be executed + current stateFunc + + // device model + model *asserts.Model + + // the disk we have all our partitions on + disk disks.Disk + + // TODO:UC20: for clarity turn this into into tristate: + // unknown|encrypted|unencrypted + isEncryptedDev bool + + // state for tracking what happens as we progress through degraded mode of + // recovery + degradedState *recoverDegradedState +} + +// degraded returns whether a degraded recover mode state has fallen back from +// the typical operation to some sort of degraded mode. +func (m *recoverModeStateMachine) degraded() bool { + r := m.degradedState + + if m.isEncryptedDev { + // for encrypted devices, we need to have ubuntu-save mounted + if r.UbuntuSave.MountState != partitionMounted { + return true + } + + // we also should have all the unlock keys as run keys + if r.UbuntuData.UnlockKey != keyRun { + return true + } + + if r.UbuntuSave.UnlockKey != keyRun { + return true + } + } else { + // for unencrypted devices, ubuntu-save must either be mounted or + // absent-but-optional + if r.UbuntuSave.MountState != partitionMounted { + if r.UbuntuSave.MountState != partitionAbsentOptional { + return true + } + } + } + + // ubuntu-boot and ubuntu-data should both be mounted + if r.UbuntuBoot.MountState != partitionMounted { + return true + } + if r.UbuntuData.MountState != partitionMounted { + return true + } + + // TODO: should we also check MountLocation too? + + // we should have nothing in the error log + if len(r.ErrorLog) != 0 { + return true + } + + return false +} + +func (m *recoverModeStateMachine) diskOpts() *disks.Options { + if m.isEncryptedDev { + return &disks.Options{ + IsDecryptedDevice: true, + } + } + return nil +} + +func (m *recoverModeStateMachine) verifyMountPoint(dir, name string) error { + matches, err := m.disk.MountPointIsFromDisk(dir, m.diskOpts()) if err != nil { return err } + if !matches { + return fmt.Errorf("cannot validate mount: %s mountpoint target %s is expected to be from disk %s but is not", name, dir, m.disk.Dev()) + } + return nil +} - // 3. mount ubuntu-data for recovery - const lockKeysOnFinish = true - device, isDecryptDev, err := secbootUnlockVolumeIfEncrypted(disk, "ubuntu-data", boot.InitramfsEncryptionKeyDir, lockKeysOnFinish) +func (m *recoverModeStateMachine) setFindState(partName, partUUID string, err error) error { + part := m.degradedState.partition(partName) if err != nil { + if _, ok := err.(disks.FilesystemLabelNotFoundError); ok { + // explicit error that the device was not found + part.FindState = partitionNotFound + m.degradedState.LogErrorf("cannot find %v partition on disk %s", partName, m.disk.Dev()) + return nil + } + // the error is not "not-found", so we have a real error + part.FindState = partitionErrFinding + m.degradedState.LogErrorf("error finding %v partition on disk %s: %v", partName, m.disk.Dev(), err) + return nil + } + + // device was found + part.FindState = partitionFound + dev := fmt.Sprintf("/dev/disk/by-partuuid/%s", partUUID) + part.partDevice = dev + part.fsDevice = dev + return nil +} + +func (m *recoverModeStateMachine) setMountState(part, where string, err error) error { + if err != nil { + m.degradedState.LogErrorf("cannot mount %v: %v", part, err) + m.degradedState.partition(part).MountState = partitionErrMounting + return nil + } + + m.degradedState.partition(part).MountState = partitionMounted + m.degradedState.partition(part).MountLocation = where + + if err := m.verifyMountPoint(where, part); err != nil { + m.degradedState.LogErrorf("cannot verify %s mount point at %v: %v", part, where, err) return err } + return nil +} + +func (m *recoverModeStateMachine) setUnlockStateWithRunKey(partName string, unlockRes secboot.UnlockResult, err error) error { + part := m.degradedState.partition(partName) + // save the device if we found it from secboot + if unlockRes.PartDevice != "" { + part.FindState = partitionFound + part.partDevice = unlockRes.PartDevice + part.fsDevice = unlockRes.FsDevice + } else { + part.FindState = partitionNotFound + } + if unlockRes.IsEncrypted { + m.isEncryptedDev = true + } + + if err != nil { + // create different error message for encrypted vs unencrypted + if unlockRes.IsEncrypted { + // if we know the device is decrypted we must also always know at + // least the partDevice (which is the encrypted block device) + m.degradedState.LogErrorf("cannot unlock encrypted %s (device %s) with sealed run key: %v", partName, part.partDevice, err) + part.UnlockState = partitionErrUnlocking + } else { + // TODO: we don't know if this is a plain not found or a different error + m.degradedState.LogErrorf("cannot locate %s partition for mounting host data: %v", partName, err) + } + + return nil + } + + if unlockRes.IsEncrypted { + // unlocked successfully + part.UnlockState = partitionUnlocked + part.UnlockKey = keyRun + } + + return nil +} + +func (m *recoverModeStateMachine) setUnlockStateWithFallbackKey(partName string, unlockRes secboot.UnlockResult, err error, partitionOptional bool) error { + // first check the result and error for consistency; since we are using udev + // there could be inconsistent results at different points in time + + // TODO: consider refactoring UnlockVolumeUsingSealedKeyIfEncrypted to not + // also find the partition on the disk, that should eliminate this + // consistency checking as we can code it such that we don't get these + // possible inconsistencies + + // do basic consistency checking on unlockRes to make sure the + // result makes sense. + if unlockRes.FsDevice != "" && err != nil { + // This case should be impossible to enter, we can't + // have a filesystem device but an error set + return fmt.Errorf("internal error: inconsistent return values from UnlockVolumeUsingSealedKeyIfEncrypted for partition %s: %v", partName, err) + } + + part := m.degradedState.partition(partName) + // Also make sure that if we previously saw a partition device that we see + // the same device again. + if unlockRes.PartDevice != "" && part.partDevice != "" && unlockRes.PartDevice != part.partDevice { + return fmt.Errorf("inconsistent partitions found for %s: previously found %s but now found %s", partName, part.partDevice, unlockRes.PartDevice) + } + + // ensure consistency between encrypted state of the device/disk and what we + // may have seen previously + if m.isEncryptedDev && !unlockRes.IsEncrypted { + // then we previously were able to positively identify an + // ubuntu-data-enc but can't anymore, so we have inconsistent results + // from inspecting the disk which is suspicious and we should fail + return fmt.Errorf("inconsistent disk encryption status: previous access resulted in encrypted, but now is unencrypted from partition %s", partName) + } + + // now actually process the result into the state + if unlockRes.PartDevice != "" { + part.FindState = partitionFound + // Note that in some case this may be redundantly assigning the same + // value to partDevice again. + part.partDevice = unlockRes.PartDevice + part.fsDevice = unlockRes.FsDevice + } + + // There are a few cases where this could be the first time that we found a + // decrypted device in the UnlockResult, but m.isEncryptedDev is still + // false. + // - The first case is if we couldn't find ubuntu-boot at all, in which case + // we can't use the run object keys from there and instead need to directly + // fallback to trying the fallback object keys from ubuntu-seed + // - The second case is if we couldn't identify an ubuntu-data-enc or an + // ubuntu-data partition at all, we still could have an ubuntu-save-enc + // partition in which case we maybe could still have an encrypted disk that + // needs unlocking with the fallback object keys from ubuntu-seed + // + // As such, if m.isEncryptedDev is false, but unlockRes.IsEncrypted is + // true, then it is safe to assign m.isEncryptedDev to true. + if !m.isEncryptedDev && unlockRes.IsEncrypted { + m.isEncryptedDev = true + } + + if err != nil { + // create different error message for encrypted vs unencrypted + if m.isEncryptedDev { + m.degradedState.LogErrorf("cannot unlock encrypted %s partition with sealed fallback key: %v", partName, err) + part.UnlockState = partitionErrUnlocking + } else { + // if we don't have an encrypted device and err != nil, then the + // device must be not-found, see above checks + + // if the partition is optional (like ubuntu-save is) then don't + // report an error for ubuntu-save not being found and also set it + // as absent-but-optional + if unlockRes.PartDevice == "" && partitionOptional { + part.MountState = partitionAbsentOptional + } else { + // log the error the partition is mandatory + m.degradedState.LogErrorf("cannot locate %s partition: %v", partName, err) + } + } + + return nil + } + + if m.isEncryptedDev { + // unlocked successfully + part.UnlockState = partitionUnlocked + + // figure out which key/method we used to unlock the partition + switch unlockRes.UnlockMethod { + case secboot.UnlockedWithSealedKey: + part.UnlockKey = keyFallback + case secboot.UnlockedWithRecoveryKey: + part.UnlockKey = keyRecovery + + // TODO: should we fail with internal error for default case here? + } + } + + return nil +} + +func newRecoverModeStateMachine(model *asserts.Model, disk disks.Disk) *recoverModeStateMachine { + m := &recoverModeStateMachine{ + model: model, + disk: disk, + degradedState: &recoverDegradedState{ + ErrorLog: []string{}, + }, + } + // first step is to mount ubuntu-boot to check for run mode keys to unlock + // ubuntu-data + m.current = m.mountBoot + return m +} + +func (m *recoverModeStateMachine) execute() (finished bool, err error) { + next, err := m.current() + m.current = next + finished = next == nil + if finished && err == nil { + if err := m.finalize(); err != nil { + return true, err + } + } + return finished, err +} + +func (m *recoverModeStateMachine) finalize() error { + // check soundness + // the grade check makes sure that if data was mounted unencrypted + // but the model is secured it will end up marked as untrusted + isEncrypted := m.isEncryptedDev || m.model.Grade() == asserts.ModelSecured + part := m.degradedState.partition("ubuntu-data") + if part.MountState == partitionMounted && isEncrypted { + // check that save and data match + // We want to avoid a chosen ubuntu-data + // (e.g. activated with a recovery key) to get access + // via its logins to the secrets in ubuntu-save (in + // particular the policy update auth key) + // TODO:UC20: we should try to be a bit more specific here in checking that + // data and save match, and not mark data as untrusted if we + // know that the real save is locked/protected (or doesn't exist + // in the case of bad corruption) because currently this code will + // mark data as untrusted, even if it was unlocked with the run + // object key and we failed to unlock ubuntu-save at all, which is + // undesirable. This effectively means that you need to have both + // ubuntu-data and ubuntu-save unlockable and have matching marker + // files in order to use the files from ubuntu-data to log-in, + // etc. + trustData, _ := checkDataAndSavaPairing(boot.InitramfsHostWritableDir) + if !trustData { + part.MountState = partitionMountedUntrusted + m.degradedState.LogErrorf("cannot trust ubuntu-data, ubuntu-save and ubuntu-data are not marked as from the same install") + } + } + + // finally, combine the states of partDevice and fsDevice into the + // exported Device field for marshalling + // ubuntu-boot is easy - it will always be unencrypted so we just set + // Device to partDevice + m.degradedState.partition("ubuntu-boot").Device = m.degradedState.partition("ubuntu-boot").partDevice + + // for ubuntu-data and save, we need to actually look at the states + for _, partName := range []string{"ubuntu-data", "ubuntu-save"} { + part := m.degradedState.partition(partName) + if part.fsDevice == "" { + // then the device is encrypted, but we failed to decrypt it, so + // set Device to the encrypted block device + part.Device = part.partDevice + } else { + // all other cases, fsDevice is set to what we want to + // export, either it is set to the decrypted mapper device in the + // case it was successfully decrypted, or it is set to the encrypted + // block device if we failed to decrypt it, or it was set to the + // unencrypted block device if it was unencrypted + part.Device = part.fsDevice + } + } + + return nil +} + +func (m *recoverModeStateMachine) trustData() bool { + return m.degradedState.partition("ubuntu-data").MountState == partitionMounted +} + +// mountBoot is the first state to execute in the state machine, it can +// transition to the following states: +// - if ubuntu-boot is mounted successfully, execute unlockDataRunKey +// - if ubuntu-boot can't be mounted, execute unlockDataFallbackKey +// - if we mounted the wrong ubuntu-boot (or otherwise can't verify which one we +// mounted), return fatal error +func (m *recoverModeStateMachine) mountBoot() (stateFunc, error) { + part := m.degradedState.partition("ubuntu-boot") + // use the disk we mounted ubuntu-seed from as a reference to find + // ubuntu-seed and mount it + partUUID, findErr := m.disk.FindMatchingPartitionUUID("ubuntu-boot") + if err := m.setFindState("ubuntu-boot", partUUID, findErr); err != nil { + return nil, err + } + if part.FindState != partitionFound { + // if we didn't find ubuntu-boot, we can't try to unlock data with the + // run key, and should instead just jump straight to attempting to + // unlock with the fallback key + return m.unlockDataFallbackKey, nil + } + + // should we fsck ubuntu-boot? probably yes because on some platforms + // (u-boot for example) ubuntu-boot is vfat and it could have been unmounted + // dirtily, and we need to fsck it to ensure it is mounted safely before + // reading keys from it + fsckSystemdOpts := &systemdMountOptions{ + NeedsFsck: true, + } + mountErr := doSystemdMount(part.fsDevice, boot.InitramfsUbuntuBootDir, fsckSystemdOpts) + if err := m.setMountState("ubuntu-boot", boot.InitramfsUbuntuBootDir, mountErr); err != nil { + return nil, err + } + if part.MountState == partitionErrMounting { + // if we didn't mount data, then try to unlock data with the + // fallback key + return m.unlockDataFallbackKey, nil + } + // next step try to unlock data with run object + return m.unlockDataRunKey, nil +} + +// stateUnlockDataRunKey will try to unlock ubuntu-data with the normal run-mode +// key, and if it fails, progresses to the next state, which is either: +// - failed to unlock data, but we know it's an encrypted device -> try to unlock with fallback key +// - failed to find data at all -> try to unlock save +// - unlocked data with run key -> mount data +func (m *recoverModeStateMachine) unlockDataRunKey() (stateFunc, error) { + runModeKey := filepath.Join(boot.InitramfsBootEncryptionKeyDir, "ubuntu-data.sealed-key") + unlockOpts := &secboot.UnlockVolumeUsingSealedKeyOptions{ + // don't allow using the recovery key to unlock, we only try using the + // recovery key after we first try the fallback object + AllowRecoveryKey: false, + } + unlockRes, unlockErr := secbootUnlockVolumeUsingSealedKeyIfEncrypted(m.disk, "ubuntu-data", runModeKey, unlockOpts) + if err := m.setUnlockStateWithRunKey("ubuntu-data", unlockRes, unlockErr); err != nil { + return nil, err + } + if unlockErr != nil { + // we couldn't unlock ubuntu-data with the primary key, or we didn't + // find it in the unencrypted case + if unlockRes.IsEncrypted { + // we know the device is encrypted, so the next state is to try + // unlocking with the fallback key + return m.unlockDataFallbackKey, nil + } + + // if we didn't even find the device to the point where it would have + // been identified as decrypted or unencrypted device, we could have + // just entirely lost ubuntu-data-enc, and we could still have an + // encrypted device, so instead try to unlock ubuntu-save with the + // fallback key, the logic there can also handle an unencrypted ubuntu-save + return m.unlockSaveFallbackKey, nil + } + + // otherwise successfully unlocked it (or just found it if it was unencrypted) + // so just mount it + return m.mountData, nil +} + +func (m *recoverModeStateMachine) unlockDataFallbackKey() (stateFunc, error) { + // try to unlock data with the fallback key on ubuntu-seed, which must have + // been mounted at this point + unlockOpts := &secboot.UnlockVolumeUsingSealedKeyOptions{ + // we want to allow using the recovery key if the fallback key fails as + // using the fallback object is the last chance before we give up trying + // to unlock data + AllowRecoveryKey: true, + } + // TODO: this prompts for a recovery key + // TODO: we should somehow customize the prompt to mention what key we need + // the user to enter, and what we are unlocking (as currently the prompt + // says "recovery key" and the partition UUID for what is being unlocked) + dataFallbackKey := filepath.Join(boot.InitramfsSeedEncryptionKeyDir, "ubuntu-data.recovery.sealed-key") + unlockRes, unlockErr := secbootUnlockVolumeUsingSealedKeyIfEncrypted(m.disk, "ubuntu-data", dataFallbackKey, unlockOpts) + const partitionMandatory = false + if err := m.setUnlockStateWithFallbackKey("ubuntu-data", unlockRes, unlockErr, partitionMandatory); err != nil { + return nil, err + } + if unlockErr != nil { + // skip trying to mount data, since we did not unlock data we cannot + // open save with with the run key, so try the fallback one + return m.unlockSaveFallbackKey, nil + } + + // unlocked it, now go mount it + return m.mountData, nil +} + +func (m *recoverModeStateMachine) mountData() (stateFunc, error) { + data := m.degradedState.partition("ubuntu-data") // don't do fsck on the data partition, it could be corrupted - if err := doSystemdMount(device, boot.InitramfsHostUbuntuDataDir, nil); err != nil { + mountErr := doSystemdMount(data.fsDevice, boot.InitramfsHostUbuntuDataDir, nil) + if err := m.setMountState("ubuntu-data", boot.InitramfsHostUbuntuDataDir, mountErr); err != nil { + return nil, err + } + if mountErr == nil && m.isEncryptedDev { + // if we succeeded in mounting data and we are encrypted, the next step + // is to unlock save with the run key from ubuntu-data + return m.unlockSaveRunKey, nil + } + + // otherwise we always fall back to unlocking save with the fallback key, + // this could be two cases: + // 1. we are unencrypted in which case the secboot function used in + // unlockSaveRunKey will fail and then proceed to trying the fallback key + // function anyways which uses a secboot function that is suitable for + // unencrypted data + // 2. we are encrypted and we failed to mount data successfully, meaning we + // don't have the bare key from ubuntu-data to use, and need to fall back + // to the sealed key from ubuntu-seed + return m.unlockSaveFallbackKey, nil +} + +func (m *recoverModeStateMachine) unlockSaveRunKey() (stateFunc, error) { + // to get to this state, we needed to have mounted ubuntu-data on host, so + // if encrypted, we can try to read the run key from host ubuntu-data + saveKey := filepath.Join(dirs.SnapFDEDirUnder(boot.InitramfsHostWritableDir), "ubuntu-save.key") + key, err := ioutil.ReadFile(saveKey) + if err != nil { + // log the error and skip to trying the fallback key + m.degradedState.LogErrorf("cannot access run ubuntu-save key: %v", err) + return m.unlockSaveFallbackKey, nil + } + + unlockRes, unlockErr := secbootUnlockEncryptedVolumeUsingKey(m.disk, "ubuntu-save", key) + if err := m.setUnlockStateWithRunKey("ubuntu-save", unlockRes, unlockErr); err != nil { + return nil, err + } + if unlockErr != nil { + // failed to unlock with run key, try fallback key + return m.unlockSaveFallbackKey, nil + } + + // unlocked it properly, go mount it + return m.mountSave, nil +} + +func (m *recoverModeStateMachine) unlockSaveFallbackKey() (stateFunc, error) { + // remember what we assumed about encryption before looking at + // save + assumeEncrypted := m.isEncryptedDev + + // try to unlock save with the fallback key on ubuntu-seed, which must have + // been mounted at this point + unlockOpts := &secboot.UnlockVolumeUsingSealedKeyOptions{ + // we want to allow using the recovery key if the fallback key fails as + // using the fallback object is the last chance before we give up trying + // to unlock save + AllowRecoveryKey: true, + } + saveFallbackKey := filepath.Join(boot.InitramfsSeedEncryptionKeyDir, "ubuntu-save.recovery.sealed-key") + // TODO: this prompts again for a recover key, but really this is the + // reinstall key we will prompt for + // TODO: we should somehow customize the prompt to mention what key we need + // the user to enter, and what we are unlocking (as currently the prompt + // says "recovery key" and the partition UUID for what is being unlocked) + unlockRes, unlockErr := secbootUnlockVolumeUsingSealedKeyIfEncrypted(m.disk, "ubuntu-save", saveFallbackKey, unlockOpts) + const partitionOptionalIfUnencrypted = true + if err := m.setUnlockStateWithFallbackKey("ubuntu-save", unlockRes, unlockErr, partitionOptionalIfUnencrypted); err != nil { + return nil, err + } + if unlockErr != nil { + // all done, nothing left to try and mount, mounting ubuntu-save is the + // last step but we couldn't find or unlock it + return nil, nil + } + + // do a consistency check to make sure that if we found ubuntu-data + // unencrypted that we don't also mount ubuntu-save as encrypted + data := m.degradedState.partition("ubuntu-data") + if unlockRes.IsEncrypted && data.FindState == partitionFound && !assumeEncrypted { + return nil, fmt.Errorf("inconsistent encryption status for disk %s: ubuntu-data (device %s) was found unencrypted but ubuntu-save (device %s) was found to be encrypted", m.disk.Dev(), data.fsDevice, unlockRes.FsDevice) + } + + // otherwise we unlocked it, so go mount it + return m.mountSave, nil +} + +func (m *recoverModeStateMachine) mountSave() (stateFunc, error) { + save := m.degradedState.partition("ubuntu-save") + // TODO: should we fsck ubuntu-save ? + mountErr := doSystemdMount(save.fsDevice, boot.InitramfsUbuntuSaveDir, nil) + if err := m.setMountState("ubuntu-save", boot.InitramfsUbuntuSaveDir, mountErr); err != nil { + return nil, err + } + // all done, nothing left to try and mount + return nil, nil +} + +func generateMountsModeRecover(mst *initramfsMountsState) error { + // steps 1 and 2 are shared with install mode + model, err := generateMountsCommonInstallRecover(mst) + if err != nil { return err } - // 3.1 verify that the host ubuntu-data comes from where we expect it to - diskOpts := &disks.Options{} - if isDecryptDev { - // then we need to specify that the data mountpoint is expected to be a - // decrypted device - diskOpts.IsDecryptedDevice = true + // get the disk that we mounted the ubuntu-seed partition from as a + // reference point for future mounts + disk, err := disks.DiskFromMountPoint(boot.InitramfsUbuntuSeedDir, nil) + if err != nil { + return err } - matches, err := disk.MountPointIsFromDisk(boot.InitramfsHostUbuntuDataDir, diskOpts) + // 3. run the state machine logic for mounting partitions, this involves + // trying to unlock then mount ubuntu-data, and then unlocking and + // mounting ubuntu-save + // see the state* functions for details of what each step does and + // possible transition points + + machine, err := func() (machine *recoverModeStateMachine, err error) { + // first state to execute is to unlock ubuntu-data with the run key + machine = newRecoverModeStateMachine(model, disk) + for { + finished, err := machine.execute() + // TODO: consider whether certain errors are fatal or not + if err != nil { + return nil, err + } + if finished { + break + } + } + + return machine, nil + }() if err != nil { return err } - if !matches { - return fmt.Errorf("cannot validate boot: ubuntu-data mountpoint is expected to be from disk %s but is not", disk.Dev()) + + // 3.1 write out degraded.json if we ended up falling back somewhere + if machine.degraded() { + b, err := json.Marshal(machine.degradedState) + if err != nil { + return err + } + + if err := os.MkdirAll(dirs.SnapBootstrapRunDir, 0755); err != nil { + return err + } + + // leave the information about degraded state at an ephemeral location + if err := ioutil.WriteFile(filepath.Join(dirs.SnapBootstrapRunDir, "degraded.json"), b, 0644); err != nil { + return err + } } // 4. final step: copy the auth data and network config from // the real ubuntu-data dir to the ephemeral ubuntu-data // dir, write the modeenv to the tmpfs data, and disable // cloud-init in recover mode - if err := copyUbuntuDataAuth(boot.InitramfsHostUbuntuDataDir, boot.InitramfsDataDir); err != nil { - return err - } - if err := copyNetworkConfig(boot.InitramfsHostUbuntuDataDir, boot.InitramfsDataDir); err != nil { - return err + + // if we have the host location, then we were able to successfully mount + // ubuntu-data, and as such we can proceed with copying files from there + // onto the tmpfs + // Proceed only if we trust ubuntu-data to be paired with ubuntu-save + if machine.trustData() { + // TODO: erroring here should fallback to copySafeDefaultData and + // proceed on with degraded mode anyways + if err := copyUbuntuDataAuth(boot.InitramfsHostUbuntuDataDir, boot.InitramfsDataDir); err != nil { + return err + } + if err := copyNetworkConfig(boot.InitramfsHostUbuntuDataDir, boot.InitramfsDataDir); err != nil { + return err + } + if err := copyUbuntuDataMisc(boot.InitramfsHostUbuntuDataDir, boot.InitramfsDataDir); err != nil { + return err + } + } else { + // we don't have ubuntu-data host mountpoint, so we should setup safe + // defaults for i.e. console-conf in the running image to block + // attackers from accessing the system - just because we can't access + // ubuntu-data doesn't mean that attackers wouldn't be able to if they + // could login + + if err := copySafeDefaultData(boot.InitramfsDataDir); err != nil { + return err + } } modeEnv := &boot.Modeenv{ @@ -327,6 +1114,24 @@ return nil } +// checkDataAndSavaPairing make sure that ubuntu-data and ubuntu-save +// come from the same install by comparing secret markers in them +func checkDataAndSavaPairing(rootdir string) (bool, error) { + // read the secret marker file from ubuntu-data + markerFile1 := filepath.Join(dirs.SnapFDEDirUnder(rootdir), "marker") + marker1, err := ioutil.ReadFile(markerFile1) + if err != nil { + return false, err + } + // read the secret marker file from ubuntu-save + markerFile2 := filepath.Join(dirs.SnapFDEDirUnderSave(boot.InitramfsUbuntuSaveDir), "marker") + marker2, err := ioutil.ReadFile(markerFile2) + if err != nil { + return false, err + } + return subtle.ConstantTimeCompare(marker1, marker2) == 1, nil +} + // mountPartitionMatchingKernelDisk will select the partition to mount at dir, // using the boot package function FindPartitionUUIDForBootedKernelDisk to // determine what partition the booted kernel came from. If which disk the @@ -351,18 +1156,18 @@ return doSystemdMount(partSrc, dir, opts) } -func generateMountsCommonInstallRecover(mst *initramfsMountsState) error { +func generateMountsCommonInstallRecover(mst *initramfsMountsState) (*asserts.Model, error) { // 1. always ensure seed partition is mounted first before the others, // since the seed partition is needed to mount the snap files there if err := mountPartitionMatchingKernelDisk(boot.InitramfsUbuntuSeedDir, "ubuntu-seed"); err != nil { - return err + return nil, err } // load model and verified essential snaps metadata typs := []snap.Type{snap.TypeBase, snap.TypeKernel, snap.TypeSnapd, snap.TypeGadget} model, essSnaps, err := mst.ReadEssential("", typs) if err != nil { - return fmt.Errorf("cannot load metadata and verify essential bootstrap snaps %v: %v", typs, err) + return nil, fmt.Errorf("cannot load metadata and verify essential bootstrap snaps %v: %v", typs, err) } // 2.1. measure model @@ -372,7 +1177,7 @@ }) }) if err != nil { - return err + return nil, err } // 2.2. (auto) select recovery system and mount seed snaps @@ -386,7 +1191,7 @@ dir := snapTypeToMountDir[essentialSnap.EssentialType] // TODO:UC20: we need to cross-check the kernel path with snapd_recovery_kernel used by grub if err := doSystemdMount(essentialSnap.Path, filepath.Join(boot.InitramfsRunMntDir, dir), nil); err != nil { - return err + return nil, err } } @@ -409,7 +1214,7 @@ } err = doSystemdMount("tmpfs", boot.InitramfsDataDir, mntOpts) if err != nil { - return err + return nil, err } // finally get the gadget snap from the essential snaps and use it to @@ -435,7 +1240,49 @@ TargetRootDir: boot.InitramfsWritableDir, GadgetSnap: gadgetSnap, } - return sysconfig.ConfigureTargetSystem(configOpts) + if err := sysconfig.ConfigureTargetSystem(configOpts); err != nil { + return nil, err + } + + return model, err +} + +func maybeMountSave(disk disks.Disk, rootdir string, encrypted bool, mountOpts *systemdMountOptions) (haveSave bool, err error) { + var saveDevice string + if encrypted { + saveKey := filepath.Join(dirs.SnapFDEDirUnder(rootdir), "ubuntu-save.key") + // if ubuntu-save exists and is encrypted, the key has been created during install + if !osutil.FileExists(saveKey) { + // ubuntu-data is encrypted, but we appear to be missing + // a key to open ubuntu-save + return false, fmt.Errorf("cannot find ubuntu-save encryption key at %v", saveKey) + } + // we have save.key, volume exists and is encrypted + key, err := ioutil.ReadFile(saveKey) + if err != nil { + return true, err + } + unlockRes, err := secbootUnlockEncryptedVolumeUsingKey(disk, "ubuntu-save", key) + if err != nil { + return true, fmt.Errorf("cannot unlock ubuntu-save volume: %v", err) + } + saveDevice = unlockRes.FsDevice + } else { + partUUID, err := disk.FindMatchingPartitionUUID("ubuntu-save") + if err != nil { + if _, ok := err.(disks.FilesystemLabelNotFoundError); ok { + // this is ok, ubuntu-save may not exist for + // non-encrypted device + return false, nil + } + return false, err + } + saveDevice = filepath.Join("/dev/disk/by-partuuid", partUUID) + } + if err := doSystemdMount(saveDevice, boot.InitramfsUbuntuSaveDir, mountOpts); err != nil { + return true, err + } + return true, nil } func generateMountsModeRun(mst *initramfsMountsState) error { @@ -459,10 +1306,15 @@ return err } - // don't run fsck on ubuntu-seed in run mode so we minimize chance of - // corruption - - if err := doSystemdMount(fmt.Sprintf("/dev/disk/by-partuuid/%s", partUUID), boot.InitramfsUbuntuSeedDir, nil); err != nil { + // fsck is safe to run on ubuntu-seed as per the manpage, it should not + // meaningfully contribute to corruption if we fsck it every time we boot, + // and it is important to fsck it because it is vfat and mounted writable + // TODO:UC20: mount it as read-only here and remount as writable when we + // need it to be writable for i.e. transitioning to recover mode + fsckSystemdOpts := &systemdMountOptions{ + NeedsFsck: true, + } + if err := doSystemdMount(fmt.Sprintf("/dev/disk/by-partuuid/%s", partUUID), boot.InitramfsUbuntuSeedDir, fsckSystemdOpts); err != nil { return err } @@ -477,26 +1329,33 @@ // one recorded in ubuntu-data modeenv during install // 3.2. mount Data - const lockKeysOnFinish = true - device, isDecryptDev, err := secbootUnlockVolumeIfEncrypted(disk, "ubuntu-data", boot.InitramfsEncryptionKeyDir, lockKeysOnFinish) + runModeKey := filepath.Join(boot.InitramfsBootEncryptionKeyDir, "ubuntu-data.sealed-key") + opts := &secboot.UnlockVolumeUsingSealedKeyOptions{ + AllowRecoveryKey: true, + } + unlockRes, err := secbootUnlockVolumeUsingSealedKeyIfEncrypted(disk, "ubuntu-data", runModeKey, opts) if err != nil { return err } - opts := &systemdMountOptions{ - // TODO: do we actually need fsck if we are mounting a mapper device? - // probably not? - NeedsFsck: true, + // TODO: do we actually need fsck if we are mounting a mapper device? + // probably not? + if err := doSystemdMount(unlockRes.FsDevice, boot.InitramfsDataDir, fsckSystemdOpts); err != nil { + return err } - if err := doSystemdMount(device, boot.InitramfsDataDir, opts); err != nil { + isEncryptedDev := unlockRes.IsEncrypted + + // 3.3. mount ubuntu-save (if present) + haveSave, err := maybeMountSave(disk, boot.InitramfsWritableDir, isEncryptedDev, fsckSystemdOpts) + if err != nil { return err } // 4.1 verify that ubuntu-data comes from where we expect it to diskOpts := &disks.Options{} - if isDecryptDev { + if unlockRes.IsEncrypted { // then we need to specify that the data mountpoint is expected to be a - // decrypted device + // decrypted device, applies to both ubuntu-data and ubuntu-save diskOpts.IsDecryptedDevice = true } @@ -509,6 +1368,34 @@ // as ubuntu-boot return fmt.Errorf("cannot validate boot: ubuntu-data mountpoint is expected to be from disk %s but is not", disk.Dev()) } + if haveSave { + // 4.1a we have ubuntu-save, verify it as well + matches, err = disk.MountPointIsFromDisk(boot.InitramfsUbuntuSaveDir, diskOpts) + if err != nil { + return err + } + if !matches { + return fmt.Errorf("cannot validate boot: ubuntu-save mountpoint is expected to be from disk %s but is not", disk.Dev()) + } + + if isEncryptedDev { + // in run mode the path to open an encrypted save is for + // data to be encrypted and the save key in it + // to be successfully used. This already should stop + // allowing to chose ubuntu-data to try to access + // save. as safety boot also stops if the keys cannot + // be locked. + // for symmetry with recover code and extra paranoia + // though also check that the markers match. + paired, err := checkDataAndSavaPairing(boot.InitramfsWritableDir) + if err != nil { + return err + } + if !paired { + return fmt.Errorf("cannot validate boot: ubuntu-save and ubuntu-data are not marked as from the same install") + } + } + } // 4.2. read modeenv modeEnv, err := boot.ReadModeenv(boot.InitramfsWritableDir) diff -Nru snapd-2.47.1+20.10.1build1/cmd/snap-bootstrap/cmd_initramfs_mounts_nosecboot.go snapd-2.48+21.04/cmd/snap-bootstrap/cmd_initramfs_mounts_nosecboot.go --- snapd-2.47.1+20.10.1build1/cmd/snap-bootstrap/cmd_initramfs_mounts_nosecboot.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48+21.04/cmd/snap-bootstrap/cmd_initramfs_mounts_nosecboot.go 2020-11-19 16:51:02.000000000 +0000 @@ -0,0 +1,52 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- +// +build nosecboot + +/* + * 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 main + +import ( + "errors" + + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/osutil/disks" + "github.com/snapcore/snapd/secboot" +) + +var ( + errNotImplemented = errors.New("not implemented") +) + +func init() { + secbootMeasureSnapSystemEpochWhenPossible = func() error { + return errNotImplemented + } + secbootMeasureSnapModelWhenPossible = func(_ func() (*asserts.Model, error)) error { + return errNotImplemented + } + secbootUnlockVolumeUsingSealedKeyIfEncrypted = func(disk disks.Disk, name string, sealedEncryptionKeyFile string, opts *secboot.UnlockVolumeUsingSealedKeyOptions) (secboot.UnlockResult, error) { + return secboot.UnlockResult{}, errNotImplemented + } + secbootUnlockEncryptedVolumeUsingKey = func(disk disks.Disk, name string, key []byte) (secboot.UnlockResult, error) { + return secboot.UnlockResult{}, errNotImplemented + } + + secbootLockTPMSealedKeys = func() error { + return errNotImplemented + } +} diff -Nru snapd-2.47.1+20.10.1build1/cmd/snap-bootstrap/cmd_initramfs_mounts_recover_degraded_test.go snapd-2.48+21.04/cmd/snap-bootstrap/cmd_initramfs_mounts_recover_degraded_test.go --- snapd-2.47.1+20.10.1build1/cmd/snap-bootstrap/cmd_initramfs_mounts_recover_degraded_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48+21.04/cmd/snap-bootstrap/cmd_initramfs_mounts_recover_degraded_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -0,0 +1,292 @@ +// -*- 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 ( + . "gopkg.in/check.v1" + + main "github.com/snapcore/snapd/cmd/snap-bootstrap" +) + +func (s *initramfsMountsSuite) TestInitramfsDegradedState(c *C) { + tt := []struct { + r main.RecoverDegradedState + encrypted bool + degraded bool + comment string + }{ + // unencrypted happy + { + r: main.RecoverDegradedState{ + UbuntuBoot: main.PartitionState{ + MountState: "mounted", + }, + UbuntuData: main.PartitionState{ + MountState: "mounted", + }, + UbuntuSave: main.PartitionState{ + MountState: "absent-but-optional", + }, + }, + degraded: false, + comment: "happy unencrypted no save", + }, + { + r: main.RecoverDegradedState{ + UbuntuBoot: main.PartitionState{ + MountState: "mounted", + }, + UbuntuData: main.PartitionState{ + MountState: "mounted", + }, + UbuntuSave: main.PartitionState{ + MountState: "mounted", + }, + }, + degraded: false, + comment: "happy unencrypted save", + }, + // unencrypted unhappy + { + r: main.RecoverDegradedState{ + UbuntuBoot: main.PartitionState{ + MountState: "error-mounting", + }, + UbuntuData: main.PartitionState{ + MountState: "mounted", + }, + UbuntuSave: main.PartitionState{ + MountState: "absent-but-optional", + }, + ErrorLog: []string{ + "cannot find ubuntu-boot partition on disk 259:0", + }, + }, + degraded: true, + comment: "unencrypted, error mounting boot", + }, + { + r: main.RecoverDegradedState{ + UbuntuBoot: main.PartitionState{ + MountState: "mounted", + }, + UbuntuData: main.PartitionState{ + MountState: "error-mounting", + }, + UbuntuSave: main.PartitionState{ + MountState: "absent-but-optional", + }, + ErrorLog: []string{ + "cannot find ubuntu-data partition on disk 259:0", + }, + }, + degraded: true, + comment: "unencrypted, error mounting data", + }, + { + r: main.RecoverDegradedState{ + UbuntuBoot: main.PartitionState{ + MountState: "mounted", + }, + UbuntuData: main.PartitionState{ + MountState: "mounted", + }, + UbuntuSave: main.PartitionState{ + MountState: "error-mounting", + }, + ErrorLog: []string{ + "cannot find ubuntu-save partition on disk 259:0", + }, + }, + degraded: true, + comment: "unencrypted, error mounting save", + }, + + // encrypted happy + { + r: main.RecoverDegradedState{ + UbuntuBoot: main.PartitionState{ + MountState: "mounted", + }, + UbuntuData: main.PartitionState{ + MountState: "mounted", + UnlockState: "unlocked", + UnlockKey: "run", + }, + UbuntuSave: main.PartitionState{ + MountState: "mounted", + UnlockState: "unlocked", + UnlockKey: "run", + }, + }, + encrypted: true, + degraded: false, + comment: "happy encrypted", + }, + // encrypted unhappy + { + r: main.RecoverDegradedState{ + UbuntuBoot: main.PartitionState{ + MountState: "error-mounting", + }, + UbuntuData: main.PartitionState{ + MountState: "mounted", + UnlockState: "unlocked", + UnlockKey: "fallback", + }, + UbuntuSave: main.PartitionState{ + MountState: "mounted", + UnlockState: "unlocked", + UnlockKey: "run", + }, + ErrorLog: []string{ + "cannot find ubuntu-boot partition on disk 259:0", + }, + }, + encrypted: true, + degraded: true, + comment: "encrypted, no boot, fallback data", + }, + { + r: main.RecoverDegradedState{ + UbuntuBoot: main.PartitionState{ + MountState: "mounted", + }, + UbuntuData: main.PartitionState{ + MountState: "mounted", + UnlockState: "unlocked", + UnlockKey: "fallback", + }, + UbuntuSave: main.PartitionState{ + MountState: "mounted", + UnlockState: "unlocked", + UnlockKey: "run", + }, + ErrorLog: []string{ + "cannot unlock encrypted ubuntu-data with sealed run key: failed to unlock ubuntu-data", + }, + }, + encrypted: true, + degraded: true, + comment: "encrypted, fallback data", + }, + { + r: main.RecoverDegradedState{ + UbuntuBoot: main.PartitionState{ + MountState: "mounted", + }, + UbuntuData: main.PartitionState{ + MountState: "mounted", + UnlockState: "unlocked", + UnlockKey: "run", + }, + UbuntuSave: main.PartitionState{ + MountState: "mounted", + UnlockState: "unlocked", + UnlockKey: "fallback", + }, + ErrorLog: []string{ + "cannot unlock encrypted ubuntu-save with sealed run key: failed to unlock ubuntu-save", + }, + }, + encrypted: true, + degraded: true, + comment: "encrypted, fallback save", + }, + { + r: main.RecoverDegradedState{ + UbuntuBoot: main.PartitionState{ + MountState: "mounted", + }, + UbuntuData: main.PartitionState{ + MountState: "mounted", + UnlockState: "unlocked", + UnlockKey: "run", + }, + UbuntuSave: main.PartitionState{ + MountState: "mounted", + UnlockState: "unlocked", + UnlockKey: "recovery", + }, + ErrorLog: []string{ + "cannot unlock encrypted ubuntu-save with sealed run key: failed to unlock ubuntu-save", + }, + }, + encrypted: true, + degraded: true, + comment: "encrypted, recovery save", + }, + { + r: main.RecoverDegradedState{ + UbuntuBoot: main.PartitionState{ + MountState: "mounted", + }, + UbuntuData: main.PartitionState{ + MountState: "mounted", + UnlockState: "unlocked", + UnlockKey: "fallback", + }, + UbuntuSave: main.PartitionState{ + MountState: "mounted", + UnlockState: "unlocked", + UnlockKey: "fallback", + }, + ErrorLog: []string{ + "cannot unlock encrypted ubuntu-data with sealed run key: failed to unlock ubuntu-data", + }, + }, + encrypted: true, + degraded: true, + comment: "encrypted, fallback data, fallback save", + }, + { + r: main.RecoverDegradedState{ + UbuntuBoot: main.PartitionState{ + MountState: "mounted", + }, + UbuntuData: main.PartitionState{ + MountState: "mounted", + UnlockState: "unlocked", + UnlockKey: "fallback", + }, + UbuntuSave: main.PartitionState{ + MountState: "not-mounted", + UnlockState: "not-unlocked", + }, + ErrorLog: []string{ + "cannot unlock encrypted ubuntu-save with sealed run key: failed to unlock ubuntu-save", + "cannot unlock encrypted ubuntu-save with sealed fallback key: failed to unlock ubuntu-save", + }, + }, + encrypted: true, + degraded: true, + comment: "encrypted, fallback data, no save", + }, + } + + for _, t := range tt { + var comment CommentInterface + if t.comment != "" { + comment = Commentf(t.comment) + } + + c.Assert(t.r.Degraded(t.encrypted), Equals, t.degraded, comment) + } +} diff -Nru snapd-2.47.1+20.10.1build1/cmd/snap-bootstrap/cmd_initramfs_mounts_secboot.go snapd-2.48+21.04/cmd/snap-bootstrap/cmd_initramfs_mounts_secboot.go --- snapd-2.47.1+20.10.1build1/cmd/snap-bootstrap/cmd_initramfs_mounts_secboot.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48+21.04/cmd/snap-bootstrap/cmd_initramfs_mounts_secboot.go 2020-11-19 16:51:02.000000000 +0000 @@ -0,0 +1,33 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- +// +build !nosecboot + +/* + * 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 main + +import ( + "github.com/snapcore/snapd/secboot" +) + +func init() { + secbootMeasureSnapSystemEpochWhenPossible = secboot.MeasureSnapSystemEpochWhenPossible + secbootMeasureSnapModelWhenPossible = secboot.MeasureSnapModelWhenPossible + secbootUnlockVolumeUsingSealedKeyIfEncrypted = secboot.UnlockVolumeUsingSealedKeyIfEncrypted + secbootUnlockEncryptedVolumeUsingKey = secboot.UnlockEncryptedVolumeUsingKey + secbootLockTPMSealedKeys = secboot.LockTPMSealedKeys +} diff -Nru snapd-2.47.1+20.10.1build1/cmd/snap-bootstrap/cmd_initramfs_mounts_test.go snapd-2.48+21.04/cmd/snap-bootstrap/cmd_initramfs_mounts_test.go --- snapd-2.47.1+20.10.1build1/cmd/snap-bootstrap/cmd_initramfs_mounts_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/cmd/snap-bootstrap/cmd_initramfs_mounts_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -21,6 +21,7 @@ import ( "bytes" + "encoding/json" "fmt" "io/ioutil" "os" @@ -41,6 +42,7 @@ "github.com/snapcore/snapd/logger" "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/osutil/disks" + "github.com/snapcore/snapd/secboot" "github.com/snapcore/snapd/seed" "github.com/snapcore/snapd/seed/seedtest" "github.com/snapcore/snapd/snap" @@ -80,12 +82,9 @@ NeedsFsck: true, } + // a boot disk without ubuntu-save defaultBootDisk = &disks.MockDiskMapping{ FilesystemLabelToPartUUID: map[string]string{ - // ubuntu-boot not strictly necessary, since we mount it first we - // don't go looking for the label ubuntu-boot on a disk, we just - // mount it and hope it's what we need, unless we have UEFI vars or - // something "ubuntu-boot": "ubuntu-boot-partuuid", "ubuntu-seed": "ubuntu-seed-partuuid", "ubuntu-data": "ubuntu-data-partuuid", @@ -94,15 +93,23 @@ DevNum: "default", } + defaultBootWithSaveDisk = &disks.MockDiskMapping{ + FilesystemLabelToPartUUID: map[string]string{ + "ubuntu-boot": "ubuntu-boot-partuuid", + "ubuntu-seed": "ubuntu-seed-partuuid", + "ubuntu-data": "ubuntu-data-partuuid", + "ubuntu-save": "ubuntu-save-partuuid", + }, + DiskHasPartitions: true, + DevNum: "default-with-save", + } + defaultEncBootDisk = &disks.MockDiskMapping{ FilesystemLabelToPartUUID: map[string]string{ - // ubuntu-boot not strictly necessary, since we mount it first we - // don't ever search a particular disk for the ubuntu-boot label, - // we just mount it and hope it's what we need, unless we have UEFI - // vars or something a la boot.PartitionUUIDForBootedKernelDisk "ubuntu-boot": "ubuntu-boot-partuuid", "ubuntu-seed": "ubuntu-seed-partuuid", "ubuntu-data-enc": "ubuntu-data-enc-partuuid", + "ubuntu-save-enc": "ubuntu-save-enc-partuuid", }, DiskHasPartitions: true, DevNum: "defaultEncDev", @@ -202,6 +209,51 @@ // by default mock that we don't have UEFI vars, etc. to get the booted // kernel partition partition uuid s.AddCleanup(main.MockPartitionUUIDForBootedKernelDisk("")) + s.AddCleanup(main.MockSecbootMeasureSnapSystemEpochWhenPossible(func() error { + return nil + })) + s.AddCleanup(main.MockSecbootMeasureSnapModelWhenPossible(func(f func() (*asserts.Model, error)) error { + c.Check(f, NotNil) + return nil + })) + s.AddCleanup(main.MockSecbootUnlockVolumeUsingSealedKeyIfEncrypted(func(disk disks.Disk, name string, sealedEncryptionKeyFile string, opts *secboot.UnlockVolumeUsingSealedKeyOptions) (secboot.UnlockResult, error) { + return foundUnencrypted(name), nil + })) + s.AddCleanup(main.MockSecbootLockTPMSealedKeys(func() error { + return nil + })) +} + +// helpers to create consistent UnlockResult values + +func foundUnencrypted(name string) secboot.UnlockResult { + dev := filepath.Join("/dev/disk/by-partuuid", name+"-partuuid") + return secboot.UnlockResult{ + PartDevice: dev, + FsDevice: dev, + } +} + +func happyUnlocked(name string, method secboot.UnlockMethod) secboot.UnlockResult { + return secboot.UnlockResult{ + PartDevice: filepath.Join("/dev/disk/by-partuuid", name+"-enc-partuuid"), + FsDevice: filepath.Join("/dev/mapper", name+"-random"), + IsEncrypted: true, + UnlockMethod: method, + } +} + +func foundEncrypted(name string) secboot.UnlockResult { + return secboot.UnlockResult{ + PartDevice: filepath.Join("/dev/disk/by-partuuid", name+"-enc-partuuid"), + // FsDevice is empty if we didn't unlock anything + FsDevice: "", + IsEncrypted: true, + } +} + +func notFoundPart() secboot.UnlockResult { + return secboot.UnlockResult{} } // makeSnapFilesOnEarlyBootUbuntuData creates the snap files on ubuntu-data as @@ -221,10 +273,27 @@ mockProcCmdline := filepath.Join(c.MkDir(), "proc-cmdline") err := ioutil.WriteFile(mockProcCmdline, []byte(newContent), 0644) c.Assert(err, IsNil) - restore := boot.MockProcCmdline(mockProcCmdline) + restore := osutil.MockProcCmdline(mockProcCmdline) s.AddCleanup(restore) } +func (s *initramfsMountsSuite) mockUbuntuSaveKeyAndMarker(c *C, rootDir, key, marker string) { + keyPath := filepath.Join(dirs.SnapFDEDirUnder(rootDir), "ubuntu-save.key") + c.Assert(os.MkdirAll(filepath.Dir(keyPath), 0700), IsNil) + c.Assert(ioutil.WriteFile(keyPath, []byte(key), 0600), IsNil) + + if marker != "" { + markerPath := filepath.Join(dirs.SnapFDEDirUnder(rootDir), "marker") + c.Assert(ioutil.WriteFile(markerPath, []byte(marker), 0600), IsNil) + } +} + +func (s *initramfsMountsSuite) mockUbuntuSaveMarker(c *C, rootDir, marker string) { + markerPath := filepath.Join(rootDir, "device/fde", "marker") + c.Assert(os.MkdirAll(filepath.Dir(markerPath), 0700), IsNil) + c.Assert(ioutil.WriteFile(markerPath, []byte(marker), 0600), IsNil) +} + func (s *initramfsMountsSuite) TestInitramfsMountsNoModeError(c *C) { s.mockProcCmdlineContent(c, "nothing-to-see") @@ -273,6 +342,7 @@ // ubuntuPartUUIDMount returns a systemdMount for the partuuid disk, expecting // that the partuuid contains in it the expected label for easier coding func ubuntuPartUUIDMount(partuuid string, mode string) systemdMount { + // all partitions are expected to be mounted with fsck on mnt := systemdMount{ opts: needsFsckDiskMountOpts, } @@ -282,12 +352,10 @@ mnt.where = boot.InitramfsUbuntuBootDir case strings.Contains(partuuid, "ubuntu-seed"): mnt.where = boot.InitramfsUbuntuSeedDir - // don't fsck in run mode - if mode == "run" { - mnt.opts = nil - } case strings.Contains(partuuid, "ubuntu-data"): mnt.where = boot.InitramfsDataDir + case strings.Contains(partuuid, "ubuntu-save"): + mnt.where = boot.InitramfsUbuntuSaveDir } return mnt @@ -339,7 +407,7 @@ s.AddCleanup(func() { // make sure that after the test is done, we had as many mount calls as // mocked mounts - c.Assert(n, Equals, len(mounts), comment) + c.Check(n, Equals, len(mounts), comment) }) return main.MockSystemdMount(func(what, where string, opts *main.SystemdMountOptions) error { n++ @@ -358,6 +426,13 @@ func (s *initramfsMountsSuite) TestInitramfsMountsInstallModeHappy(c *C) { s.mockProcCmdlineContent(c, "snapd_recovery_mode=install snapd_recovery_system="+s.sysLabel) + // ensure that we check that access to sealed keys were locked + sealedKeysLocked := false + defer main.MockSecbootLockTPMSealedKeys(func() error { + sealedKeysLocked = true + return nil + })() + restore := s.mockSystemdMountSequence(c, []systemdMount{ ubuntuLabelMount("ubuntu-seed", "install"), s.makeSeedSnapSystemdMount(snap.TypeSnapd), @@ -380,6 +455,8 @@ `) cloudInitDisable := filepath.Join(boot.InitramfsWritableDir, "_writable_defaults/etc/cloud/cloud-init.disabled") c.Check(cloudInitDisable, testutil.FilePresent) + + c.Check(sealedKeysLocked, Equals, true) } func (s *initramfsMountsSuite) TestInitramfsMountsInstallModeGadgetDefaultsHappy(c *C) { @@ -478,7 +555,53 @@ c.Check(cloudInitDisable, testutil.FilePresent) } -func (s *initramfsMountsSuite) TestInitramfsMountsRunModeHappy(c *C) { +func (s *initramfsMountsSuite) TestInitramfsMountsRunModeUnencryptedWithSaveHappy(c *C) { + s.mockProcCmdlineContent(c, "snapd_recovery_mode=run") + + restore := disks.MockMountPointDisksToPartitionMapping( + map[disks.Mountpoint]*disks.MockDiskMapping{ + {Mountpoint: boot.InitramfsUbuntuBootDir}: defaultBootWithSaveDisk, + {Mountpoint: boot.InitramfsDataDir}: defaultBootWithSaveDisk, + {Mountpoint: boot.InitramfsUbuntuSaveDir}: defaultBootWithSaveDisk, + }, + ) + defer restore() + + restore = s.mockSystemdMountSequence(c, []systemdMount{ + ubuntuLabelMount("ubuntu-boot", "run"), + ubuntuPartUUIDMount("ubuntu-seed-partuuid", "run"), + ubuntuPartUUIDMount("ubuntu-data-partuuid", "run"), + ubuntuPartUUIDMount("ubuntu-save-partuuid", "run"), + s.makeRunSnapSystemdMount(snap.TypeBase, s.core20), + s.makeRunSnapSystemdMount(snap.TypeKernel, s.kernel), + }, nil) + defer restore() + + // mock a bootloader + bloader := boottest.MockUC20RunBootenv(bootloadertest.Mock("mock", c.MkDir())) + bootloader.Force(bloader) + defer bootloader.Force(nil) + + // set the current kernel + restore = bloader.SetEnabledKernel(s.kernel) + defer restore() + + makeSnapFilesOnEarlyBootUbuntuData(c, s.kernel, s.core20) + + // write modeenv + modeEnv := boot.Modeenv{ + Mode: "run", + Base: s.core20.Filename(), + CurrentKernels: []string{s.kernel.Filename()}, + } + err := modeEnv.WriteTo(boot.InitramfsWritableDir) + c.Assert(err, IsNil) + + _, err = main.Parser().ParseArgs([]string{"initramfs-mounts"}) + c.Assert(err, IsNil) +} + +func (s *initramfsMountsSuite) testInitramfsMountsRunModeNoSaveUnencrypted(c *C) error { s.mockProcCmdlineContent(c, "snapd_recovery_mode=run") restore := disks.MockMountPointDisksToPartitionMapping( @@ -519,7 +642,31 @@ c.Assert(err, IsNil) _, err = main.Parser().ParseArgs([]string{"initramfs-mounts"}) + return err +} + +func (s *initramfsMountsSuite) TestInitramfsMountsRunModeNoSaveUnencryptedHappy(c *C) { + // ensure that we check that access to sealed keys were locked + sealedKeysLocked := false + defer main.MockSecbootLockTPMSealedKeys(func() error { + sealedKeysLocked = true + return nil + })() + + err := s.testInitramfsMountsRunModeNoSaveUnencrypted(c) c.Assert(err, IsNil) + + c.Check(sealedKeysLocked, Equals, true) +} + +func (s *initramfsMountsSuite) TestInitramfsMountsRunModeNoSaveUnencryptedKeyLockingUnhappy(c *C) { + // have blocking sealed keys fail + defer main.MockSecbootLockTPMSealedKeys(func() error { + return fmt.Errorf("blocking keys failed") + })() + + err := s.testInitramfsMountsRunModeNoSaveUnencrypted(c) + c.Assert(err, ErrorMatches, "error locking access to sealed keys: blocking keys failed") } func (s *initramfsMountsSuite) TestInitramfsMountsInstallModeRealSystemdMountTimesOutNoMount(c *C) { @@ -670,32 +817,28 @@ "--no-pager", "--no-ask-password", "--fsck=yes", - }, - { + }, { "systemd-mount", filepath.Join(s.seedDir, "snaps", s.snapd.Filename()), snapdMnt, "--no-pager", "--no-ask-password", "--fsck=no", - }, - { + }, { "systemd-mount", filepath.Join(s.seedDir, "snaps", s.kernel.Filename()), kernelMnt, "--no-pager", "--no-ask-password", "--fsck=no", - }, - { + }, { "systemd-mount", filepath.Join(s.seedDir, "snaps", s.core20.Filename()), baseMnt, "--no-pager", "--no-ask-password", "--fsck=no", - }, - { + }, { "systemd-mount", "tmpfs", boot.InitramfsDataDir, @@ -707,7 +850,7 @@ }) } -func (s *initramfsMountsSuite) TestInitramfsMountsRecoverModeHappyRealSystemdMount(c *C) { +func (s *initramfsMountsSuite) TestInitramfsMountsRecoverModeNoSaveHappyRealSystemdMount(c *C) { s.mockProcCmdlineContent(c, "snapd_recovery_mode=recover snapd_recovery_system="+s.sysLabel) baseMnt := filepath.Join(boot.InitramfsRunMntDir, "base") @@ -717,6 +860,7 @@ restore := disks.MockMountPointDisksToPartitionMapping( map[disks.Mountpoint]*disks.MockDiskMapping{ {Mountpoint: boot.InitramfsUbuntuSeedDir}: defaultBootDisk, + {Mountpoint: boot.InitramfsUbuntuBootDir}: defaultBootDisk, {Mountpoint: boot.InitramfsHostUbuntuDataDir}: defaultBootDisk, }, ) @@ -752,6 +896,9 @@ c.Assert(where, Equals, boot.InitramfsDataDir) return n%2 == 0, nil case 11, 12: + c.Assert(where, Equals, boot.InitramfsUbuntuBootDir) + return n%2 == 0, nil + case 13, 14: c.Assert(where, Equals, boot.InitramfsHostUbuntuDataDir) return n%2 == 0, nil default: @@ -761,6 +908,23 @@ }) defer restore() + unlockVolumeWithSealedKeyCalls := 0 + restore = main.MockSecbootUnlockVolumeUsingSealedKeyIfEncrypted(func(disk disks.Disk, name string, sealedEncryptionKeyFile string, opts *secboot.UnlockVolumeUsingSealedKeyOptions) (secboot.UnlockResult, error) { + // this test doesn't use ubuntu-save, so we need to return an + // unencrypted ubuntu-data the first time, but not found the second time + unlockVolumeWithSealedKeyCalls++ + switch unlockVolumeWithSealedKeyCalls { + case 1: + return foundUnencrypted(name), nil + case 2: + return notFoundPart(), fmt.Errorf("error enumerating to find ubuntu-save") + default: + c.Errorf("unexpected call (number %d) to UnlockVolumeUsingSealedKeyIfEncrypted", unlockVolumeWithSealedKeyCalls) + return secboot.UnlockResult{}, fmt.Errorf("unexpected call (%d) to UnlockVolumeUsingSealedKeyIfEncrypted", unlockVolumeWithSealedKeyCalls) + } + }) + defer restore() + // mock a bootloader bloader := boottest.MockUC20RunBootenv(bootloadertest.Mock("mock", c.MkDir())) bootloader.Force(bloader) @@ -809,8 +973,8 @@ } } - // 2 IsMounted calls per mount point, so 12 total IsMounted calls - c.Assert(n, Equals, 12) + // 2 IsMounted calls per mount point, so 14 total IsMounted calls + c.Assert(n, Equals, 14) c.Assert(cmd.Calls(), DeepEquals, [][]string{ { @@ -820,32 +984,28 @@ "--no-pager", "--no-ask-password", "--fsck=yes", - }, - { + }, { "systemd-mount", filepath.Join(s.seedDir, "snaps", s.snapd.Filename()), snapdMnt, "--no-pager", "--no-ask-password", "--fsck=no", - }, - { + }, { "systemd-mount", filepath.Join(s.seedDir, "snaps", s.kernel.Filename()), kernelMnt, "--no-pager", "--no-ask-password", "--fsck=no", - }, - { + }, { "systemd-mount", filepath.Join(s.seedDir, "snaps", s.core20.Filename()), baseMnt, "--no-pager", "--no-ask-password", "--fsck=no", - }, - { + }, { "systemd-mount", "tmpfs", boot.InitramfsDataDir, @@ -853,19 +1013,177 @@ "--no-ask-password", "--type=tmpfs", "--fsck=no", + }, { + "systemd-mount", + "/dev/disk/by-partuuid/ubuntu-boot-partuuid", + boot.InitramfsUbuntuBootDir, + "--no-pager", + "--no-ask-password", + "--fsck=yes", + }, { + "systemd-mount", + "/dev/disk/by-partuuid/ubuntu-data-partuuid", + boot.InitramfsHostUbuntuDataDir, + "--no-pager", + "--no-ask-password", + "--fsck=no", + }, + }) + + // we should not have written a degraded.json + c.Assert(filepath.Join(dirs.SnapBootstrapRunDir, "degraded.json"), testutil.FileAbsent) + + // we should have only tried to unseal things twice, first for ubuntu-data + // unencrypted, then for ubuntu-save unencrypted + c.Assert(unlockVolumeWithSealedKeyCalls, Equals, 2) +} + +func (s *initramfsMountsSuite) TestInitramfsMountsRecoverModeWithSaveHappyRealSystemdMount(c *C) { + s.mockProcCmdlineContent(c, "snapd_recovery_mode=recover snapd_recovery_system="+s.sysLabel) + + restore := disks.MockMountPointDisksToPartitionMapping( + map[disks.Mountpoint]*disks.MockDiskMapping{ + {Mountpoint: boot.InitramfsUbuntuSeedDir}: defaultBootWithSaveDisk, + {Mountpoint: boot.InitramfsUbuntuBootDir}: defaultBootWithSaveDisk, + {Mountpoint: boot.InitramfsHostUbuntuDataDir}: defaultBootWithSaveDisk, + {Mountpoint: boot.InitramfsUbuntuSaveDir}: defaultBootWithSaveDisk, }, + ) + defer restore() + + baseMnt := filepath.Join(boot.InitramfsRunMntDir, "base") + kernelMnt := filepath.Join(boot.InitramfsRunMntDir, "kernel") + snapdMnt := filepath.Join(boot.InitramfsRunMntDir, "snapd") + + // don't do anything from systemd-mount, we verify the arguments passed at + // the end with cmd.Calls + cmd := testutil.MockCommand(c, "systemd-mount", ``) + defer cmd.Restore() + + isMountedChecks := []string{} + restore = main.MockOsutilIsMounted(func(where string) (bool, error) { + isMountedChecks = append(isMountedChecks, where) + return true, nil + }) + defer restore() + + // mock a bootloader + bloader := boottest.MockUC20RunBootenv(bootloadertest.Mock("mock", c.MkDir())) + bootloader.Force(bloader) + defer bootloader.Force(nil) + + // set the current kernel + restore = bloader.SetEnabledKernel(s.kernel) + defer restore() + + makeSnapFilesOnEarlyBootUbuntuData(c, s.kernel, s.core20) + + // write modeenv + modeEnv := boot.Modeenv{ + Mode: "run", + Base: s.core20.Filename(), + CurrentKernels: []string{s.kernel.Filename()}, + } + err := modeEnv.WriteTo(boot.InitramfsWritableDir) + c.Assert(err, IsNil) + + s.testRecoverModeHappy(c) + + c.Check(s.Stdout.String(), Equals, "") + + // check that all of the override files are present + for _, initrdUnit := range []string{ + "initrd.target", + "initrd-fs.target", + "initrd-switch-root.target", + "local-fs.target", + } { + + mountUnit := systemd.EscapeUnitNamePath(boot.InitramfsUbuntuSaveDir) + fname := fmt.Sprintf("snap_bootstrap_%s.conf", mountUnit) + unitFile := filepath.Join(dirs.GlobalRootDir, "/run/systemd/system", initrdUnit+".d", fname) + c.Assert(unitFile, testutil.FileEquals, fmt.Sprintf(`[Unit] +Requires=%[1]s +After=%[1]s +`, mountUnit+".mount")) + } + + c.Check(isMountedChecks, DeepEquals, []string{ + boot.InitramfsUbuntuSeedDir, + snapdMnt, + kernelMnt, + baseMnt, + boot.InitramfsDataDir, + boot.InitramfsUbuntuBootDir, + boot.InitramfsHostUbuntuDataDir, + boot.InitramfsUbuntuSaveDir, + }) + c.Check(cmd.Calls(), DeepEquals, [][]string{ { "systemd-mount", + "/dev/disk/by-label/ubuntu-seed", + boot.InitramfsUbuntuSeedDir, + "--no-pager", + "--no-ask-password", + "--fsck=yes", + }, { + "systemd-mount", + filepath.Join(s.seedDir, "snaps", s.snapd.Filename()), + snapdMnt, + "--no-pager", + "--no-ask-password", + "--fsck=no", + }, { + "systemd-mount", + filepath.Join(s.seedDir, "snaps", s.kernel.Filename()), + kernelMnt, + "--no-pager", + "--no-ask-password", + "--fsck=no", + }, { + "systemd-mount", + filepath.Join(s.seedDir, "snaps", s.core20.Filename()), + baseMnt, + "--no-pager", + "--no-ask-password", + "--fsck=no", + }, { + "systemd-mount", + "tmpfs", + boot.InitramfsDataDir, + "--no-pager", + "--no-ask-password", + "--type=tmpfs", + "--fsck=no", + }, { + "systemd-mount", + "/dev/disk/by-partuuid/ubuntu-boot-partuuid", + boot.InitramfsUbuntuBootDir, + "--no-pager", + "--no-ask-password", + "--fsck=yes", + }, { + "systemd-mount", "/dev/disk/by-partuuid/ubuntu-data-partuuid", boot.InitramfsHostUbuntuDataDir, "--no-pager", "--no-ask-password", "--fsck=no", + }, { + "systemd-mount", + "/dev/disk/by-partuuid/ubuntu-save-partuuid", + boot.InitramfsUbuntuSaveDir, + "--no-pager", + "--no-ask-password", + "--fsck=no", }, }) + + // we should not have written a degraded.json + c.Assert(filepath.Join(dirs.SnapBootstrapRunDir, "degraded.json"), testutil.FileAbsent) } -func (s *initramfsMountsSuite) TestInitramfsMountsRunModeHappyRealSystemdMount(c *C) { +func (s *initramfsMountsSuite) TestInitramfsMountsRunModeHappyNoSaveRealSystemdMount(c *C) { s.mockProcCmdlineContent(c, "snapd_recovery_mode=run") restore := disks.MockMountPointDisksToPartitionMapping( @@ -973,32 +1291,28 @@ "--no-pager", "--no-ask-password", "--fsck=yes", - }, - { + }, { "systemd-mount", "/dev/disk/by-partuuid/ubuntu-seed-partuuid", boot.InitramfsUbuntuSeedDir, "--no-pager", "--no-ask-password", - "--fsck=no", - }, - { + "--fsck=yes", + }, { "systemd-mount", "/dev/disk/by-partuuid/ubuntu-data-partuuid", boot.InitramfsDataDir, "--no-pager", "--no-ask-password", "--fsck=yes", - }, - { + }, { "systemd-mount", filepath.Join(dirs.SnapBlobDirUnder(boot.InitramfsWritableDir), s.core20.Filename()), baseMnt, "--no-pager", "--no-ask-password", "--fsck=no", - }, - { + }, { "systemd-mount", filepath.Join(dirs.SnapBlobDirUnder(boot.InitramfsWritableDir), s.kernel.Filename()), kernelMnt, @@ -1009,34 +1323,159 @@ }) } -func (s *initramfsMountsSuite) TestInitramfsMountsRunModeFirstBootRecoverySystemSetHappy(c *C) { +func (s *initramfsMountsSuite) TestInitramfsMountsRunModeWithSaveHappyRealSystemdMount(c *C) { s.mockProcCmdlineContent(c, "snapd_recovery_mode=run") restore := disks.MockMountPointDisksToPartitionMapping( map[disks.Mountpoint]*disks.MockDiskMapping{ - {Mountpoint: boot.InitramfsUbuntuBootDir}: defaultBootDisk, - {Mountpoint: boot.InitramfsDataDir}: defaultBootDisk, + {Mountpoint: boot.InitramfsUbuntuBootDir}: defaultBootWithSaveDisk, + {Mountpoint: boot.InitramfsDataDir}: defaultBootWithSaveDisk, + {Mountpoint: boot.InitramfsUbuntuSaveDir}: defaultBootWithSaveDisk, }, ) defer restore() - restore = s.mockSystemdMountSequence(c, []systemdMount{ - ubuntuLabelMount("ubuntu-boot", "run"), - ubuntuPartUUIDMount("ubuntu-seed-partuuid", "run"), - ubuntuPartUUIDMount("ubuntu-data-partuuid", "run"), - s.makeRunSnapSystemdMount(snap.TypeBase, s.core20), - s.makeRunSnapSystemdMount(snap.TypeKernel, s.kernel), - // RecoverySystem set makes us mount the snapd snap here - s.makeSeedSnapSystemdMount(snap.TypeSnapd), - }, nil) - defer restore() - - // mock a bootloader - bloader := boottest.MockUC20RunBootenv(bootloadertest.Mock("mock", c.MkDir())) - bootloader.Force(bloader) - defer bootloader.Force(nil) + baseMnt := filepath.Join(boot.InitramfsRunMntDir, "base") + kernelMnt := filepath.Join(boot.InitramfsRunMntDir, "kernel") - // set the current kernel + // don't do anything from systemd-mount, we verify the arguments passed at + // the end with cmd.Calls + cmd := testutil.MockCommand(c, "systemd-mount", ``) + defer cmd.Restore() + + isMountedChecks := []string{} + restore = main.MockOsutilIsMounted(func(where string) (bool, error) { + isMountedChecks = append(isMountedChecks, where) + return true, nil + }) + defer restore() + + // mock a bootloader + bloader := boottest.MockUC20RunBootenv(bootloadertest.Mock("mock", c.MkDir())) + bootloader.Force(bloader) + defer bootloader.Force(nil) + + // set the current kernel + restore = bloader.SetEnabledKernel(s.kernel) + defer restore() + + makeSnapFilesOnEarlyBootUbuntuData(c, s.kernel, s.core20) + + // write modeenv + modeEnv := boot.Modeenv{ + Mode: "run", + Base: s.core20.Filename(), + CurrentKernels: []string{s.kernel.Filename()}, + } + err := modeEnv.WriteTo(boot.InitramfsWritableDir) + c.Assert(err, IsNil) + + _, err = main.Parser().ParseArgs([]string{"initramfs-mounts"}) + c.Assert(err, IsNil) + c.Check(s.Stdout.String(), Equals, "") + + // check that all of the override files are present + for _, initrdUnit := range []string{ + "initrd.target", + "initrd-fs.target", + "initrd-switch-root.target", + "local-fs.target", + } { + + mountUnit := systemd.EscapeUnitNamePath(boot.InitramfsUbuntuSaveDir) + fname := fmt.Sprintf("snap_bootstrap_%s.conf", mountUnit) + unitFile := filepath.Join(dirs.GlobalRootDir, "/run/systemd/system", initrdUnit+".d", fname) + c.Assert(unitFile, testutil.FileEquals, fmt.Sprintf(`[Unit] +Requires=%[1]s +After=%[1]s +`, mountUnit+".mount")) + } + + c.Check(isMountedChecks, DeepEquals, []string{ + boot.InitramfsUbuntuBootDir, + boot.InitramfsUbuntuSeedDir, + boot.InitramfsDataDir, + boot.InitramfsUbuntuSaveDir, + baseMnt, + kernelMnt, + }) + c.Check(cmd.Calls(), DeepEquals, [][]string{ + { + "systemd-mount", + "/dev/disk/by-label/ubuntu-boot", + boot.InitramfsUbuntuBootDir, + "--no-pager", + "--no-ask-password", + "--fsck=yes", + }, { + "systemd-mount", + "/dev/disk/by-partuuid/ubuntu-seed-partuuid", + boot.InitramfsUbuntuSeedDir, + "--no-pager", + "--no-ask-password", + "--fsck=yes", + }, { + "systemd-mount", + "/dev/disk/by-partuuid/ubuntu-data-partuuid", + boot.InitramfsDataDir, + "--no-pager", + "--no-ask-password", + "--fsck=yes", + }, { + "systemd-mount", + "/dev/disk/by-partuuid/ubuntu-save-partuuid", + boot.InitramfsUbuntuSaveDir, + "--no-pager", + "--no-ask-password", + "--fsck=yes", + }, { + "systemd-mount", + filepath.Join(dirs.SnapBlobDirUnder(boot.InitramfsWritableDir), s.core20.Filename()), + baseMnt, + "--no-pager", + "--no-ask-password", + "--fsck=no", + }, { + "systemd-mount", + filepath.Join(dirs.SnapBlobDirUnder(boot.InitramfsWritableDir), s.kernel.Filename()), + kernelMnt, + "--no-pager", + "--no-ask-password", + "--fsck=no", + }, + }) +} + +func (s *initramfsMountsSuite) TestInitramfsMountsRunModeFirstBootRecoverySystemSetHappy(c *C) { + s.mockProcCmdlineContent(c, "snapd_recovery_mode=run") + + restore := disks.MockMountPointDisksToPartitionMapping( + map[disks.Mountpoint]*disks.MockDiskMapping{ + {Mountpoint: boot.InitramfsUbuntuBootDir}: defaultBootWithSaveDisk, + {Mountpoint: boot.InitramfsDataDir}: defaultBootWithSaveDisk, + {Mountpoint: boot.InitramfsUbuntuSaveDir}: defaultBootWithSaveDisk, + }, + ) + defer restore() + + restore = s.mockSystemdMountSequence(c, []systemdMount{ + ubuntuLabelMount("ubuntu-boot", "run"), + ubuntuPartUUIDMount("ubuntu-seed-partuuid", "run"), + ubuntuPartUUIDMount("ubuntu-data-partuuid", "run"), + ubuntuPartUUIDMount("ubuntu-save-partuuid", "run"), + s.makeRunSnapSystemdMount(snap.TypeBase, s.core20), + s.makeRunSnapSystemdMount(snap.TypeKernel, s.kernel), + // RecoverySystem set makes us mount the snapd snap here + s.makeSeedSnapSystemdMount(snap.TypeSnapd), + }, nil) + defer restore() + + // mock a bootloader + bloader := boottest.MockUC20RunBootenv(bootloadertest.Mock("mock", c.MkDir())) + bootloader.Force(bloader) + defer bootloader.Force(nil) + + // set the current kernel restore = bloader.SetEnabledKernel(s.kernel) defer restore() @@ -1054,6 +1493,9 @@ _, err = main.Parser().ParseArgs([]string{"initramfs-mounts"}) c.Assert(err, IsNil) + + // we should not have written a degraded.json + c.Assert(filepath.Join(dirs.SnapBootstrapRunDir, "degraded.json"), testutil.FileAbsent) } func (s *initramfsMountsSuite) TestInitramfsMountsRunModeWithBootedKernelPartUUIDHappy(c *C) { @@ -1110,10 +1552,18 @@ func (s *initramfsMountsSuite) TestInitramfsMountsRunModeEncryptedDataHappy(c *C) { s.mockProcCmdlineContent(c, "snapd_recovery_mode=run") + // ensure that we check that access to sealed keys were locked + sealedKeysLocked := false + defer main.MockSecbootLockTPMSealedKeys(func() error { + sealedKeysLocked = true + return nil + })() + restore := disks.MockMountPointDisksToPartitionMapping( map[disks.Mountpoint]*disks.MockDiskMapping{ - {Mountpoint: boot.InitramfsUbuntuBootDir}: defaultEncBootDisk, - {Mountpoint: boot.InitramfsDataDir, IsDecryptedDevice: true}: defaultEncBootDisk, + {Mountpoint: boot.InitramfsUbuntuBootDir}: defaultEncBootDisk, + {Mountpoint: boot.InitramfsDataDir, IsDecryptedDevice: true}: defaultEncBootDisk, + {Mountpoint: boot.InitramfsUbuntuSaveDir, IsDecryptedDevice: true}: defaultEncBootDisk, }, ) defer restore() @@ -1122,32 +1572,53 @@ ubuntuLabelMount("ubuntu-boot", "run"), ubuntuPartUUIDMount("ubuntu-seed-partuuid", "run"), { - "path-to-device", + "/dev/mapper/ubuntu-data-random", boot.InitramfsDataDir, needsFsckDiskMountOpts, }, + { + "/dev/mapper/ubuntu-save-random", + boot.InitramfsUbuntuSaveDir, + needsFsckDiskMountOpts, + }, s.makeRunSnapSystemdMount(snap.TypeBase, s.core20), s.makeRunSnapSystemdMount(snap.TypeKernel, s.kernel), }, nil) defer restore() // write the installed model like makebootable does it - err := os.MkdirAll(boot.InitramfsUbuntuBootDir, 0755) + err := os.MkdirAll(filepath.Join(boot.InitramfsUbuntuBootDir, "device"), 0755) c.Assert(err, IsNil) - mf, err := os.Create(filepath.Join(boot.InitramfsUbuntuBootDir, "model")) + mf, err := os.Create(filepath.Join(boot.InitramfsUbuntuBootDir, "device/model")) c.Assert(err, IsNil) defer mf.Close() err = asserts.NewEncoder(mf).Encode(s.model) c.Assert(err, IsNil) - activated := false - restore = main.MockSecbootUnlockVolumeIfEncrypted(func(disk disks.Disk, name string, encryptionKeyDir string, lockKeysOnFinish bool) (string, bool, error) { + dataActivated := false + restore = main.MockSecbootUnlockVolumeUsingSealedKeyIfEncrypted(func(disk disks.Disk, name string, sealedEncryptionKeyFile string, opts *secboot.UnlockVolumeUsingSealedKeyOptions) (secboot.UnlockResult, error) { c.Assert(name, Equals, "ubuntu-data") - c.Assert(encryptionKeyDir, Equals, filepath.Join(s.tmpDir, "run/mnt/ubuntu-seed/device/fde")) - c.Assert(lockKeysOnFinish, Equals, true) - activated = true + c.Assert(sealedEncryptionKeyFile, Equals, filepath.Join(s.tmpDir, "run/mnt/ubuntu-boot/device/fde/ubuntu-data.sealed-key")) + c.Assert(opts, DeepEquals, &secboot.UnlockVolumeUsingSealedKeyOptions{ + AllowRecoveryKey: true, + }) + + dataActivated = true // return true because we are using an encrypted device - return "path-to-device", true, nil + return happyUnlocked("ubuntu-data", secboot.UnlockedWithSealedKey), nil + }) + defer restore() + + s.mockUbuntuSaveKeyAndMarker(c, boot.InitramfsWritableDir, "foo", "marker") + s.mockUbuntuSaveMarker(c, boot.InitramfsUbuntuSaveDir, "marker") + + saveActivated := false + restore = main.MockSecbootUnlockEncryptedVolumeUsingKey(func(disk disks.Disk, name string, key []byte) (secboot.UnlockResult, error) { + c.Check(dataActivated, Equals, true, Commentf("ubuntu-data not activated yet")) + saveActivated = true + c.Assert(name, Equals, "ubuntu-save") + c.Assert(key, DeepEquals, []byte("foo")) + return happyUnlocked("ubuntu-save", secboot.UnlockedWithKey), nil }) defer restore() @@ -1193,15 +1664,178 @@ _, err = main.Parser().ParseArgs([]string{"initramfs-mounts"}) c.Assert(err, IsNil) - c.Check(activated, Equals, true) + c.Check(dataActivated, Equals, true) + c.Check(saveActivated, Equals, true) c.Check(measureEpochCalls, Equals, 1) c.Check(measureModelCalls, Equals, 1) c.Check(measuredModel, DeepEquals, s.model) + c.Check(sealedKeysLocked, Equals, true) c.Assert(filepath.Join(dirs.SnapBootstrapRunDir, "secboot-epoch-measured"), testutil.FilePresent) c.Assert(filepath.Join(dirs.SnapBootstrapRunDir, "run-model-measured"), testutil.FilePresent) } +func (s *initramfsMountsSuite) TestInitramfsMountsRunModeEncryptedDataUnhappyNoSave(c *C) { + s.mockProcCmdlineContent(c, "snapd_recovery_mode=run") + + defaultEncNoSaveBootDisk := &disks.MockDiskMapping{ + FilesystemLabelToPartUUID: map[string]string{ + "ubuntu-boot": "ubuntu-boot-partuuid", + "ubuntu-seed": "ubuntu-seed-partuuid", + "ubuntu-data-enc": "ubuntu-data-enc-partuuid", + // missing ubuntu-save + }, + DiskHasPartitions: true, + DevNum: "defaultEncDev", + } + + restore := disks.MockMountPointDisksToPartitionMapping( + map[disks.Mountpoint]*disks.MockDiskMapping{ + {Mountpoint: boot.InitramfsUbuntuBootDir}: defaultEncNoSaveBootDisk, + {Mountpoint: boot.InitramfsDataDir, IsDecryptedDevice: true}: defaultEncNoSaveBootDisk, + }, + ) + defer restore() + + restore = s.mockSystemdMountSequence(c, []systemdMount{ + ubuntuLabelMount("ubuntu-boot", "run"), + ubuntuPartUUIDMount("ubuntu-seed-partuuid", "run"), + { + "/dev/mapper/ubuntu-data-random", + boot.InitramfsDataDir, + needsFsckDiskMountOpts, + }, + }, nil) + defer restore() + + dataActivated := false + restore = main.MockSecbootUnlockVolumeUsingSealedKeyIfEncrypted(func(disk disks.Disk, name string, sealedEncryptionKeyFile string, opts *secboot.UnlockVolumeUsingSealedKeyOptions) (secboot.UnlockResult, error) { + c.Assert(name, Equals, "ubuntu-data") + dataActivated = true + // return true because we are using an encrypted device + return happyUnlocked("ubuntu-data", secboot.UnlockedWithSealedKey), nil + }) + defer restore() + + // the test does not mock ubuntu-save.key, the secboot helper for + // opening a volume using the key should not be called + restore = main.MockSecbootUnlockEncryptedVolumeUsingKey(func(disk disks.Disk, name string, key []byte) (secboot.UnlockResult, error) { + c.Fatal("unexpected call") + return secboot.UnlockResult{}, fmt.Errorf("unexpected call") + }) + defer restore() + + restore = main.MockSecbootMeasureSnapSystemEpochWhenPossible(func() error { return nil }) + defer restore() + restore = main.MockSecbootMeasureSnapModelWhenPossible(func(findModel func() (*asserts.Model, error)) error { + return nil + }) + defer restore() + + // mock a bootloader + bloader := boottest.MockUC20RunBootenv(bootloadertest.Mock("mock", c.MkDir())) + bootloader.Force(bloader) + defer bootloader.Force(nil) + + // set the current kernel + restore = bloader.SetEnabledKernel(s.kernel) + defer restore() + + makeSnapFilesOnEarlyBootUbuntuData(c, s.kernel, s.core20) + + // write modeenv + modeEnv := boot.Modeenv{ + Mode: "run", + Base: s.core20.Filename(), + CurrentKernels: []string{s.kernel.Filename()}, + } + err := modeEnv.WriteTo(boot.InitramfsWritableDir) + c.Assert(err, IsNil) + + _, err = main.Parser().ParseArgs([]string{"initramfs-mounts"}) + c.Assert(err, ErrorMatches, "cannot find ubuntu-save encryption key at .*/run/mnt/data/system-data/var/lib/snapd/device/fde/ubuntu-save.key") + c.Check(dataActivated, Equals, true) +} + +func (s *initramfsMountsSuite) TestInitramfsMountsRunModeEncryptedDataUnhappyUnlockSaveFail(c *C) { + // ensure that we check that access to sealed keys were locked + sealedKeysLocked := false + defer main.MockSecbootLockTPMSealedKeys(func() error { + sealedKeysLocked = true + return fmt.Errorf("blocking keys failed") + })() + + s.mockProcCmdlineContent(c, "snapd_recovery_mode=run") + restore := disks.MockMountPointDisksToPartitionMapping( + map[disks.Mountpoint]*disks.MockDiskMapping{ + {Mountpoint: boot.InitramfsUbuntuBootDir}: defaultEncBootDisk, + {Mountpoint: boot.InitramfsDataDir, IsDecryptedDevice: true}: defaultEncBootDisk, + {Mountpoint: boot.InitramfsUbuntuSaveDir, IsDecryptedDevice: true}: defaultEncBootDisk, + }, + ) + defer restore() + + restore = s.mockSystemdMountSequence(c, []systemdMount{ + ubuntuLabelMount("ubuntu-boot", "run"), + ubuntuPartUUIDMount("ubuntu-seed-partuuid", "run"), + { + "/dev/mapper/ubuntu-data-random", + boot.InitramfsDataDir, + needsFsckDiskMountOpts, + }, + }, nil) + defer restore() + + dataActivated := false + restore = main.MockSecbootUnlockVolumeUsingSealedKeyIfEncrypted(func(disk disks.Disk, name string, sealedEncryptionKeyFile string, opts *secboot.UnlockVolumeUsingSealedKeyOptions) (secboot.UnlockResult, error) { + c.Assert(name, Equals, "ubuntu-data") + dataActivated = true + // return true because we are using an encrypted device + return happyUnlocked("ubuntu-data", secboot.UnlockedWithSealedKey), nil + }) + defer restore() + + s.mockUbuntuSaveKeyAndMarker(c, boot.InitramfsWritableDir, "foo", "") + restore = main.MockSecbootUnlockEncryptedVolumeUsingKey(func(disk disks.Disk, name string, key []byte) (secboot.UnlockResult, error) { + c.Check(dataActivated, Equals, true, Commentf("ubuntu-data not yet activated")) + return foundEncrypted("ubuntu-save"), fmt.Errorf("ubuntu-save unlock fail") + }) + defer restore() + + restore = main.MockSecbootMeasureSnapSystemEpochWhenPossible(func() error { return nil }) + defer restore() + restore = main.MockSecbootMeasureSnapModelWhenPossible(func(findModel func() (*asserts.Model, error)) error { + return nil + }) + defer restore() + + // mock a bootloader + bloader := boottest.MockUC20RunBootenv(bootloadertest.Mock("mock", c.MkDir())) + bootloader.Force(bloader) + defer bootloader.Force(nil) + + // set the current kernel + restore = bloader.SetEnabledKernel(s.kernel) + defer restore() + + makeSnapFilesOnEarlyBootUbuntuData(c, s.kernel, s.core20) + + // write modeenv + modeEnv := boot.Modeenv{ + Mode: "run", + Base: s.core20.Filename(), + CurrentKernels: []string{s.kernel.Filename()}, + } + err := modeEnv.WriteTo(boot.InitramfsWritableDir) + c.Assert(err, IsNil) + + _, err = main.Parser().ParseArgs([]string{"initramfs-mounts"}) + c.Assert(err, ErrorMatches, "cannot unlock ubuntu-save volume: ubuntu-save unlock fail") + c.Check(dataActivated, Equals, true) + // locking sealing keys was attempted, error was only logged + c.Check(sealedKeysLocked, Equals, true) +} + func (s *initramfsMountsSuite) TestInitramfsMountsRunModeEncryptedNoModel(c *C) { s.testInitramfsMountsEncryptedNoModel(c, "run", "", 1) } @@ -1217,6 +1851,13 @@ func (s *initramfsMountsSuite) testInitramfsMountsEncryptedNoModel(c *C, mode, label string, expectedMeasureModelCalls int) { s.mockProcCmdlineContent(c, fmt.Sprintf("snapd_recovery_mode=%s", mode)) + // ensure that we check that access to sealed keys were locked + sealedKeysLocked := false + defer main.MockSecbootLockTPMSealedKeys(func() error { + sealedKeysLocked = true + return fmt.Errorf("blocking keys failed") + })() + // install and recover mounts are just ubuntu-seed before we fail var restore func() if mode == "run" { @@ -1268,7 +1909,7 @@ defer restore() _, err := main.Parser().ParseArgs([]string{"initramfs-mounts"}) - where := "/run/mnt/ubuntu-boot/model" + where := "/run/mnt/ubuntu-boot/device/model" if mode != "run" { where = fmt.Sprintf("/run/mnt/ubuntu-seed/systems/%s/model", label) } @@ -1279,6 +1920,7 @@ gl, err := filepath.Glob(filepath.Join(dirs.SnapBootstrapRunDir, "*-model-measured")) c.Assert(err, IsNil) c.Assert(gl, HasLen, 0) + c.Check(sealedKeysLocked, Equals, true) } func (s *initramfsMountsSuite) TestInitramfsMountsRunModeUpgradeScenarios(c *C) { @@ -1602,6 +2244,14 @@ } func (s *initramfsMountsSuite) testRecoverModeHappy(c *C) { + // ensure that we check that access to sealed keys were locked + sealedKeysLocked := false + restore := main.MockSecbootLockTPMSealedKeys(func() error { + sealedKeysLocked = true + return nil + }) + defer restore() + // mock various files that are copied around during recover mode (and files // that shouldn't be copied around) ephemeralUbuntuData := filepath.Join(boot.InitramfsRunMntDir, "data/") @@ -1662,6 +2312,9 @@ _, err = main.Parser().ParseArgs([]string{"initramfs-mounts"}) c.Assert(err, IsNil) + // we always need to lock access to sealed keys + c.Check(sealedKeysLocked, Equals, true) + modeEnv := filepath.Join(ephemeralUbuntuData, "/system-data/var/lib/snapd/modeenv") c.Check(modeEnv, testutil.FileEquals, `mode=recover recovery_system=20191118 @@ -1709,8 +2362,10 @@ restore = disks.MockMountPointDisksToPartitionMapping( map[disks.Mountpoint]*disks.MockDiskMapping{ - {Mountpoint: boot.InitramfsUbuntuSeedDir}: defaultBootDisk, - {Mountpoint: boot.InitramfsHostUbuntuDataDir}: defaultBootDisk, + {Mountpoint: boot.InitramfsUbuntuSeedDir}: defaultBootWithSaveDisk, + {Mountpoint: boot.InitramfsUbuntuBootDir}: defaultBootWithSaveDisk, + {Mountpoint: boot.InitramfsHostUbuntuDataDir}: defaultBootWithSaveDisk, + {Mountpoint: boot.InitramfsUbuntuSaveDir}: defaultBootWithSaveDisk, }, ) defer restore() @@ -1726,14 +2381,27 @@ tmpfsMountOpts, }, { - "/dev/disk/by-partuuid/ubuntu-data-partuuid", - boot.InitramfsHostUbuntuDataDir, + "/dev/disk/by-partuuid/ubuntu-boot-partuuid", + boot.InitramfsUbuntuBootDir, + needsFsckDiskMountOpts, + }, + { + "/dev/disk/by-partuuid/ubuntu-data-partuuid", + boot.InitramfsHostUbuntuDataDir, + nil, + }, + { + "/dev/disk/by-partuuid/ubuntu-save-partuuid", + boot.InitramfsUbuntuSaveDir, nil, }, }, nil) defer restore() s.testRecoverModeHappy(c) + + // we should not have written a degraded.json + c.Assert(filepath.Join(dirs.SnapBootstrapRunDir, "degraded.json"), testutil.FileAbsent) } func (s *initramfsMountsSuite) TestInitramfsMountsRecoverModeGadgetDefaultsHappy(c *C) { @@ -1766,8 +2434,10 @@ restore = disks.MockMountPointDisksToPartitionMapping( map[disks.Mountpoint]*disks.MockDiskMapping{ - {Mountpoint: boot.InitramfsUbuntuSeedDir}: defaultBootDisk, - {Mountpoint: boot.InitramfsHostUbuntuDataDir}: defaultBootDisk, + {Mountpoint: boot.InitramfsUbuntuSeedDir}: defaultBootWithSaveDisk, + {Mountpoint: boot.InitramfsUbuntuBootDir}: defaultBootWithSaveDisk, + {Mountpoint: boot.InitramfsHostUbuntuDataDir}: defaultBootWithSaveDisk, + {Mountpoint: boot.InitramfsUbuntuSaveDir}: defaultBootWithSaveDisk, }, ) defer restore() @@ -1783,10 +2453,20 @@ tmpfsMountOpts, }, { + "/dev/disk/by-partuuid/ubuntu-boot-partuuid", + boot.InitramfsUbuntuBootDir, + needsFsckDiskMountOpts, + }, + { "/dev/disk/by-partuuid/ubuntu-data-partuuid", boot.InitramfsHostUbuntuDataDir, nil, }, + { + "/dev/disk/by-partuuid/ubuntu-save-partuuid", + boot.InitramfsUbuntuSaveDir, + nil, + }, }, nil) defer restore() @@ -1801,6 +2481,9 @@ s.testRecoverModeHappy(c) + // we should not have written a degraded.json + c.Assert(filepath.Join(dirs.SnapBootstrapRunDir, "degraded.json"), testutil.FileAbsent) + c.Assert(osutil.FileExists(filepath.Join(boot.InitramfsWritableDir, "_writable_defaults/etc/cloud/cloud-init.disabled")), Equals, true) // check that everything from the gadget defaults was setup @@ -1826,8 +2509,10 @@ restore = disks.MockMountPointDisksToPartitionMapping( map[disks.Mountpoint]*disks.MockDiskMapping{ - {Mountpoint: boot.InitramfsUbuntuSeedDir}: defaultBootDisk, - {Mountpoint: boot.InitramfsHostUbuntuDataDir}: defaultBootDisk, + {Mountpoint: boot.InitramfsUbuntuSeedDir}: defaultBootWithSaveDisk, + {Mountpoint: boot.InitramfsUbuntuBootDir}: defaultBootWithSaveDisk, + {Mountpoint: boot.InitramfsHostUbuntuDataDir}: defaultBootWithSaveDisk, + {Mountpoint: boot.InitramfsUbuntuSaveDir}: defaultBootWithSaveDisk, }, ) defer restore() @@ -1847,14 +2532,27 @@ tmpfsMountOpts, }, { + "/dev/disk/by-partuuid/ubuntu-boot-partuuid", + boot.InitramfsUbuntuBootDir, + needsFsckDiskMountOpts, + }, + { "/dev/disk/by-partuuid/ubuntu-data-partuuid", boot.InitramfsHostUbuntuDataDir, nil, }, + { + "/dev/disk/by-partuuid/ubuntu-save-partuuid", + boot.InitramfsUbuntuSaveDir, + nil, + }, }, nil) defer restore() s.testRecoverModeHappy(c) + + // we should not have written a degraded.json + c.Assert(filepath.Join(dirs.SnapBootstrapRunDir, "degraded.json"), testutil.FileAbsent) } func (s *initramfsMountsSuite) TestInitramfsMountsRecoverModeHappyEncrypted(c *C) { @@ -1871,24 +2569,45 @@ restore = disks.MockMountPointDisksToPartitionMapping( map[disks.Mountpoint]*disks.MockDiskMapping{ {Mountpoint: boot.InitramfsUbuntuSeedDir}: defaultEncBootDisk, + {Mountpoint: boot.InitramfsUbuntuBootDir}: defaultEncBootDisk, { Mountpoint: boot.InitramfsHostUbuntuDataDir, IsDecryptedDevice: true, }: defaultEncBootDisk, + { + Mountpoint: boot.InitramfsUbuntuSaveDir, + IsDecryptedDevice: true, + }: defaultEncBootDisk, }, ) defer restore() - activated := false - restore = main.MockSecbootUnlockVolumeIfEncrypted(func(disk disks.Disk, name string, encryptionKeyDir string, lockKeysOnFinish bool) (string, bool, error) { + dataActivated := false + restore = main.MockSecbootUnlockVolumeUsingSealedKeyIfEncrypted(func(disk disks.Disk, name string, sealedEncryptionKeyFile string, opts *secboot.UnlockVolumeUsingSealedKeyOptions) (secboot.UnlockResult, error) { c.Assert(name, Equals, "ubuntu-data") - c.Assert(encryptionKeyDir, Equals, filepath.Join(s.tmpDir, "run/mnt/ubuntu-seed/device/fde")) + c.Assert(sealedEncryptionKeyFile, Equals, filepath.Join(s.tmpDir, "run/mnt/ubuntu-boot/device/fde/ubuntu-data.sealed-key")) + encDevPartUUID, err := disk.FindMatchingPartitionUUID(name + "-enc") c.Assert(err, IsNil) c.Assert(encDevPartUUID, Equals, "ubuntu-data-enc-partuuid") - c.Assert(lockKeysOnFinish, Equals, true) - activated = true - return filepath.Join("/dev/disk/by-partuuid", encDevPartUUID), true, nil + c.Assert(opts, DeepEquals, &secboot.UnlockVolumeUsingSealedKeyOptions{}) + dataActivated = true + return happyUnlocked("ubuntu-data", secboot.UnlockedWithSealedKey), nil + }) + defer restore() + + s.mockUbuntuSaveKeyAndMarker(c, boot.InitramfsHostWritableDir, "foo", "marker") + s.mockUbuntuSaveMarker(c, boot.InitramfsUbuntuSaveDir, "marker") + + saveActivated := false + restore = main.MockSecbootUnlockEncryptedVolumeUsingKey(func(disk disks.Disk, name string, key []byte) (secboot.UnlockResult, error) { + c.Check(dataActivated, Equals, true, Commentf("ubuntu-data not activated yet")) + encDevPartUUID, err := disk.FindMatchingPartitionUUID(name + "-enc") + c.Assert(err, IsNil) + c.Assert(encDevPartUUID, Equals, "ubuntu-save-enc-partuuid") + c.Assert(key, DeepEquals, []byte("foo")) + saveActivated = true + return happyUnlocked("ubuntu-save", secboot.UnlockedWithKey), nil }) defer restore() @@ -1923,16 +2642,30 @@ tmpfsMountOpts, }, { - "/dev/disk/by-partuuid/ubuntu-data-enc-partuuid", + "/dev/disk/by-partuuid/ubuntu-boot-partuuid", + boot.InitramfsUbuntuBootDir, + needsFsckDiskMountOpts, + }, + { + "/dev/mapper/ubuntu-data-random", boot.InitramfsHostUbuntuDataDir, nil, }, + { + "/dev/mapper/ubuntu-save-random", + boot.InitramfsUbuntuSaveDir, + nil, + }, }, nil) defer restore() s.testRecoverModeHappy(c) - c.Check(activated, Equals, true) + // we should not have written a degraded.json + c.Assert(filepath.Join(dirs.SnapBootstrapRunDir, "degraded.json"), testutil.FileAbsent) + + c.Check(dataActivated, Equals, true) + c.Check(saveActivated, Equals, true) c.Check(measureEpochCalls, Equals, 1) c.Check(measureModelCalls, Equals, 1) c.Check(measuredModel, DeepEquals, s.model) @@ -1941,55 +2674,90 @@ c.Assert(filepath.Join(dirs.SnapBootstrapRunDir, fmt.Sprintf("%s-model-measured", s.sysLabel)), testutil.FilePresent) } -func (s *initramfsMountsSuite) TestInitramfsMountsRecoverModeEncryptedAttackerFSAttachedHappy(c *C) { +func checkDegradedJSON(c *C, exp map[string]interface{}) { + b, err := ioutil.ReadFile(filepath.Join(dirs.SnapBootstrapRunDir, "degraded.json")) + c.Assert(err, IsNil) + degradedJSONObj := make(map[string]interface{}, 0) + err = json.Unmarshal(b, °radedJSONObj) + c.Assert(err, IsNil) + + c.Assert(degradedJSONObj, DeepEquals, exp) +} + +func (s *initramfsMountsSuite) TestInitramfsMountsRecoverModeEncryptedDegradedDataUnlockFallbackHappy(c *C) { s.mockProcCmdlineContent(c, "snapd_recovery_mode=recover snapd_recovery_system="+s.sysLabel) restore := main.MockPartitionUUIDForBootedKernelDisk("") defer restore() - // setup a bootloader for setting the bootenv + // setup a bootloader for setting the bootenv after we are done bloader := bootloadertest.Mock("mock", c.MkDir()) bootloader.Force(bloader) defer bootloader.Force(nil) - mockDisk := &disks.MockDiskMapping{ - FilesystemLabelToPartUUID: map[string]string{ - "ubuntu-seed": "ubuntu-seed-partuuid", - "ubuntu-data-enc": "ubuntu-data-enc-partuuid", - }, - DiskHasPartitions: true, - DevNum: "bootDev", - } - restore = disks.MockMountPointDisksToPartitionMapping( map[disks.Mountpoint]*disks.MockDiskMapping{ - {Mountpoint: boot.InitramfsUbuntuSeedDir}: mockDisk, + {Mountpoint: boot.InitramfsUbuntuSeedDir}: defaultEncBootDisk, + {Mountpoint: boot.InitramfsUbuntuBootDir}: defaultEncBootDisk, { Mountpoint: boot.InitramfsHostUbuntuDataDir, IsDecryptedDevice: true, - }: mockDisk, - // this is the attacker fs on a different disk - {Mountpoint: "somewhere-else"}: { - FilesystemLabelToPartUUID: map[string]string{ - "ubuntu-seed": "ubuntu-seed-attacker-partuuid", - "ubuntu-data-enc": "ubuntu-data-enc-attacker-partuuid", - }, - DiskHasPartitions: true, - DevNum: "attackerDev", - }, + }: defaultEncBootDisk, + { + Mountpoint: boot.InitramfsUbuntuSaveDir, + IsDecryptedDevice: true, + }: defaultEncBootDisk, }, ) defer restore() - activated := false - restore = main.MockSecbootUnlockVolumeIfEncrypted(func(disk disks.Disk, name string, encryptionKeyDir string, lockKeysOnFinish bool) (string, bool, error) { - c.Assert(name, Equals, "ubuntu-data") + dataActivated := false + saveActivated := false + unlockVolumeWithSealedKeyCalls := 0 + restore = main.MockSecbootUnlockVolumeUsingSealedKeyIfEncrypted(func(disk disks.Disk, name string, sealedEncryptionKeyFile string, opts *secboot.UnlockVolumeUsingSealedKeyOptions) (secboot.UnlockResult, error) { + unlockVolumeWithSealedKeyCalls++ + switch unlockVolumeWithSealedKeyCalls { + + case 1: + // pretend we can't unlock ubuntu-data with the main run key + c.Assert(name, Equals, "ubuntu-data") + c.Assert(sealedEncryptionKeyFile, Equals, filepath.Join(s.tmpDir, "run/mnt/ubuntu-boot/device/fde/ubuntu-data.sealed-key")) + encDevPartUUID, err := disk.FindMatchingPartitionUUID(name + "-enc") + c.Assert(err, IsNil) + c.Assert(encDevPartUUID, Equals, "ubuntu-data-enc-partuuid") + c.Assert(opts, DeepEquals, &secboot.UnlockVolumeUsingSealedKeyOptions{}) + return foundEncrypted("ubuntu-data"), fmt.Errorf("failed to unlock ubuntu-data") + + case 2: + // now we can unlock ubuntu-data with the fallback key + c.Assert(name, Equals, "ubuntu-data") + c.Assert(sealedEncryptionKeyFile, Equals, filepath.Join(s.tmpDir, "run/mnt/ubuntu-seed/device/fde/ubuntu-data.recovery.sealed-key")) + encDevPartUUID, err := disk.FindMatchingPartitionUUID(name + "-enc") + c.Assert(err, IsNil) + c.Assert(encDevPartUUID, Equals, "ubuntu-data-enc-partuuid") + c.Assert(opts, DeepEquals, &secboot.UnlockVolumeUsingSealedKeyOptions{ + AllowRecoveryKey: true, + }) + dataActivated = true + return happyUnlocked("ubuntu-data", secboot.UnlockedWithSealedKey), nil + default: + c.Errorf("unexpected call to UnlockVolumeUsingSealedKeyIfEncrypted (num %d)", unlockVolumeWithSealedKeyCalls) + return secboot.UnlockResult{}, fmt.Errorf("broken test") + } + }) + defer restore() + + s.mockUbuntuSaveKeyAndMarker(c, boot.InitramfsHostWritableDir, "foo", "marker") + s.mockUbuntuSaveMarker(c, boot.InitramfsUbuntuSaveDir, "marker") + + restore = main.MockSecbootUnlockEncryptedVolumeUsingKey(func(disk disks.Disk, name string, key []byte) (secboot.UnlockResult, error) { + c.Check(dataActivated, Equals, true, Commentf("ubuntu-data not activated yet")) encDevPartUUID, err := disk.FindMatchingPartitionUUID(name + "-enc") c.Assert(err, IsNil) - c.Assert(encDevPartUUID, Equals, "ubuntu-data-enc-partuuid") - c.Assert(lockKeysOnFinish, Equals, true) - activated = true - return filepath.Join("/dev/disk/by-partuuid", encDevPartUUID), true, nil + c.Assert(encDevPartUUID, Equals, "ubuntu-save-enc-partuuid") + c.Assert(key, DeepEquals, []byte("foo")) + saveActivated = true + return happyUnlocked("ubuntu-save", secboot.UnlockedWithKey), nil }) defer restore() @@ -2024,16 +2792,56 @@ tmpfsMountOpts, }, { - "/dev/disk/by-partuuid/ubuntu-data-enc-partuuid", + "/dev/disk/by-partuuid/ubuntu-boot-partuuid", + boot.InitramfsUbuntuBootDir, + needsFsckDiskMountOpts, + }, + { + "/dev/mapper/ubuntu-data-random", boot.InitramfsHostUbuntuDataDir, nil, }, + { + "/dev/mapper/ubuntu-save-random", + boot.InitramfsUbuntuSaveDir, + nil, + }, }, nil) defer restore() s.testRecoverModeHappy(c) - c.Check(activated, Equals, true) + checkDegradedJSON(c, map[string]interface{}{ + "ubuntu-boot": map[string]interface{}{ + "find-state": "found", + "mount-state": "mounted", + "device": "/dev/disk/by-partuuid/ubuntu-boot-partuuid", + "mount-location": boot.InitramfsUbuntuBootDir, + }, + "ubuntu-data": map[string]interface{}{ + "device": "/dev/mapper/ubuntu-data-random", + "unlock-state": "unlocked", + "find-state": "found", + "mount-state": "mounted", + "unlock-key": "fallback", + "mount-location": boot.InitramfsHostUbuntuDataDir, + }, + "ubuntu-save": map[string]interface{}{ + "device": "/dev/mapper/ubuntu-save-random", + "unlock-key": "run", + "unlock-state": "unlocked", + "mount-state": "mounted", + "find-state": "found", + "mount-location": boot.InitramfsUbuntuSaveDir, + }, + "error-log": []interface{}{ + "cannot unlock encrypted ubuntu-data (device /dev/disk/by-partuuid/ubuntu-data-enc-partuuid) with sealed run key: failed to unlock ubuntu-data", + }, + }) + + c.Check(dataActivated, Equals, true) + c.Check(unlockVolumeWithSealedKeyCalls, Equals, 2) + c.Check(saveActivated, Equals, true) c.Check(measureEpochCalls, Equals, 1) c.Check(measureModelCalls, Equals, 1) c.Check(measuredModel, DeepEquals, s.model) @@ -2042,11 +2850,109 @@ c.Assert(filepath.Join(dirs.SnapBootstrapRunDir, fmt.Sprintf("%s-model-measured", s.sysLabel)), testutil.FilePresent) } -func (s *initramfsMountsSuite) testInitramfsMountsInstallRecoverModeMeasure(c *C, mode string) { - s.mockProcCmdlineContent(c, fmt.Sprintf("snapd_recovery_mode=%s snapd_recovery_system=%s", mode, s.sysLabel)) +func (s *initramfsMountsSuite) TestInitramfsMountsRecoverModeEncryptedDegradedSaveUnlockFallbackHappy(c *C) { + s.mockProcCmdlineContent(c, "snapd_recovery_mode=recover snapd_recovery_system="+s.sysLabel) - modeMnts := []systemdMount{ - ubuntuLabelMount("ubuntu-seed", mode), + restore := main.MockPartitionUUIDForBootedKernelDisk("") + defer restore() + + // setup a bootloader for setting the bootenv after we are done + bloader := bootloadertest.Mock("mock", c.MkDir()) + bootloader.Force(bloader) + defer bootloader.Force(nil) + + restore = disks.MockMountPointDisksToPartitionMapping( + map[disks.Mountpoint]*disks.MockDiskMapping{ + {Mountpoint: boot.InitramfsUbuntuSeedDir}: defaultEncBootDisk, + {Mountpoint: boot.InitramfsUbuntuBootDir}: defaultEncBootDisk, + { + Mountpoint: boot.InitramfsHostUbuntuDataDir, + IsDecryptedDevice: true, + }: defaultEncBootDisk, + { + Mountpoint: boot.InitramfsUbuntuSaveDir, + IsDecryptedDevice: true, + }: defaultEncBootDisk, + }, + ) + defer restore() + + dataActivated := false + saveActivationAttempted := false + unlockVolumeWithSealedKeyCalls := 0 + restore = main.MockSecbootUnlockVolumeUsingSealedKeyIfEncrypted(func(disk disks.Disk, name string, sealedEncryptionKeyFile string, opts *secboot.UnlockVolumeUsingSealedKeyOptions) (secboot.UnlockResult, error) { + unlockVolumeWithSealedKeyCalls++ + switch unlockVolumeWithSealedKeyCalls { + + case 1: + // ubuntu data can be unlocked fine + c.Assert(name, Equals, "ubuntu-data") + c.Assert(sealedEncryptionKeyFile, Equals, filepath.Join(s.tmpDir, "run/mnt/ubuntu-boot/device/fde/ubuntu-data.sealed-key")) + encDevPartUUID, err := disk.FindMatchingPartitionUUID(name + "-enc") + c.Assert(err, IsNil) + c.Assert(encDevPartUUID, Equals, "ubuntu-data-enc-partuuid") + c.Assert(opts, DeepEquals, &secboot.UnlockVolumeUsingSealedKeyOptions{}) + dataActivated = true + return happyUnlocked("ubuntu-data", secboot.UnlockedWithSealedKey), nil + + case 2: + // then after ubuntu-save is attempted to be unlocked with the + // unsealed run object on the encrypted data partition, we fall back + // to using the sealed object on ubuntu-seed for save + c.Assert(saveActivationAttempted, Equals, true) + c.Assert(name, Equals, "ubuntu-save") + c.Assert(sealedEncryptionKeyFile, Equals, filepath.Join(s.tmpDir, "run/mnt/ubuntu-seed/device/fde/ubuntu-save.recovery.sealed-key")) + encDevPartUUID, err := disk.FindMatchingPartitionUUID(name + "-enc") + c.Assert(err, IsNil) + c.Assert(encDevPartUUID, Equals, "ubuntu-save-enc-partuuid") + c.Assert(opts, DeepEquals, &secboot.UnlockVolumeUsingSealedKeyOptions{ + AllowRecoveryKey: true, + }) + dataActivated = true + return happyUnlocked("ubuntu-save", secboot.UnlockedWithSealedKey), nil + default: + c.Errorf("unexpected call to UnlockVolumeUsingSealedKeyIfEncrypted (num %d)", unlockVolumeWithSealedKeyCalls) + return secboot.UnlockResult{}, fmt.Errorf("broken test") + } + }) + defer restore() + + s.mockUbuntuSaveKeyAndMarker(c, boot.InitramfsHostWritableDir, "foo", "marker") + s.mockUbuntuSaveMarker(c, boot.InitramfsUbuntuSaveDir, "marker") + + restore = main.MockSecbootUnlockEncryptedVolumeUsingKey(func(disk disks.Disk, name string, key []byte) (secboot.UnlockResult, error) { + c.Check(dataActivated, Equals, true, Commentf("ubuntu-data not activated yet")) + encDevPartUUID, err := disk.FindMatchingPartitionUUID(name + "-enc") + c.Assert(err, IsNil) + c.Assert(encDevPartUUID, Equals, "ubuntu-save-enc-partuuid") + c.Assert(key, DeepEquals, []byte("foo")) + saveActivationAttempted = true + return foundEncrypted("ubuntu-save"), fmt.Errorf("failed to unlock ubuntu-save with run object") + }) + defer restore() + + measureEpochCalls := 0 + measureModelCalls := 0 + restore = main.MockSecbootMeasureSnapSystemEpochWhenPossible(func() error { + measureEpochCalls++ + return nil + }) + defer restore() + + var measuredModel *asserts.Model + restore = main.MockSecbootMeasureSnapModelWhenPossible(func(findModel func() (*asserts.Model, error)) error { + measureModelCalls++ + var err error + measuredModel, err = findModel() + if err != nil { + return err + } + return nil + }) + defer restore() + + restore = s.mockSystemdMountSequence(c, []systemdMount{ + ubuntuLabelMount("ubuntu-seed", "recover"), s.makeSeedSnapSystemdMount(snap.TypeSnapd), s.makeSeedSnapSystemdMount(snap.TypeKernel), s.makeSeedSnapSystemdMount(snap.TypeBase), @@ -2055,38 +2961,1680 @@ boot.InitramfsDataDir, tmpfsMountOpts, }, - } + { + "/dev/disk/by-partuuid/ubuntu-boot-partuuid", + boot.InitramfsUbuntuBootDir, + needsFsckDiskMountOpts, + }, + { + "/dev/mapper/ubuntu-data-random", + boot.InitramfsHostUbuntuDataDir, + nil, + }, + { + "/dev/mapper/ubuntu-save-random", + boot.InitramfsUbuntuSaveDir, + nil, + }, + }, nil) + defer restore() - mockDiskMapping := map[disks.Mountpoint]*disks.MockDiskMapping{ - {Mountpoint: boot.InitramfsUbuntuSeedDir}: { - FilesystemLabelToPartUUID: map[string]string{ - "ubuntu-seed": "ubuntu-seed-partuuid", - }, - DiskHasPartitions: true, + s.testRecoverModeHappy(c) + + checkDegradedJSON(c, map[string]interface{}{ + "ubuntu-boot": map[string]interface{}{ + "find-state": "found", + "mount-state": "mounted", + "device": "/dev/disk/by-partuuid/ubuntu-boot-partuuid", + "mount-location": boot.InitramfsUbuntuBootDir, + }, + "ubuntu-data": map[string]interface{}{ + "device": "/dev/mapper/ubuntu-data-random", + "unlock-state": "unlocked", + "find-state": "found", + "mount-state": "mounted", + "unlock-key": "run", + "mount-location": boot.InitramfsHostUbuntuDataDir, + }, + "ubuntu-save": map[string]interface{}{ + "device": "/dev/mapper/ubuntu-save-random", + "unlock-key": "fallback", + "unlock-state": "unlocked", + "mount-state": "mounted", + "find-state": "found", + "mount-location": boot.InitramfsUbuntuSaveDir, + }, + "error-log": []interface{}{ + "cannot unlock encrypted ubuntu-save (device /dev/disk/by-partuuid/ubuntu-save-enc-partuuid) with sealed run key: failed to unlock ubuntu-save with run object", + }, + }) + + c.Check(dataActivated, Equals, true) + c.Check(unlockVolumeWithSealedKeyCalls, Equals, 2) + c.Check(saveActivationAttempted, Equals, true) + c.Check(measureEpochCalls, Equals, 1) + c.Check(measureModelCalls, Equals, 1) + c.Check(measuredModel, DeepEquals, s.model) + + c.Assert(filepath.Join(dirs.SnapBootstrapRunDir, "secboot-epoch-measured"), testutil.FilePresent) + c.Assert(filepath.Join(dirs.SnapBootstrapRunDir, fmt.Sprintf("%s-model-measured", s.sysLabel)), testutil.FilePresent) +} + +func (s *initramfsMountsSuite) TestInitramfsMountsRecoverModeEncryptedDegradedAbsentBootDataUnlockFallbackHappy(c *C) { + s.mockProcCmdlineContent(c, "snapd_recovery_mode=recover snapd_recovery_system="+s.sysLabel) + + restore := main.MockPartitionUUIDForBootedKernelDisk("") + defer restore() + + // setup a bootloader for setting the bootenv after we are done + bloader := bootloadertest.Mock("mock", c.MkDir()) + bootloader.Force(bloader) + defer bootloader.Force(nil) + + defaultEncDiskNoBoot := &disks.MockDiskMapping{ + FilesystemLabelToPartUUID: map[string]string{ + "ubuntu-seed": "ubuntu-seed-partuuid", + "ubuntu-data-enc": "ubuntu-data-enc-partuuid", + "ubuntu-save-enc": "ubuntu-save-enc-partuuid", }, + DiskHasPartitions: true, + DevNum: "defaultEncDevNoBoot", } - if mode == "recover" { - // setup a bootloader for setting the bootenv after we are done - bloader := bootloadertest.Mock("mock", c.MkDir()) - bootloader.Force(bloader) - defer bootloader.Force(nil) + restore = disks.MockMountPointDisksToPartitionMapping( + map[disks.Mountpoint]*disks.MockDiskMapping{ + {Mountpoint: boot.InitramfsUbuntuSeedDir}: defaultEncDiskNoBoot, + // no ubuntu-boot so we fall back to unlocking data with fallback + // key right away + { + Mountpoint: boot.InitramfsHostUbuntuDataDir, + IsDecryptedDevice: true, + }: defaultEncDiskNoBoot, + { + Mountpoint: boot.InitramfsUbuntuSaveDir, + IsDecryptedDevice: true, + }: defaultEncDiskNoBoot, + }, + ) + defer restore() - // add the expected mount of ubuntu-data onto the host data dir - modeMnts = append(modeMnts, systemdMount{ - "/dev/disk/by-partuuid/ubuntu-data-partuuid", - boot.InitramfsHostUbuntuDataDir, - nil, - }) + dataActivated := false + unlockVolumeWithSealedKeyCalls := 0 + restore = main.MockSecbootUnlockVolumeUsingSealedKeyIfEncrypted(func(disk disks.Disk, name string, sealedEncryptionKeyFile string, opts *secboot.UnlockVolumeUsingSealedKeyOptions) (secboot.UnlockResult, error) { + unlockVolumeWithSealedKeyCalls++ + switch unlockVolumeWithSealedKeyCalls { + case 1: + // we skip trying to unlock with run key on ubuntu-boot and go + // directly to using the fallback key on ubuntu-seed + c.Assert(name, Equals, "ubuntu-data") + c.Assert(sealedEncryptionKeyFile, Equals, filepath.Join(s.tmpDir, "run/mnt/ubuntu-seed/device/fde/ubuntu-data.recovery.sealed-key")) + encDevPartUUID, err := disk.FindMatchingPartitionUUID(name + "-enc") + c.Assert(err, IsNil) + c.Assert(encDevPartUUID, Equals, "ubuntu-data-enc-partuuid") + c.Assert(opts, DeepEquals, &secboot.UnlockVolumeUsingSealedKeyOptions{ + AllowRecoveryKey: true, + }) + dataActivated = true + return happyUnlocked("ubuntu-data", secboot.UnlockedWithSealedKey), nil + default: + c.Errorf("unexpected call to UnlockVolumeUsingSealedKeyIfEncrypted (num %d)", unlockVolumeWithSealedKeyCalls) + return secboot.UnlockResult{}, fmt.Errorf("broken test") + } + }) + defer restore() + + s.mockUbuntuSaveKeyAndMarker(c, boot.InitramfsHostWritableDir, "foo", "marker") + s.mockUbuntuSaveMarker(c, boot.InitramfsUbuntuSaveDir, "marker") + + restore = main.MockSecbootUnlockEncryptedVolumeUsingKey(func(disk disks.Disk, name string, key []byte) (secboot.UnlockResult, error) { + c.Check(dataActivated, Equals, true, Commentf("ubuntu-data not activated yet")) + encDevPartUUID, err := disk.FindMatchingPartitionUUID(name + "-enc") + c.Assert(err, IsNil) + c.Assert(encDevPartUUID, Equals, "ubuntu-save-enc-partuuid") + c.Assert(key, DeepEquals, []byte("foo")) + return happyUnlocked("ubuntu-save", secboot.UnlockedWithKey), nil + }) + defer restore() + + measureEpochCalls := 0 + measureModelCalls := 0 + restore = main.MockSecbootMeasureSnapSystemEpochWhenPossible(func() error { + measureEpochCalls++ + return nil + }) + defer restore() + + var measuredModel *asserts.Model + restore = main.MockSecbootMeasureSnapModelWhenPossible(func(findModel func() (*asserts.Model, error)) error { + measureModelCalls++ + var err error + measuredModel, err = findModel() + if err != nil { + return err + } + return nil + }) + defer restore() + + restore = s.mockSystemdMountSequence(c, []systemdMount{ + ubuntuLabelMount("ubuntu-seed", "recover"), + s.makeSeedSnapSystemdMount(snap.TypeSnapd), + s.makeSeedSnapSystemdMount(snap.TypeKernel), + s.makeSeedSnapSystemdMount(snap.TypeBase), + { + "tmpfs", + boot.InitramfsDataDir, + tmpfsMountOpts, + }, + // no ubuntu-boot + { + "/dev/mapper/ubuntu-data-random", + boot.InitramfsHostUbuntuDataDir, + nil, + }, + { + "/dev/mapper/ubuntu-save-random", + boot.InitramfsUbuntuSaveDir, + nil, + }, + }, nil) + defer restore() + + s.testRecoverModeHappy(c) + + checkDegradedJSON(c, map[string]interface{}{ + "ubuntu-boot": map[string]interface{}{ + "find-state": "not-found", + }, + "ubuntu-data": map[string]interface{}{ + "device": "/dev/mapper/ubuntu-data-random", + "unlock-state": "unlocked", + "find-state": "found", + "mount-state": "mounted", + "unlock-key": "fallback", + "mount-location": boot.InitramfsHostUbuntuDataDir, + }, + "ubuntu-save": map[string]interface{}{ + "device": "/dev/mapper/ubuntu-save-random", + "unlock-key": "run", + "unlock-state": "unlocked", + "mount-state": "mounted", + "find-state": "found", + "mount-location": boot.InitramfsUbuntuSaveDir, + }, + "error-log": []interface{}{ + "cannot find ubuntu-boot partition on disk defaultEncDevNoBoot", + }, + }) + + c.Check(dataActivated, Equals, true) + c.Check(unlockVolumeWithSealedKeyCalls, Equals, 1) + c.Check(measureEpochCalls, Equals, 1) + c.Check(measureModelCalls, Equals, 1) + c.Check(measuredModel, DeepEquals, s.model) + + c.Assert(filepath.Join(dirs.SnapBootstrapRunDir, "secboot-epoch-measured"), testutil.FilePresent) + c.Assert(filepath.Join(dirs.SnapBootstrapRunDir, fmt.Sprintf("%s-model-measured", s.sysLabel)), testutil.FilePresent) +} + +func (s *initramfsMountsSuite) TestInitramfsMountsRecoverModeEncryptedDegradedAbsentBootDataUnlockRecoveryKeyHappy(c *C) { + s.mockProcCmdlineContent(c, "snapd_recovery_mode=recover snapd_recovery_system="+s.sysLabel) + + restore := main.MockPartitionUUIDForBootedKernelDisk("") + defer restore() + + // setup a bootloader for setting the bootenv after we are done + bloader := bootloadertest.Mock("mock", c.MkDir()) + bootloader.Force(bloader) + defer bootloader.Force(nil) + + defaultEncDiskNoBoot := &disks.MockDiskMapping{ + FilesystemLabelToPartUUID: map[string]string{ + "ubuntu-seed": "ubuntu-seed-partuuid", + "ubuntu-data-enc": "ubuntu-data-enc-partuuid", + "ubuntu-save-enc": "ubuntu-save-enc-partuuid", + }, + DiskHasPartitions: true, + DevNum: "defaultEncDevNoBoot", + } + + restore = disks.MockMountPointDisksToPartitionMapping( + map[disks.Mountpoint]*disks.MockDiskMapping{ + {Mountpoint: boot.InitramfsUbuntuSeedDir}: defaultEncDiskNoBoot, + // no ubuntu-boot so we fall back to unlocking data with fallback + // key right away + { + Mountpoint: boot.InitramfsHostUbuntuDataDir, + IsDecryptedDevice: true, + }: defaultEncDiskNoBoot, + { + Mountpoint: boot.InitramfsUbuntuSaveDir, + IsDecryptedDevice: true, + }: defaultEncDiskNoBoot, + }, + ) + defer restore() + + dataActivated := false + unlockVolumeWithSealedKeyCalls := 0 + restore = main.MockSecbootUnlockVolumeUsingSealedKeyIfEncrypted(func(disk disks.Disk, name string, sealedEncryptionKeyFile string, opts *secboot.UnlockVolumeUsingSealedKeyOptions) (secboot.UnlockResult, error) { + unlockVolumeWithSealedKeyCalls++ + switch unlockVolumeWithSealedKeyCalls { + case 1: + // we skip trying to unlock with run key on ubuntu-boot and go + // directly to using the fallback key on ubuntu-seed + c.Assert(name, Equals, "ubuntu-data") + c.Assert(sealedEncryptionKeyFile, Equals, filepath.Join(s.tmpDir, "run/mnt/ubuntu-seed/device/fde/ubuntu-data.recovery.sealed-key")) + encDevPartUUID, err := disk.FindMatchingPartitionUUID(name + "-enc") + c.Assert(err, IsNil) + c.Assert(encDevPartUUID, Equals, "ubuntu-data-enc-partuuid") + c.Assert(opts, DeepEquals, &secboot.UnlockVolumeUsingSealedKeyOptions{ + AllowRecoveryKey: true, + }) + dataActivated = true + // it was unlocked with a recovery key + + return happyUnlocked("ubuntu-data", secboot.UnlockedWithRecoveryKey), nil + default: + c.Errorf("unexpected call to UnlockVolumeUsingSealedKeyIfEncrypted (num %d)", unlockVolumeWithSealedKeyCalls) + return secboot.UnlockResult{}, fmt.Errorf("broken test") + } + }) + defer restore() + + s.mockUbuntuSaveKeyAndMarker(c, boot.InitramfsHostWritableDir, "foo", "marker") + s.mockUbuntuSaveMarker(c, boot.InitramfsUbuntuSaveDir, "marker") + + restore = main.MockSecbootUnlockEncryptedVolumeUsingKey(func(disk disks.Disk, name string, key []byte) (secboot.UnlockResult, error) { + c.Check(dataActivated, Equals, true, Commentf("ubuntu-data not activated yet")) + encDevPartUUID, err := disk.FindMatchingPartitionUUID(name + "-enc") + c.Assert(err, IsNil) + c.Assert(encDevPartUUID, Equals, "ubuntu-save-enc-partuuid") + c.Assert(key, DeepEquals, []byte("foo")) + return happyUnlocked("ubuntu-save", secboot.UnlockedWithKey), nil + }) + defer restore() + + measureEpochCalls := 0 + measureModelCalls := 0 + restore = main.MockSecbootMeasureSnapSystemEpochWhenPossible(func() error { + measureEpochCalls++ + return nil + }) + defer restore() + + var measuredModel *asserts.Model + restore = main.MockSecbootMeasureSnapModelWhenPossible(func(findModel func() (*asserts.Model, error)) error { + measureModelCalls++ + var err error + measuredModel, err = findModel() + if err != nil { + return err + } + return nil + }) + defer restore() + + restore = s.mockSystemdMountSequence(c, []systemdMount{ + ubuntuLabelMount("ubuntu-seed", "recover"), + s.makeSeedSnapSystemdMount(snap.TypeSnapd), + s.makeSeedSnapSystemdMount(snap.TypeKernel), + s.makeSeedSnapSystemdMount(snap.TypeBase), + { + "tmpfs", + boot.InitramfsDataDir, + tmpfsMountOpts, + }, + // no ubuntu-boot + { + "/dev/mapper/ubuntu-data-random", + boot.InitramfsHostUbuntuDataDir, + nil, + }, + { + "/dev/mapper/ubuntu-save-random", + boot.InitramfsUbuntuSaveDir, + nil, + }, + }, nil) + defer restore() + + s.testRecoverModeHappy(c) + + checkDegradedJSON(c, map[string]interface{}{ + "ubuntu-boot": map[string]interface{}{ + "find-state": "not-found", + }, + "ubuntu-data": map[string]interface{}{ + "device": "/dev/mapper/ubuntu-data-random", + "unlock-state": "unlocked", + "find-state": "found", + "mount-state": "mounted", + "unlock-key": "recovery", + "mount-location": boot.InitramfsHostUbuntuDataDir, + }, + "ubuntu-save": map[string]interface{}{ + "device": "/dev/mapper/ubuntu-save-random", + "unlock-key": "run", + "unlock-state": "unlocked", + "mount-state": "mounted", + "find-state": "found", + "mount-location": boot.InitramfsUbuntuSaveDir, + }, + "error-log": []interface{}{ + "cannot find ubuntu-boot partition on disk defaultEncDevNoBoot", + }, + }) + + c.Check(dataActivated, Equals, true) + c.Check(unlockVolumeWithSealedKeyCalls, Equals, 1) + c.Check(measureEpochCalls, Equals, 1) + c.Check(measureModelCalls, Equals, 1) + c.Check(measuredModel, DeepEquals, s.model) + + c.Assert(filepath.Join(dirs.SnapBootstrapRunDir, "secboot-epoch-measured"), testutil.FilePresent) + c.Assert(filepath.Join(dirs.SnapBootstrapRunDir, fmt.Sprintf("%s-model-measured", s.sysLabel)), testutil.FilePresent) +} + +func (s *initramfsMountsSuite) TestInitramfsMountsRecoverModeEncryptedDegradedDataUnlockFailSaveUnlockFallbackHappy(c *C) { + // test a scenario when unsealing of data fails with both the run key + // and fallback key, but save can be unlocked using the fallback key + + s.mockProcCmdlineContent(c, "snapd_recovery_mode=recover snapd_recovery_system="+s.sysLabel) + + restore := main.MockPartitionUUIDForBootedKernelDisk("") + defer restore() + + // setup a bootloader for setting the bootenv after we are done + bloader := bootloadertest.Mock("mock", c.MkDir()) + bootloader.Force(bloader) + defer bootloader.Force(nil) + + restore = disks.MockMountPointDisksToPartitionMapping( + map[disks.Mountpoint]*disks.MockDiskMapping{ + {Mountpoint: boot.InitramfsUbuntuSeedDir}: defaultEncBootDisk, + {Mountpoint: boot.InitramfsUbuntuBootDir}: defaultEncBootDisk, + { + Mountpoint: boot.InitramfsUbuntuSaveDir, + IsDecryptedDevice: true, + }: defaultEncBootDisk, + }, + ) + defer restore() + + dataActivationAttempts := 0 + saveActivated := false + unlockVolumeWithSealedKeyCalls := 0 + restore = main.MockSecbootUnlockVolumeUsingSealedKeyIfEncrypted(func(disk disks.Disk, name string, sealedEncryptionKeyFile string, opts *secboot.UnlockVolumeUsingSealedKeyOptions) (secboot.UnlockResult, error) { + unlockVolumeWithSealedKeyCalls++ + switch unlockVolumeWithSealedKeyCalls { + + case 1: + // ubuntu data can't be unlocked with run key + c.Assert(name, Equals, "ubuntu-data") + c.Assert(sealedEncryptionKeyFile, Equals, filepath.Join(s.tmpDir, "run/mnt/ubuntu-boot/device/fde/ubuntu-data.sealed-key")) + encDevPartUUID, err := disk.FindMatchingPartitionUUID(name + "-enc") + c.Assert(err, IsNil) + c.Assert(encDevPartUUID, Equals, "ubuntu-data-enc-partuuid") + c.Assert(opts, DeepEquals, &secboot.UnlockVolumeUsingSealedKeyOptions{}) + dataActivationAttempts++ + return foundEncrypted("ubuntu-data"), fmt.Errorf("failed to unlock ubuntu-data with run object") + + case 2: + // nor can it be unlocked with fallback key + c.Assert(name, Equals, "ubuntu-data") + c.Assert(sealedEncryptionKeyFile, Equals, filepath.Join(s.tmpDir, "run/mnt/ubuntu-seed/device/fde/ubuntu-data.recovery.sealed-key")) + encDevPartUUID, err := disk.FindMatchingPartitionUUID(name + "-enc") + c.Assert(err, IsNil) + c.Assert(encDevPartUUID, Equals, "ubuntu-data-enc-partuuid") + c.Assert(opts, DeepEquals, &secboot.UnlockVolumeUsingSealedKeyOptions{ + AllowRecoveryKey: true, + }) + dataActivationAttempts++ + return foundEncrypted("ubuntu-data"), fmt.Errorf("failed to unlock ubuntu-data with fallback object") + + case 3: + // we can however still unlock ubuntu-save (somehow?) + c.Assert(name, Equals, "ubuntu-save") + c.Assert(sealedEncryptionKeyFile, Equals, filepath.Join(s.tmpDir, "run/mnt/ubuntu-seed/device/fde/ubuntu-save.recovery.sealed-key")) + encDevPartUUID, err := disk.FindMatchingPartitionUUID(name + "-enc") + c.Assert(err, IsNil) + c.Assert(encDevPartUUID, Equals, "ubuntu-save-enc-partuuid") + c.Assert(opts, DeepEquals, &secboot.UnlockVolumeUsingSealedKeyOptions{ + AllowRecoveryKey: true, + }) + saveActivated = true + return happyUnlocked("ubuntu-save", secboot.UnlockedWithSealedKey), nil + default: + c.Errorf("unexpected call to UnlockVolumeUsingSealedKeyIfEncrypted (num %d)", unlockVolumeWithSealedKeyCalls) + return secboot.UnlockResult{}, fmt.Errorf("broken test") + } + }) + defer restore() + + s.mockUbuntuSaveKeyAndMarker(c, boot.InitramfsHostWritableDir, "foo", "") + + restore = main.MockSecbootUnlockEncryptedVolumeUsingKey(func(disk disks.Disk, name string, key []byte) (secboot.UnlockResult, error) { + // nothing can call this function in the tested scenario + c.Fatalf("unexpected call") + return secboot.UnlockResult{}, fmt.Errorf("unexpected call") + }) + defer restore() + + measureEpochCalls := 0 + measureModelCalls := 0 + restore = main.MockSecbootMeasureSnapSystemEpochWhenPossible(func() error { + measureEpochCalls++ + return nil + }) + defer restore() + + var measuredModel *asserts.Model + restore = main.MockSecbootMeasureSnapModelWhenPossible(func(findModel func() (*asserts.Model, error)) error { + measureModelCalls++ + var err error + measuredModel, err = findModel() + if err != nil { + return err + } + return nil + }) + defer restore() + + restore = s.mockSystemdMountSequence(c, []systemdMount{ + ubuntuLabelMount("ubuntu-seed", "recover"), + s.makeSeedSnapSystemdMount(snap.TypeSnapd), + s.makeSeedSnapSystemdMount(snap.TypeKernel), + s.makeSeedSnapSystemdMount(snap.TypeBase), + { + "tmpfs", + boot.InitramfsDataDir, + tmpfsMountOpts, + }, + { + "/dev/disk/by-partuuid/ubuntu-boot-partuuid", + boot.InitramfsUbuntuBootDir, + needsFsckDiskMountOpts, + }, + { + "/dev/mapper/ubuntu-save-random", + boot.InitramfsUbuntuSaveDir, + nil, + }, + }, nil) + defer restore() + + // ensure that we check that access to sealed keys were locked + sealedKeysLocked := false + restore = main.MockSecbootLockTPMSealedKeys(func() error { + sealedKeysLocked = true + return nil + }) + defer restore() + + _, err := main.Parser().ParseArgs([]string{"initramfs-mounts"}) + c.Assert(err, IsNil) + + // we always need to lock access to sealed keys + c.Check(sealedKeysLocked, Equals, true) + + modeEnv := filepath.Join(boot.InitramfsWritableDir, "var/lib/snapd/modeenv") + c.Check(modeEnv, testutil.FileEquals, `mode=recover +recovery_system=20191118 +`) + + checkDegradedJSON(c, map[string]interface{}{ + "ubuntu-boot": map[string]interface{}{ + "device": "/dev/disk/by-partuuid/ubuntu-boot-partuuid", + "mount-state": "mounted", + "find-state": "found", + "mount-location": boot.InitramfsUbuntuBootDir, + }, + "ubuntu-data": map[string]interface{}{ + "find-state": "found", + "device": "/dev/disk/by-partuuid/ubuntu-data-enc-partuuid", + "unlock-state": "error-unlocking", + }, + "ubuntu-save": map[string]interface{}{ + "device": "/dev/mapper/ubuntu-save-random", + "unlock-key": "fallback", + "unlock-state": "unlocked", + "mount-state": "mounted", + "find-state": "found", + "mount-location": boot.InitramfsUbuntuSaveDir, + }, + "error-log": []interface{}{ + "cannot unlock encrypted ubuntu-data (device /dev/disk/by-partuuid/ubuntu-data-enc-partuuid) with sealed run key: failed to unlock ubuntu-data with run object", + "cannot unlock encrypted ubuntu-data partition with sealed fallback key: failed to unlock ubuntu-data with fallback object", + }, + }) + + bloader2, err := bootloader.Find("", nil) + c.Assert(err, IsNil) + m, err := bloader2.GetBootVars("snapd_recovery_system", "snapd_recovery_mode") + c.Assert(err, IsNil) + c.Assert(m, DeepEquals, map[string]string{ + "snapd_recovery_system": "20191118", + "snapd_recovery_mode": "run", + }) + + // since we didn't mount data at all, we won't have copied in files from + // there and instead will copy safe defaults to the ephemeral data + c.Assert(filepath.Join(boot.InitramfsRunMntDir, "/data/system-data/var/lib/console-conf/complete"), testutil.FilePresent) + + c.Check(dataActivationAttempts, Equals, 2) + c.Check(saveActivated, Equals, true) + c.Check(unlockVolumeWithSealedKeyCalls, Equals, 3) + c.Check(measureEpochCalls, Equals, 1) + c.Check(measureModelCalls, Equals, 1) + c.Check(measuredModel, DeepEquals, s.model) + + c.Assert(filepath.Join(dirs.SnapBootstrapRunDir, "secboot-epoch-measured"), testutil.FilePresent) + c.Assert(filepath.Join(dirs.SnapBootstrapRunDir, fmt.Sprintf("%s-model-measured", s.sysLabel)), testutil.FilePresent) +} + +func (s *initramfsMountsSuite) TestInitramfsMountsRecoverModeDegradedAbsentDataSaveFallbackHappy(c *C) { + // test a scenario when data cannot be found but unencrypted save can be + // mounted + + s.mockProcCmdlineContent(c, "snapd_recovery_mode=recover snapd_recovery_system="+s.sysLabel) + + restore := main.MockPartitionUUIDForBootedKernelDisk("") + defer restore() + + // setup a bootloader for setting the bootenv after we are done + bloader := bootloadertest.Mock("mock", c.MkDir()) + bootloader.Force(bloader) + defer bootloader.Force(nil) + + // no ubuntu-data on the disk at all + mockDiskNoData := &disks.MockDiskMapping{ + FilesystemLabelToPartUUID: map[string]string{ + "ubuntu-boot": "ubuntu-boot-partuuid", + "ubuntu-seed": "ubuntu-seed-partuuid", + "ubuntu-save": "ubuntu-save-partuuid", + }, + DiskHasPartitions: true, + DevNum: "noDataUnenc", + } + + restore = disks.MockMountPointDisksToPartitionMapping( + map[disks.Mountpoint]*disks.MockDiskMapping{ + {Mountpoint: boot.InitramfsUbuntuSeedDir}: mockDiskNoData, + {Mountpoint: boot.InitramfsUbuntuBootDir}: mockDiskNoData, + {Mountpoint: boot.InitramfsUbuntuSaveDir}: mockDiskNoData, + }, + ) + defer restore() + + dataActivated := false + saveActivated := false + unlockVolumeWithSealedKeyCalls := 0 + restore = main.MockSecbootUnlockVolumeUsingSealedKeyIfEncrypted(func(disk disks.Disk, name string, sealedEncryptionKeyFile string, opts *secboot.UnlockVolumeUsingSealedKeyOptions) (secboot.UnlockResult, error) { + unlockVolumeWithSealedKeyCalls++ + switch unlockVolumeWithSealedKeyCalls { + + case 1: + // ubuntu data can't be found at all + c.Assert(name, Equals, "ubuntu-data") + c.Assert(sealedEncryptionKeyFile, Equals, filepath.Join(s.tmpDir, "run/mnt/ubuntu-boot/device/fde/ubuntu-data.sealed-key")) + _, err := disk.FindMatchingPartitionUUID(name + "-enc") + c.Assert(err, FitsTypeOf, disks.FilesystemLabelNotFoundError{}) + c.Assert(opts, DeepEquals, &secboot.UnlockVolumeUsingSealedKeyOptions{}) + // sanity check that we can't find a normal ubuntu-data either + _, err = disk.FindMatchingPartitionUUID(name) + c.Assert(err, FitsTypeOf, disks.FilesystemLabelNotFoundError{}) + dataActivated = true + // data not found at all + return notFoundPart(), fmt.Errorf("error enumerating to find ubuntu-data") + + case 2: + // we can however still mount unecrypted ubuntu-save + c.Assert(name, Equals, "ubuntu-save") + c.Assert(sealedEncryptionKeyFile, Equals, filepath.Join(s.tmpDir, "run/mnt/ubuntu-seed/device/fde/ubuntu-save.recovery.sealed-key")) + _, err := disk.FindMatchingPartitionUUID(name + "-enc") + c.Assert(err, FitsTypeOf, disks.FilesystemLabelNotFoundError{}) + unencDevPartUUID, err := disk.FindMatchingPartitionUUID(name) + c.Assert(err, IsNil) + c.Assert(unencDevPartUUID, Equals, "ubuntu-save-partuuid") + c.Assert(opts, DeepEquals, &secboot.UnlockVolumeUsingSealedKeyOptions{ + AllowRecoveryKey: true, + }) + saveActivated = true + return foundUnencrypted("ubuntu-save"), nil + default: + c.Errorf("unexpected call to UnlockVolumeUsingSealedKeyIfEncrypted (num %d)", unlockVolumeWithSealedKeyCalls) + return secboot.UnlockResult{}, fmt.Errorf("broken test") + } + }) + defer restore() + + s.mockUbuntuSaveKeyAndMarker(c, boot.InitramfsHostWritableDir, "foo", "") + + restore = main.MockSecbootUnlockEncryptedVolumeUsingKey(func(disk disks.Disk, name string, key []byte) (secboot.UnlockResult, error) { + // nothing can call this function in the tested scenario + c.Fatalf("unexpected call") + return secboot.UnlockResult{}, fmt.Errorf("unexpected call") + }) + defer restore() + + measureEpochCalls := 0 + measureModelCalls := 0 + restore = main.MockSecbootMeasureSnapSystemEpochWhenPossible(func() error { + measureEpochCalls++ + return nil + }) + defer restore() + + var measuredModel *asserts.Model + restore = main.MockSecbootMeasureSnapModelWhenPossible(func(findModel func() (*asserts.Model, error)) error { + measureModelCalls++ + var err error + measuredModel, err = findModel() + if err != nil { + return err + } + return nil + }) + defer restore() + + restore = s.mockSystemdMountSequence(c, []systemdMount{ + ubuntuLabelMount("ubuntu-seed", "recover"), + s.makeSeedSnapSystemdMount(snap.TypeSnapd), + s.makeSeedSnapSystemdMount(snap.TypeKernel), + s.makeSeedSnapSystemdMount(snap.TypeBase), + { + "tmpfs", + boot.InitramfsDataDir, + tmpfsMountOpts, + }, + { + "/dev/disk/by-partuuid/ubuntu-boot-partuuid", + boot.InitramfsUbuntuBootDir, + needsFsckDiskMountOpts, + }, + { + "/dev/disk/by-partuuid/ubuntu-save-partuuid", + boot.InitramfsUbuntuSaveDir, + nil, + }, + }, nil) + defer restore() + + // ensure that we check that access to sealed keys were locked + sealedKeysLocked := false + restore = main.MockSecbootLockTPMSealedKeys(func() error { + sealedKeysLocked = true + return nil + }) + defer restore() + + _, err := main.Parser().ParseArgs([]string{"initramfs-mounts"}) + c.Assert(err, IsNil) + + // we always need to lock access to sealed keys + c.Check(sealedKeysLocked, Equals, true) + + modeEnv := filepath.Join(boot.InitramfsWritableDir, "var/lib/snapd/modeenv") + c.Check(modeEnv, testutil.FileEquals, `mode=recover +recovery_system=20191118 +`) + + checkDegradedJSON(c, map[string]interface{}{ + "ubuntu-boot": map[string]interface{}{ + "device": "/dev/disk/by-partuuid/ubuntu-boot-partuuid", + "mount-state": "mounted", + "find-state": "found", + "mount-location": boot.InitramfsUbuntuBootDir, + }, + "ubuntu-data": map[string]interface{}{ + "find-state": "not-found", + }, + "ubuntu-save": map[string]interface{}{ + "device": "/dev/disk/by-partuuid/ubuntu-save-partuuid", + "mount-state": "mounted", + "find-state": "found", + "mount-location": boot.InitramfsUbuntuSaveDir, + }, + "error-log": []interface{}{ + "cannot locate ubuntu-data partition for mounting host data: error enumerating to find ubuntu-data", + }, + }) + + bloader2, err := bootloader.Find("", nil) + c.Assert(err, IsNil) + m, err := bloader2.GetBootVars("snapd_recovery_system", "snapd_recovery_mode") + c.Assert(err, IsNil) + c.Assert(m, DeepEquals, map[string]string{ + "snapd_recovery_system": "20191118", + "snapd_recovery_mode": "run", + }) + + // since we didn't mount data at all, we won't have copied in files from + // there and instead will copy safe defaults to the ephemeral data + c.Assert(filepath.Join(boot.InitramfsRunMntDir, "/data/system-data/var/lib/console-conf/complete"), testutil.FilePresent) + + c.Check(dataActivated, Equals, true) + c.Check(saveActivated, Equals, true) + c.Check(unlockVolumeWithSealedKeyCalls, Equals, 2) + c.Check(measureEpochCalls, Equals, 1) + c.Check(measureModelCalls, Equals, 1) + c.Check(measuredModel, DeepEquals, s.model) + + c.Assert(filepath.Join(dirs.SnapBootstrapRunDir, "secboot-epoch-measured"), testutil.FilePresent) + c.Assert(filepath.Join(dirs.SnapBootstrapRunDir, fmt.Sprintf("%s-model-measured", s.sysLabel)), testutil.FilePresent) +} + +func (s *initramfsMountsSuite) TestInitramfsMountsRecoverModeDegradedUnencryptedDataSaveEncryptedUnhappy(c *C) { + // test a scenario when data is unencrypted but save is encrypted + s.mockProcCmdlineContent(c, "snapd_recovery_mode=recover snapd_recovery_system="+s.sysLabel) + + restore := main.MockPartitionUUIDForBootedKernelDisk("") + defer restore() + + // setup a bootloader for setting the bootenv after we are done + bloader := bootloadertest.Mock("mock", c.MkDir()) + bootloader.Force(bloader) + defer bootloader.Force(nil) + + // no ubuntu-data on the disk at all + mockDiskDataUnencSaveEnc := &disks.MockDiskMapping{ + FilesystemLabelToPartUUID: map[string]string{ + "ubuntu-boot": "ubuntu-boot-partuuid", + "ubuntu-seed": "ubuntu-seed-partuuid", + // ubuntu-data is unencrypted but ubuntu-save is encrypted + "ubuntu-data": "ubuntu-data-partuuid", + "ubuntu-save-enc": "ubuntu-save-enc-partuuid", + }, + DiskHasPartitions: true, + DevNum: "dataUnencSaveEnc", + } + + restore = disks.MockMountPointDisksToPartitionMapping( + map[disks.Mountpoint]*disks.MockDiskMapping{ + {Mountpoint: boot.InitramfsUbuntuSeedDir}: mockDiskDataUnencSaveEnc, + {Mountpoint: boot.InitramfsUbuntuBootDir}: mockDiskDataUnencSaveEnc, + {Mountpoint: boot.InitramfsHostUbuntuDataDir}: mockDiskDataUnencSaveEnc, + // we don't include the mountpoint for ubuntu-save, since it should + // never be mounted - we fail as soon as we find the encrypted save + // and unlock it, but before we mount it + }, + ) + defer restore() + + dataActivated := false + saveActivated := false + unlockVolumeWithSealedKeyCalls := 0 + restore = main.MockSecbootUnlockVolumeUsingSealedKeyIfEncrypted(func(disk disks.Disk, name string, sealedEncryptionKeyFile string, opts *secboot.UnlockVolumeUsingSealedKeyOptions) (secboot.UnlockResult, error) { + unlockVolumeWithSealedKeyCalls++ + switch unlockVolumeWithSealedKeyCalls { + + case 1: + // ubuntu data is a plain old unencrypted partition + c.Assert(name, Equals, "ubuntu-data") + c.Assert(sealedEncryptionKeyFile, Equals, filepath.Join(s.tmpDir, "run/mnt/ubuntu-boot/device/fde/ubuntu-data.sealed-key")) + _, err := disk.FindMatchingPartitionUUID(name + "-enc") + c.Assert(err, FitsTypeOf, disks.FilesystemLabelNotFoundError{}) + c.Assert(opts, DeepEquals, &secboot.UnlockVolumeUsingSealedKeyOptions{}) + // sanity check that we can't find a normal ubuntu-data either + partUUID, err := disk.FindMatchingPartitionUUID(name) + c.Assert(err, IsNil) + c.Assert(partUUID, Equals, "ubuntu-data-partuuid") + dataActivated = true + + return foundUnencrypted("ubuntu-data"), nil + + case 2: + // we can however still find/unlock ubuntu-save with the recovery key + c.Assert(name, Equals, "ubuntu-save") + c.Assert(sealedEncryptionKeyFile, Equals, filepath.Join(s.tmpDir, "run/mnt/ubuntu-seed/device/fde/ubuntu-save.recovery.sealed-key")) + _, err := disk.FindMatchingPartitionUUID(name) + c.Assert(err, FitsTypeOf, disks.FilesystemLabelNotFoundError{}) + encDevPartUUID, err := disk.FindMatchingPartitionUUID(name + "-enc") + c.Assert(err, IsNil) + c.Assert(encDevPartUUID, Equals, "ubuntu-save-enc-partuuid") + c.Assert(opts, DeepEquals, &secboot.UnlockVolumeUsingSealedKeyOptions{ + AllowRecoveryKey: true, + }) + saveActivated = true + return happyUnlocked("ubuntu-save", secboot.UnlockedWithRecoveryKey), nil + default: + c.Errorf("unexpected call to UnlockVolumeUsingSealedKeyIfEncrypted (num %d)", unlockVolumeWithSealedKeyCalls) + return secboot.UnlockResult{}, fmt.Errorf("broken test") + } + }) + defer restore() + + s.mockUbuntuSaveKeyAndMarker(c, boot.InitramfsHostWritableDir, "foo", "") + + restore = main.MockSecbootUnlockEncryptedVolumeUsingKey(func(disk disks.Disk, name string, key []byte) (secboot.UnlockResult, error) { + // nothing can call this function in the tested scenario + c.Fatalf("unexpected call") + return secboot.UnlockResult{}, fmt.Errorf("unexpected call") + }) + defer restore() + + measureEpochCalls := 0 + measureModelCalls := 0 + restore = main.MockSecbootMeasureSnapSystemEpochWhenPossible(func() error { + measureEpochCalls++ + return nil + }) + defer restore() + + var measuredModel *asserts.Model + restore = main.MockSecbootMeasureSnapModelWhenPossible(func(findModel func() (*asserts.Model, error)) error { + measureModelCalls++ + var err error + measuredModel, err = findModel() + if err != nil { + return err + } + return nil + }) + defer restore() + + restore = s.mockSystemdMountSequence(c, []systemdMount{ + ubuntuLabelMount("ubuntu-seed", "recover"), + s.makeSeedSnapSystemdMount(snap.TypeSnapd), + s.makeSeedSnapSystemdMount(snap.TypeKernel), + s.makeSeedSnapSystemdMount(snap.TypeBase), + { + "tmpfs", + boot.InitramfsDataDir, + tmpfsMountOpts, + }, + { + "/dev/disk/by-partuuid/ubuntu-boot-partuuid", + boot.InitramfsUbuntuBootDir, + needsFsckDiskMountOpts, + }, + { + "/dev/disk/by-partuuid/ubuntu-data-partuuid", + boot.InitramfsHostUbuntuDataDir, + nil, + }, + }, nil) + defer restore() + + // ensure that we check that access to sealed keys were locked + sealedKeysLocked := false + restore = main.MockSecbootLockTPMSealedKeys(func() error { + sealedKeysLocked = true + return nil + }) + defer restore() + + _, err := main.Parser().ParseArgs([]string{"initramfs-mounts"}) + c.Assert(err, ErrorMatches, `inconsistent encryption status for disk dataUnencSaveEnc: ubuntu-data \(device /dev/disk/by-partuuid/ubuntu-data-partuuid\) was found unencrypted but ubuntu-save \(device /dev/mapper/ubuntu-save-random\) was found to be encrypted`) + + // we always need to lock access to sealed keys + c.Check(sealedKeysLocked, Equals, true) + + c.Check(dataActivated, Equals, true) + c.Check(saveActivated, Equals, true) + c.Check(unlockVolumeWithSealedKeyCalls, Equals, 2) + c.Check(measureEpochCalls, Equals, 1) + c.Check(measureModelCalls, Equals, 1) + c.Check(measuredModel, DeepEquals, s.model) + + c.Assert(filepath.Join(dirs.SnapBootstrapRunDir, "secboot-epoch-measured"), testutil.FilePresent) + c.Assert(filepath.Join(dirs.SnapBootstrapRunDir, fmt.Sprintf("%s-model-measured", s.sysLabel)), testutil.FilePresent) +} + +func (s *initramfsMountsSuite) TestInitramfsMountsRecoverModeEncryptedDegradedAbsentDataSaveUnlockFallbackHappy(c *C) { + // test a scenario when data cannot be found but save can be + // unlocked using the fallback key + + s.mockProcCmdlineContent(c, "snapd_recovery_mode=recover snapd_recovery_system="+s.sysLabel) + + restore := main.MockPartitionUUIDForBootedKernelDisk("") + defer restore() + + // setup a bootloader for setting the bootenv after we are done + bloader := bootloadertest.Mock("mock", c.MkDir()) + bootloader.Force(bloader) + defer bootloader.Force(nil) + + // no ubuntu-data on the disk at all + mockDiskNoData := &disks.MockDiskMapping{ + FilesystemLabelToPartUUID: map[string]string{ + "ubuntu-boot": "ubuntu-boot-partuuid", + "ubuntu-seed": "ubuntu-seed-partuuid", + "ubuntu-save-enc": "ubuntu-save-enc-partuuid", + }, + DiskHasPartitions: true, + DevNum: "defaultEncDev", + } + + restore = disks.MockMountPointDisksToPartitionMapping( + map[disks.Mountpoint]*disks.MockDiskMapping{ + {Mountpoint: boot.InitramfsUbuntuSeedDir}: mockDiskNoData, + {Mountpoint: boot.InitramfsUbuntuBootDir}: mockDiskNoData, + { + Mountpoint: boot.InitramfsUbuntuSaveDir, + IsDecryptedDevice: true, + }: mockDiskNoData, + }, + ) + defer restore() + + dataActivated := false + saveActivated := false + unlockVolumeWithSealedKeyCalls := 0 + restore = main.MockSecbootUnlockVolumeUsingSealedKeyIfEncrypted(func(disk disks.Disk, name string, sealedEncryptionKeyFile string, opts *secboot.UnlockVolumeUsingSealedKeyOptions) (secboot.UnlockResult, error) { + unlockVolumeWithSealedKeyCalls++ + switch unlockVolumeWithSealedKeyCalls { + + case 1: + // ubuntu data can't be found at all + c.Assert(name, Equals, "ubuntu-data") + c.Assert(sealedEncryptionKeyFile, Equals, filepath.Join(s.tmpDir, "run/mnt/ubuntu-boot/device/fde/ubuntu-data.sealed-key")) + _, err := disk.FindMatchingPartitionUUID(name + "-enc") + c.Assert(err, FitsTypeOf, disks.FilesystemLabelNotFoundError{}) + c.Assert(opts, DeepEquals, &secboot.UnlockVolumeUsingSealedKeyOptions{}) + dataActivated = true + // data not found at all + return notFoundPart(), fmt.Errorf("error enumerating to find ubuntu-data") + + case 2: + // we can however still unlock ubuntu-save with the fallback key + c.Assert(name, Equals, "ubuntu-save") + c.Assert(sealedEncryptionKeyFile, Equals, filepath.Join(s.tmpDir, "run/mnt/ubuntu-seed/device/fde/ubuntu-save.recovery.sealed-key")) + encDevPartUUID, err := disk.FindMatchingPartitionUUID(name + "-enc") + c.Assert(err, IsNil) + c.Assert(encDevPartUUID, Equals, "ubuntu-save-enc-partuuid") + c.Assert(opts, DeepEquals, &secboot.UnlockVolumeUsingSealedKeyOptions{ + AllowRecoveryKey: true, + }) + saveActivated = true + return happyUnlocked("ubuntu-save", secboot.UnlockedWithSealedKey), nil + default: + c.Errorf("unexpected call to UnlockVolumeUsingSealedKeyIfEncrypted (num %d)", unlockVolumeWithSealedKeyCalls) + return secboot.UnlockResult{}, fmt.Errorf("broken test") + } + }) + defer restore() + + s.mockUbuntuSaveKeyAndMarker(c, boot.InitramfsHostWritableDir, "foo", "") + + restore = main.MockSecbootUnlockEncryptedVolumeUsingKey(func(disk disks.Disk, name string, key []byte) (secboot.UnlockResult, error) { + // nothing can call this function in the tested scenario + c.Fatalf("unexpected call") + return secboot.UnlockResult{}, fmt.Errorf("unexpected call") + }) + defer restore() + + measureEpochCalls := 0 + measureModelCalls := 0 + restore = main.MockSecbootMeasureSnapSystemEpochWhenPossible(func() error { + measureEpochCalls++ + return nil + }) + defer restore() + + var measuredModel *asserts.Model + restore = main.MockSecbootMeasureSnapModelWhenPossible(func(findModel func() (*asserts.Model, error)) error { + measureModelCalls++ + var err error + measuredModel, err = findModel() + if err != nil { + return err + } + return nil + }) + defer restore() + + restore = s.mockSystemdMountSequence(c, []systemdMount{ + ubuntuLabelMount("ubuntu-seed", "recover"), + s.makeSeedSnapSystemdMount(snap.TypeSnapd), + s.makeSeedSnapSystemdMount(snap.TypeKernel), + s.makeSeedSnapSystemdMount(snap.TypeBase), + { + "tmpfs", + boot.InitramfsDataDir, + tmpfsMountOpts, + }, + { + "/dev/disk/by-partuuid/ubuntu-boot-partuuid", + boot.InitramfsUbuntuBootDir, + needsFsckDiskMountOpts, + }, + { + "/dev/mapper/ubuntu-save-random", + boot.InitramfsUbuntuSaveDir, + nil, + }, + }, nil) + defer restore() + + // ensure that we check that access to sealed keys were locked + sealedKeysLocked := false + restore = main.MockSecbootLockTPMSealedKeys(func() error { + sealedKeysLocked = true + return nil + }) + defer restore() + + _, err := main.Parser().ParseArgs([]string{"initramfs-mounts"}) + c.Assert(err, IsNil) + + // we always need to lock access to sealed keys + c.Check(sealedKeysLocked, Equals, true) + + modeEnv := filepath.Join(boot.InitramfsWritableDir, "var/lib/snapd/modeenv") + c.Check(modeEnv, testutil.FileEquals, `mode=recover +recovery_system=20191118 +`) + + checkDegradedJSON(c, map[string]interface{}{ + "ubuntu-boot": map[string]interface{}{ + "device": "/dev/disk/by-partuuid/ubuntu-boot-partuuid", + "mount-state": "mounted", + "find-state": "found", + "mount-location": boot.InitramfsUbuntuBootDir, + }, + "ubuntu-data": map[string]interface{}{ + "find-state": "not-found", + }, + "ubuntu-save": map[string]interface{}{ + "device": "/dev/mapper/ubuntu-save-random", + "unlock-key": "fallback", + "unlock-state": "unlocked", + "mount-state": "mounted", + "find-state": "found", + "mount-location": boot.InitramfsUbuntuSaveDir, + }, + "error-log": []interface{}{ + "cannot locate ubuntu-data partition for mounting host data: error enumerating to find ubuntu-data", + }, + }) + + bloader2, err := bootloader.Find("", nil) + c.Assert(err, IsNil) + m, err := bloader2.GetBootVars("snapd_recovery_system", "snapd_recovery_mode") + c.Assert(err, IsNil) + c.Assert(m, DeepEquals, map[string]string{ + "snapd_recovery_system": "20191118", + "snapd_recovery_mode": "run", + }) + + // since we didn't mount data at all, we won't have copied in files from + // there and instead will copy safe defaults to the ephemeral data + c.Assert(filepath.Join(boot.InitramfsRunMntDir, "/data/system-data/var/lib/console-conf/complete"), testutil.FilePresent) + + c.Check(dataActivated, Equals, true) + c.Check(saveActivated, Equals, true) + c.Check(unlockVolumeWithSealedKeyCalls, Equals, 2) + c.Check(measureEpochCalls, Equals, 1) + c.Check(measureModelCalls, Equals, 1) + c.Check(measuredModel, DeepEquals, s.model) + + c.Assert(filepath.Join(dirs.SnapBootstrapRunDir, "secboot-epoch-measured"), testutil.FilePresent) + c.Assert(filepath.Join(dirs.SnapBootstrapRunDir, fmt.Sprintf("%s-model-measured", s.sysLabel)), testutil.FilePresent) +} + +func (s *initramfsMountsSuite) TestInitramfsMountsRecoverModeEncryptedDegradedDataUnlockFailSaveUnlockFailHappy(c *C) { + // test a scenario when unlocking data with both run and fallback keys + // fails, followed by a failure to unlock save with the fallback key + + s.mockProcCmdlineContent(c, "snapd_recovery_mode=recover snapd_recovery_system="+s.sysLabel) + + restore := main.MockPartitionUUIDForBootedKernelDisk("") + defer restore() + + // setup a bootloader for setting the bootenv after we are done + bloader := bootloadertest.Mock("mock", c.MkDir()) + bootloader.Force(bloader) + defer bootloader.Force(nil) + + restore = disks.MockMountPointDisksToPartitionMapping( + // no ubuntu-data mountpoint is mocked, but there is an + // ubuntu-data-enc partition in the disk we find + map[disks.Mountpoint]*disks.MockDiskMapping{ + {Mountpoint: boot.InitramfsUbuntuSeedDir}: defaultEncBootDisk, + {Mountpoint: boot.InitramfsUbuntuBootDir}: defaultEncBootDisk, + { + Mountpoint: boot.InitramfsUbuntuSaveDir, + IsDecryptedDevice: true, + }: defaultEncBootDisk, + }, + ) + defer restore() + + dataActivationAttempts := 0 + saveUnsealActivationAttempted := false + unlockVolumeWithSealedKeyCalls := 0 + restore = main.MockSecbootUnlockVolumeUsingSealedKeyIfEncrypted(func(disk disks.Disk, name string, sealedEncryptionKeyFile string, opts *secboot.UnlockVolumeUsingSealedKeyOptions) (secboot.UnlockResult, error) { + unlockVolumeWithSealedKeyCalls++ + switch unlockVolumeWithSealedKeyCalls { + + case 1: + // ubuntu data can't be unlocked with run key + c.Assert(name, Equals, "ubuntu-data") + c.Assert(sealedEncryptionKeyFile, Equals, filepath.Join(s.tmpDir, "run/mnt/ubuntu-boot/device/fde/ubuntu-data.sealed-key")) + encDevPartUUID, err := disk.FindMatchingPartitionUUID(name + "-enc") + c.Assert(err, IsNil) + c.Assert(encDevPartUUID, Equals, "ubuntu-data-enc-partuuid") + c.Assert(opts, DeepEquals, &secboot.UnlockVolumeUsingSealedKeyOptions{}) + dataActivationAttempts++ + return foundEncrypted("ubuntu-data"), fmt.Errorf("failed to unlock ubuntu-data with run object") + + case 2: + // nor can it be unlocked with fallback key + c.Assert(name, Equals, "ubuntu-data") + c.Assert(sealedEncryptionKeyFile, Equals, filepath.Join(s.tmpDir, "run/mnt/ubuntu-seed/device/fde/ubuntu-data.recovery.sealed-key")) + encDevPartUUID, err := disk.FindMatchingPartitionUUID(name + "-enc") + c.Assert(err, IsNil) + c.Assert(encDevPartUUID, Equals, "ubuntu-data-enc-partuuid") + c.Assert(opts, DeepEquals, &secboot.UnlockVolumeUsingSealedKeyOptions{ + AllowRecoveryKey: true, + }) + dataActivationAttempts++ + return foundEncrypted("ubuntu-data"), fmt.Errorf("failed to unlock ubuntu-data with fallback object") + + case 3: + // we also fail to unlock save + + // no attempts to activate ubuntu-save yet + c.Assert(name, Equals, "ubuntu-save") + c.Assert(sealedEncryptionKeyFile, Equals, filepath.Join(s.tmpDir, "run/mnt/ubuntu-seed/device/fde/ubuntu-save.recovery.sealed-key")) + encDevPartUUID, err := disk.FindMatchingPartitionUUID(name + "-enc") + c.Assert(err, IsNil) + c.Assert(encDevPartUUID, Equals, "ubuntu-save-enc-partuuid") + c.Assert(opts, DeepEquals, &secboot.UnlockVolumeUsingSealedKeyOptions{ + AllowRecoveryKey: true, + }) + saveUnsealActivationAttempted = true + return foundEncrypted("ubuntu-save"), fmt.Errorf("failed to unlock ubuntu-save with fallback object") + + default: + c.Errorf("unexpected call to UnlockVolumeUsingSealedKeyIfEncrypted (num %d)", unlockVolumeWithSealedKeyCalls) + return secboot.UnlockResult{}, fmt.Errorf("broken test") + } + }) + defer restore() + + s.mockUbuntuSaveKeyAndMarker(c, boot.InitramfsHostWritableDir, "foo", "") + + restore = main.MockSecbootUnlockEncryptedVolumeUsingKey(func(disk disks.Disk, name string, key []byte) (secboot.UnlockResult, error) { + // nothing can call this function in the tested scenario + c.Fatalf("unexpected call") + return secboot.UnlockResult{}, fmt.Errorf("unexpected call") + }) + defer restore() + + measureEpochCalls := 0 + measureModelCalls := 0 + restore = main.MockSecbootMeasureSnapSystemEpochWhenPossible(func() error { + measureEpochCalls++ + return nil + }) + defer restore() + + var measuredModel *asserts.Model + restore = main.MockSecbootMeasureSnapModelWhenPossible(func(findModel func() (*asserts.Model, error)) error { + measureModelCalls++ + var err error + measuredModel, err = findModel() + if err != nil { + return err + } + return nil + }) + defer restore() + + restore = s.mockSystemdMountSequence(c, []systemdMount{ + ubuntuLabelMount("ubuntu-seed", "recover"), + s.makeSeedSnapSystemdMount(snap.TypeSnapd), + s.makeSeedSnapSystemdMount(snap.TypeKernel), + s.makeSeedSnapSystemdMount(snap.TypeBase), + { + "tmpfs", + boot.InitramfsDataDir, + tmpfsMountOpts, + }, + { + "/dev/disk/by-partuuid/ubuntu-boot-partuuid", + boot.InitramfsUbuntuBootDir, + needsFsckDiskMountOpts, + }, + }, nil) + defer restore() + + // ensure that we check that access to sealed keys were locked + sealedKeysLocked := false + restore = main.MockSecbootLockTPMSealedKeys(func() error { + sealedKeysLocked = true + return nil + }) + defer restore() + + _, err := main.Parser().ParseArgs([]string{"initramfs-mounts"}) + c.Assert(err, IsNil) + + // we always need to lock access to sealed keys + c.Check(sealedKeysLocked, Equals, true) + + modeEnv := filepath.Join(boot.InitramfsRunMntDir, "data/system-data/var/lib/snapd/modeenv") + c.Check(modeEnv, testutil.FileEquals, `mode=recover +recovery_system=20191118 +`) + + checkDegradedJSON(c, map[string]interface{}{ + "ubuntu-boot": map[string]interface{}{ + "device": "/dev/disk/by-partuuid/ubuntu-boot-partuuid", + "mount-state": "mounted", + "find-state": "found", + "mount-location": boot.InitramfsUbuntuBootDir, + }, + "ubuntu-data": map[string]interface{}{ + "find-state": "found", + "device": "/dev/disk/by-partuuid/ubuntu-data-enc-partuuid", + "unlock-state": "error-unlocking", + }, + "ubuntu-save": map[string]interface{}{ + "find-state": "found", + "device": "/dev/disk/by-partuuid/ubuntu-save-enc-partuuid", + "unlock-state": "error-unlocking", + }, + "error-log": []interface{}{ + "cannot unlock encrypted ubuntu-data (device /dev/disk/by-partuuid/ubuntu-data-enc-partuuid) with sealed run key: failed to unlock ubuntu-data with run object", + "cannot unlock encrypted ubuntu-data partition with sealed fallback key: failed to unlock ubuntu-data with fallback object", + "cannot unlock encrypted ubuntu-save partition with sealed fallback key: failed to unlock ubuntu-save with fallback object", + }, + }) + + bloader2, err := bootloader.Find("", nil) + c.Assert(err, IsNil) + m, err := bloader2.GetBootVars("snapd_recovery_system", "snapd_recovery_mode") + c.Assert(err, IsNil) + c.Assert(m, DeepEquals, map[string]string{ + "snapd_recovery_system": "20191118", + "snapd_recovery_mode": "run", + }) + + // since we didn't mount data at all, we won't have copied in files from + // there and instead will copy safe defaults to the ephemeral data + c.Assert(filepath.Join(boot.InitramfsRunMntDir, "/data/system-data/var/lib/console-conf/complete"), testutil.FilePresent) + + c.Check(dataActivationAttempts, Equals, 2) + c.Check(saveUnsealActivationAttempted, Equals, true) + c.Check(unlockVolumeWithSealedKeyCalls, Equals, 3) + c.Check(measureEpochCalls, Equals, 1) + c.Check(measureModelCalls, Equals, 1) + c.Check(measuredModel, DeepEquals, s.model) + + c.Assert(filepath.Join(dirs.SnapBootstrapRunDir, "secboot-epoch-measured"), testutil.FilePresent) + c.Assert(filepath.Join(dirs.SnapBootstrapRunDir, fmt.Sprintf("%s-model-measured", s.sysLabel)), testutil.FilePresent) +} + +func (s *initramfsMountsSuite) TestInitramfsMountsRecoverModeEncryptedMismatchedMarker(c *C) { + s.mockProcCmdlineContent(c, "snapd_recovery_mode=recover snapd_recovery_system="+s.sysLabel) + + restore := main.MockPartitionUUIDForBootedKernelDisk("") + defer restore() + + // setup a bootloader for setting the bootenv after we are done + bloader := bootloadertest.Mock("mock", c.MkDir()) + bootloader.Force(bloader) + defer bootloader.Force(nil) + + restore = disks.MockMountPointDisksToPartitionMapping( + map[disks.Mountpoint]*disks.MockDiskMapping{ + {Mountpoint: boot.InitramfsUbuntuSeedDir}: defaultEncBootDisk, + {Mountpoint: boot.InitramfsUbuntuBootDir}: defaultEncBootDisk, + { + Mountpoint: boot.InitramfsHostUbuntuDataDir, + IsDecryptedDevice: true, + }: defaultEncBootDisk, + { + Mountpoint: boot.InitramfsUbuntuSaveDir, + IsDecryptedDevice: true, + }: defaultEncBootDisk, + }, + ) + defer restore() + + dataActivated := false + restore = main.MockSecbootUnlockVolumeUsingSealedKeyIfEncrypted(func(disk disks.Disk, name string, sealedEncryptionKeyFile string, opts *secboot.UnlockVolumeUsingSealedKeyOptions) (secboot.UnlockResult, error) { + c.Assert(name, Equals, "ubuntu-data") + c.Assert(sealedEncryptionKeyFile, Equals, filepath.Join(s.tmpDir, "run/mnt/ubuntu-boot/device/fde/ubuntu-data.sealed-key")) + + encDevPartUUID, err := disk.FindMatchingPartitionUUID(name + "-enc") + c.Assert(err, IsNil) + c.Assert(encDevPartUUID, Equals, "ubuntu-data-enc-partuuid") + c.Assert(opts, DeepEquals, &secboot.UnlockVolumeUsingSealedKeyOptions{}) + dataActivated = true + return happyUnlocked("ubuntu-data", secboot.UnlockedWithSealedKey), nil + }) + defer restore() + + s.mockUbuntuSaveKeyAndMarker(c, boot.InitramfsHostWritableDir, "foo", "other-marker") + s.mockUbuntuSaveMarker(c, boot.InitramfsUbuntuSaveDir, "marker") + + saveActivated := false + restore = main.MockSecbootUnlockEncryptedVolumeUsingKey(func(disk disks.Disk, name string, key []byte) (secboot.UnlockResult, error) { + c.Check(dataActivated, Equals, true, Commentf("ubuntu-data not activated yet")) + encDevPartUUID, err := disk.FindMatchingPartitionUUID(name + "-enc") + c.Assert(err, IsNil) + c.Assert(encDevPartUUID, Equals, "ubuntu-save-enc-partuuid") + c.Assert(key, DeepEquals, []byte("foo")) + saveActivated = true + return happyUnlocked("ubuntu-save", secboot.UnlockedWithKey), nil + }) + defer restore() + + measureEpochCalls := 0 + measureModelCalls := 0 + restore = main.MockSecbootMeasureSnapSystemEpochWhenPossible(func() error { + measureEpochCalls++ + return nil + }) + defer restore() + + var measuredModel *asserts.Model + restore = main.MockSecbootMeasureSnapModelWhenPossible(func(findModel func() (*asserts.Model, error)) error { + measureModelCalls++ + var err error + measuredModel, err = findModel() + if err != nil { + return err + } + return nil + }) + defer restore() + + restore = s.mockSystemdMountSequence(c, []systemdMount{ + ubuntuLabelMount("ubuntu-seed", "recover"), + s.makeSeedSnapSystemdMount(snap.TypeSnapd), + s.makeSeedSnapSystemdMount(snap.TypeKernel), + s.makeSeedSnapSystemdMount(snap.TypeBase), + { + "tmpfs", + boot.InitramfsDataDir, + tmpfsMountOpts, + }, + { + "/dev/disk/by-partuuid/ubuntu-boot-partuuid", + boot.InitramfsUbuntuBootDir, + needsFsckDiskMountOpts, + }, + { + "/dev/mapper/ubuntu-data-random", + boot.InitramfsHostUbuntuDataDir, + nil, + }, + { + "/dev/mapper/ubuntu-save-random", + boot.InitramfsUbuntuSaveDir, + nil, + }, + }, nil) + defer restore() + + // ensure that we check that access to sealed keys were locked + sealedKeysLocked := false + restore = main.MockSecbootLockTPMSealedKeys(func() error { + sealedKeysLocked = true + return nil + }) + defer restore() + + _, err := main.Parser().ParseArgs([]string{"initramfs-mounts"}) + c.Assert(err, IsNil) + + // we always need to lock access to sealed keys + c.Check(sealedKeysLocked, Equals, true) + + modeEnv := filepath.Join(boot.InitramfsWritableDir, "var/lib/snapd/modeenv") + c.Check(modeEnv, testutil.FileEquals, `mode=recover +recovery_system=20191118 +`) + + checkDegradedJSON(c, map[string]interface{}{ + "ubuntu-boot": map[string]interface{}{ + "device": "/dev/disk/by-partuuid/ubuntu-boot-partuuid", + "mount-state": "mounted", + "find-state": "found", + "mount-location": boot.InitramfsUbuntuBootDir, + }, + "ubuntu-data": map[string]interface{}{ + "device": "/dev/mapper/ubuntu-data-random", + "unlock-state": "unlocked", + "find-state": "found", + "mount-state": "mounted-untrusted", + "unlock-key": "run", + "mount-location": boot.InitramfsHostUbuntuDataDir, + }, + "ubuntu-save": map[string]interface{}{ + "device": "/dev/mapper/ubuntu-save-random", + "unlock-key": "run", + "unlock-state": "unlocked", + "mount-state": "mounted", + "find-state": "found", + "mount-location": boot.InitramfsUbuntuSaveDir, + }, + "error-log": []interface{}{"cannot trust ubuntu-data, ubuntu-save and ubuntu-data are not marked as from the same install"}, + }) + + bloader2, err := bootloader.Find("", nil) + c.Assert(err, IsNil) + m, err := bloader2.GetBootVars("snapd_recovery_system", "snapd_recovery_mode") + c.Assert(err, IsNil) + c.Assert(m, DeepEquals, map[string]string{ + "snapd_recovery_system": "20191118", + "snapd_recovery_mode": "run", + }) + + // since we didn't mount data at all, we won't have copied in files from + // there and instead will copy safe defaults to the ephemeral data + c.Assert(filepath.Join(boot.InitramfsRunMntDir, "/data/system-data/var/lib/console-conf/complete"), testutil.FilePresent) + + c.Check(dataActivated, Equals, true) + c.Check(saveActivated, Equals, true) + c.Check(measureEpochCalls, Equals, 1) + c.Check(measureModelCalls, Equals, 1) + c.Check(measuredModel, DeepEquals, s.model) + + c.Assert(filepath.Join(dirs.SnapBootstrapRunDir, "secboot-epoch-measured"), testutil.FilePresent) + c.Assert(filepath.Join(dirs.SnapBootstrapRunDir, fmt.Sprintf("%s-model-measured", s.sysLabel)), testutil.FilePresent) +} + +func (s *initramfsMountsSuite) TestInitramfsMountsRecoverModeEncryptedAttackerFSAttachedHappy(c *C) { + s.mockProcCmdlineContent(c, "snapd_recovery_mode=recover snapd_recovery_system="+s.sysLabel) + + restore := main.MockPartitionUUIDForBootedKernelDisk("") + defer restore() + + // setup a bootloader for setting the bootenv + bloader := bootloadertest.Mock("mock", c.MkDir()) + bootloader.Force(bloader) + defer bootloader.Force(nil) + + mockDisk := &disks.MockDiskMapping{ + FilesystemLabelToPartUUID: map[string]string{ + "ubuntu-seed": "ubuntu-seed-partuuid", + "ubuntu-boot": "ubuntu-boot-partuuid", + "ubuntu-data-enc": "ubuntu-data-enc-partuuid", + "ubuntu-save-enc": "ubuntu-save-enc-partuuid", + }, + DiskHasPartitions: true, + DevNum: "bootDev", + } + attackerDisk := &disks.MockDiskMapping{ + FilesystemLabelToPartUUID: map[string]string{ + "ubuntu-seed": "ubuntu-seed-attacker-partuuid", + "ubuntu-boot": "ubuntu-boot-attacker-partuuid", + "ubuntu-data-enc": "ubuntu-data-enc-attacker-partuuid", + "ubuntu-save-enc": "ubuntu-save-enc-attacker-partuuid", + }, + DiskHasPartitions: true, + DevNum: "attackerDev", + } + + restore = disks.MockMountPointDisksToPartitionMapping( + map[disks.Mountpoint]*disks.MockDiskMapping{ + {Mountpoint: boot.InitramfsUbuntuSeedDir}: mockDisk, + {Mountpoint: boot.InitramfsUbuntuBootDir}: mockDisk, + { + Mountpoint: boot.InitramfsHostUbuntuDataDir, + IsDecryptedDevice: true, + }: mockDisk, + { + Mountpoint: boot.InitramfsUbuntuSaveDir, + IsDecryptedDevice: true, + }: mockDisk, + // this is the attacker fs on a different disk + {Mountpoint: "somewhere-else"}: attackerDisk, + }, + ) + defer restore() + + activated := false + restore = main.MockSecbootUnlockVolumeUsingSealedKeyIfEncrypted(func(disk disks.Disk, name string, sealedEncryptionKeyFile string, opts *secboot.UnlockVolumeUsingSealedKeyOptions) (secboot.UnlockResult, error) { + c.Assert(name, Equals, "ubuntu-data") + encDevPartUUID, err := disk.FindMatchingPartitionUUID(name + "-enc") + c.Assert(err, IsNil) + c.Assert(encDevPartUUID, Equals, "ubuntu-data-enc-partuuid") + c.Assert(opts, DeepEquals, &secboot.UnlockVolumeUsingSealedKeyOptions{}) + + activated = true + return happyUnlocked("ubuntu-data", secboot.UnlockedWithSealedKey), nil + }) + defer restore() + + s.mockUbuntuSaveKeyAndMarker(c, boot.InitramfsHostWritableDir, "foo", "marker") + s.mockUbuntuSaveMarker(c, boot.InitramfsUbuntuSaveDir, "marker") + + restore = main.MockSecbootUnlockEncryptedVolumeUsingKey(func(disk disks.Disk, name string, key []byte) (secboot.UnlockResult, error) { + encDevPartUUID, err := disk.FindMatchingPartitionUUID(name + "-enc") + c.Assert(err, IsNil) + c.Assert(encDevPartUUID, Equals, "ubuntu-save-enc-partuuid") + c.Assert(key, DeepEquals, []byte("foo")) + return happyUnlocked("ubuntu-save", secboot.UnlockedWithKey), nil + }) + defer restore() + + measureEpochCalls := 0 + measureModelCalls := 0 + restore = main.MockSecbootMeasureSnapSystemEpochWhenPossible(func() error { + measureEpochCalls++ + return nil + }) + defer restore() + + var measuredModel *asserts.Model + restore = main.MockSecbootMeasureSnapModelWhenPossible(func(findModel func() (*asserts.Model, error)) error { + measureModelCalls++ + var err error + measuredModel, err = findModel() + if err != nil { + return err + } + return nil + }) + defer restore() + + restore = s.mockSystemdMountSequence(c, []systemdMount{ + ubuntuLabelMount("ubuntu-seed", "recover"), + s.makeSeedSnapSystemdMount(snap.TypeSnapd), + s.makeSeedSnapSystemdMount(snap.TypeKernel), + s.makeSeedSnapSystemdMount(snap.TypeBase), + { + "tmpfs", + boot.InitramfsDataDir, + tmpfsMountOpts, + }, + { + "/dev/disk/by-partuuid/ubuntu-boot-partuuid", + boot.InitramfsUbuntuBootDir, + needsFsckDiskMountOpts, + }, + { + "/dev/mapper/ubuntu-data-random", + boot.InitramfsHostUbuntuDataDir, + nil, + }, + { + "/dev/mapper/ubuntu-save-random", + boot.InitramfsUbuntuSaveDir, + nil, + }, + }, nil) + defer restore() + + s.testRecoverModeHappy(c) + + c.Check(activated, Equals, true) + c.Check(measureEpochCalls, Equals, 1) + c.Check(measureModelCalls, Equals, 1) + c.Check(measuredModel, DeepEquals, s.model) + + c.Assert(filepath.Join(dirs.SnapBootstrapRunDir, "secboot-epoch-measured"), testutil.FilePresent) + c.Assert(filepath.Join(dirs.SnapBootstrapRunDir, fmt.Sprintf("%s-model-measured", s.sysLabel)), testutil.FilePresent) +} + +func (s *initramfsMountsSuite) testInitramfsMountsInstallRecoverModeMeasure(c *C, mode string) { + s.mockProcCmdlineContent(c, fmt.Sprintf("snapd_recovery_mode=%s snapd_recovery_system=%s", mode, s.sysLabel)) + + modeMnts := []systemdMount{ + ubuntuLabelMount("ubuntu-seed", mode), + s.makeSeedSnapSystemdMount(snap.TypeSnapd), + s.makeSeedSnapSystemdMount(snap.TypeKernel), + s.makeSeedSnapSystemdMount(snap.TypeBase), + { + "tmpfs", + boot.InitramfsDataDir, + tmpfsMountOpts, + }, + } + + mockDiskMapping := map[disks.Mountpoint]*disks.MockDiskMapping{ + {Mountpoint: boot.InitramfsUbuntuSeedDir}: { + FilesystemLabelToPartUUID: map[string]string{ + "ubuntu-seed": "ubuntu-seed-partuuid", + }, + DiskHasPartitions: true, + }, + } + + if mode == "recover" { + // setup a bootloader for setting the bootenv after we are done + bloader := bootloadertest.Mock("mock", c.MkDir()) + bootloader.Force(bloader) + defer bootloader.Force(nil) + + // add the expected mount of ubuntu-data onto the host data dir + modeMnts = append(modeMnts, + systemdMount{ + "/dev/disk/by-partuuid/ubuntu-boot-partuuid", + boot.InitramfsUbuntuBootDir, + needsFsckDiskMountOpts, + }, + systemdMount{ + "/dev/disk/by-partuuid/ubuntu-data-partuuid", + boot.InitramfsHostUbuntuDataDir, + nil, + }, + systemdMount{ + "/dev/disk/by-partuuid/ubuntu-save-partuuid", + boot.InitramfsUbuntuSaveDir, + nil, + }) - // also add the ubuntu-data fs label to the disk referenced by the - // ubuntu-seed partition + // also add the ubuntu-data and ubuntu-save fs labels to the + // disk referenced by the ubuntu-seed partition disk := mockDiskMapping[disks.Mountpoint{Mountpoint: boot.InitramfsUbuntuSeedDir}] + disk.FilesystemLabelToPartUUID["ubuntu-boot"] = "ubuntu-boot-partuuid" disk.FilesystemLabelToPartUUID["ubuntu-data"] = "ubuntu-data-partuuid" + disk.FilesystemLabelToPartUUID["ubuntu-save"] = "ubuntu-save-partuuid" - // and also add the /run/mnt/host/ubuntu-data mountpoint for - // cross-checking after it is mounted + // and also add the /run/mnt/host/ubuntu-{boot,data,save} mountpoints + // for cross-checking after mounting + mockDiskMapping[disks.Mountpoint{Mountpoint: boot.InitramfsUbuntuBootDir}] = disk mockDiskMapping[disks.Mountpoint{Mountpoint: boot.InitramfsHostUbuntuDataDir}] = disk + mockDiskMapping[disks.Mountpoint{Mountpoint: boot.InitramfsUbuntuSaveDir}] = disk } restore := disks.MockMountPointDisksToPartitionMapping(mockDiskMapping) diff -Nru snapd-2.47.1+20.10.1build1/cmd/snap-bootstrap/export_test.go snapd-2.48+21.04/cmd/snap-bootstrap/export_test.go --- snapd-2.47.1+20.10.1build1/cmd/snap-bootstrap/export_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/cmd/snap-bootstrap/export_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -25,6 +25,7 @@ "github.com/snapcore/snapd/asserts" "github.com/snapcore/snapd/osutil/disks" + "github.com/snapcore/snapd/secboot" ) var ( @@ -35,6 +36,18 @@ type SystemdMountOptions = systemdMountOptions +type RecoverDegradedState = recoverDegradedState + +type PartitionState = partitionState + +func (r *RecoverDegradedState) Degraded(isEncrypted bool) bool { + m := recoverModeStateMachine{ + isEncryptedDev: isEncrypted, + degradedState: r, + } + return m.degraded() +} + func MockTimeNow(f func() time.Time) (restore func()) { old := timeNow timeNow = f @@ -77,11 +90,19 @@ } } -func MockSecbootUnlockVolumeIfEncrypted(f func(disk disks.Disk, name string, encryptionKeyDir string, lockKeysOnFinish bool) (string, bool, error)) (restore func()) { - old := secbootUnlockVolumeIfEncrypted - secbootUnlockVolumeIfEncrypted = f +func MockSecbootUnlockVolumeUsingSealedKeyIfEncrypted(f func(disk disks.Disk, name string, sealedEncryptionKeyFile string, opts *secboot.UnlockVolumeUsingSealedKeyOptions) (secboot.UnlockResult, error)) (restore func()) { + old := secbootUnlockVolumeUsingSealedKeyIfEncrypted + secbootUnlockVolumeUsingSealedKeyIfEncrypted = f return func() { - secbootUnlockVolumeIfEncrypted = old + secbootUnlockVolumeUsingSealedKeyIfEncrypted = old + } +} + +func MockSecbootUnlockEncryptedVolumeUsingKey(f func(disk disks.Disk, name string, key []byte) (secboot.UnlockResult, error)) (restore func()) { + old := secbootUnlockEncryptedVolumeUsingKey + secbootUnlockEncryptedVolumeUsingKey = f + return func() { + secbootUnlockEncryptedVolumeUsingKey = old } } @@ -101,6 +122,14 @@ } } +func MockSecbootLockTPMSealedKeys(f func() error) (restore func()) { + old := secbootLockTPMSealedKeys + secbootLockTPMSealedKeys = f + return func() { + secbootLockTPMSealedKeys = old + } +} + func MockPartitionUUIDForBootedKernelDisk(uuid string) (restore func()) { old := bootFindPartitionUUIDForBootedKernelDisk bootFindPartitionUUIDForBootedKernelDisk = func() (string, error) { diff -Nru snapd-2.47.1+20.10.1build1/cmd/snap-bootstrap/initramfs_mounts_state.go snapd-2.48+21.04/cmd/snap-bootstrap/initramfs_mounts_state.go --- snapd-2.47.1+20.10.1build1/cmd/snap-bootstrap/initramfs_mounts_state.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/cmd/snap-bootstrap/initramfs_mounts_state.go 2020-11-19 16:51:02.000000000 +0000 @@ -66,7 +66,7 @@ return nil, fmt.Errorf("internal error: unverified boot model access is for limited run mode use") } - mf, err := os.Open(filepath.Join(boot.InitramfsUbuntuBootDir, "model")) + mf, err := os.Open(filepath.Join(boot.InitramfsUbuntuBootDir, "device/model")) if err != nil { return nil, fmt.Errorf("cannot read model assertion: %v", err) } diff -Nru snapd-2.47.1+20.10.1build1/cmd/snap-confine/mount-support.c snapd-2.48+21.04/cmd/snap-confine/mount-support.c --- snapd-2.47.1+20.10.1build1/cmd/snap-confine/mount-support.c 2020-10-19 18:24:02.000000000 +0000 +++ snapd-2.48+21.04/cmd/snap-confine/mount-support.c 2020-11-19 16:51:02.000000000 +0000 @@ -33,7 +33,6 @@ #include #include #include -#include #include #include diff -Nru snapd-2.47.1+20.10.1build1/cmd/snap-confine/ns-support.c snapd-2.48+21.04/cmd/snap-confine/ns-support.c --- snapd-2.47.1+20.10.1build1/cmd/snap-confine/ns-support.c 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/cmd/snap-confine/ns-support.c 2020-11-19 16:51:02.000000000 +0000 @@ -93,7 +93,7 @@ if (readlinkat(init_mnt_fd, "", init_buf, sizeof init_buf) < 0) { if (errno == ENOENT) { // According to namespaces(7) on a pre 3.8 kernel the namespace - // files are hardlinks, not sylinks. If that happens readlinkat + // files are hardlinks, not symlinks. If that happens readlinkat // fails with ENOENT. As a quick workaround for this special-case // functionality, just bail out and do nothing without raising an // error. diff -Nru snapd-2.47.1+20.10.1build1/cmd/snap-confine/snap-confine.apparmor.in snapd-2.48+21.04/cmd/snap-confine/snap-confine.apparmor.in --- snapd-2.47.1+20.10.1build1/cmd/snap-confine/snap-confine.apparmor.in 2020-10-19 18:24:02.000000000 +0000 +++ snapd-2.48+21.04/cmd/snap-confine/snap-confine.apparmor.in 2020-11-19 16:51:02.000000000 +0000 @@ -71,7 +71,7 @@ # querying udev /etc/udev/udev.conf r, /sys/**/uevent r, - /usr/lib/snapd/snap-device-helper ixr, # drop + @LIBEXECDIR@/snap-device-helper ixr, # drop /{,usr/}lib/udev/snappy-app-dev ixr, # drop /run/udev/** rw, /{,usr/}bin/tr ixr, diff -Nru snapd-2.47.1+20.10.1build1/cmd/snap-confine/snap-confine.c snapd-2.48+21.04/cmd/snap-confine/snap-confine.c --- snapd-2.47.1+20.10.1build1/cmd/snap-confine/snap-confine.c 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/cmd/snap-confine/snap-confine.c 2020-11-19 16:51:02.000000000 +0000 @@ -124,7 +124,7 @@ /** * sc_preserve_and_sanitize_process_state sanitizes process state. * - * The following process state is sanitised: + * The following process state is sanitized: * - the umask is set to 0 * - the current working directory is set to / * diff -Nru snapd-2.47.1+20.10.1build1/cmd/snap-confine/snap-device-helper snapd-2.48+21.04/cmd/snap-confine/snap-device-helper --- snapd-2.47.1+20.10.1build1/cmd/snap-confine/snap-device-helper 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/cmd/snap-confine/snap-device-helper 2020-11-19 16:51:02.000000000 +0000 @@ -19,7 +19,7 @@ [ "$NOSNAP" != "$APPNAME" ] || { echo "malformed appname $APPNAME" >&2; exit 1; } # FIXME: this will break for instances that are called "hook" :( -# Handle hooks first, the the nosnap part looks like this: +# Handle hooks first, the nosnap part looks like this: # - "$snap_hook_$hookname" # - "$snap_$instance_hook_$hookname # we need to make sure we change this to: diff -Nru snapd-2.47.1+20.10.1build1/cmd/snapd-generator/main.c snapd-2.48+21.04/cmd/snapd-generator/main.c --- snapd-2.47.1+20.10.1build1/cmd/snapd-generator/main.c 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/cmd/snapd-generator/main.c 2020-11-19 16:51:02.000000000 +0000 @@ -185,7 +185,7 @@ fprintf(stderr, "cannot open %s: %m\n", fname); return 2; } - fprintf(f, "[Mount]\nType=%s\n", fstype); + fprintf(f, "[Mount]\nType=%s\nOptions=nodev,ro,x-gdu.hide,allow_other\nLazyUnmount=yes\n", fstype); } return 0; diff -Nru snapd-2.47.1+20.10.1build1/cmd/snaplock/runinhibit/inhibit.go snapd-2.48+21.04/cmd/snaplock/runinhibit/inhibit.go --- snapd-2.47.1+20.10.1build1/cmd/snaplock/runinhibit/inhibit.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/cmd/snaplock/runinhibit/inhibit.go 2020-11-19 16:51:02.000000000 +0000 @@ -54,9 +54,12 @@ HintInhibitedForRefresh Hint = "refresh" ) +func hintFile(snapName string) string { + return filepath.Join(InhibitDir, snapName+".lock") +} + func openHintFileLock(snapName string) (*osutil.FileLock, error) { - fname := filepath.Join(InhibitDir, snapName+".lock") - return osutil.NewFileLockWithMode(fname, 0644) + return osutil.NewFileLockWithMode(hintFile(snapName), 0644) } // LockWithHint sets a persistent "snap run" inhibition lock, for the given snap, with a given hint. @@ -133,3 +136,19 @@ } return Hint(string(buf)), nil } + +// RemoveLockFile removes the run inhibition lock for the given snap. +// +// This function should not be used as a substitute of Unlock, as race-free +// ability to inspect the inhibition state relies on flock(2) which requires the +// file to exist in the first place and non-privileged processes cannot create +// it. +// +// The function does not fail if the inhibition lock does not exist. +func RemoveLockFile(snapName string) error { + err := os.Remove(hintFile(snapName)) + if err != nil && !os.IsNotExist(err) { + return err + } + return nil +} diff -Nru snapd-2.47.1+20.10.1build1/cmd/snaplock/runinhibit/inhibit_test.go snapd-2.48+21.04/cmd/snaplock/runinhibit/inhibit_test.go --- snapd-2.47.1+20.10.1build1/cmd/snaplock/runinhibit/inhibit_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/cmd/snaplock/runinhibit/inhibit_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -144,3 +144,13 @@ c.Assert(err, IsNil) c.Check(hint, Equals, runinhibit.HintNotInhibited) } + +func (s *runInhibitSuite) TestRemoveLockFile(c *C) { + c.Assert(runinhibit.LockWithHint("pkg", runinhibit.HintInhibitedForRefresh), IsNil) + c.Check(filepath.Join(runinhibit.InhibitDir, "pkg.lock"), testutil.FilePresent) + + c.Assert(runinhibit.RemoveLockFile("pkg"), IsNil) + c.Check(filepath.Join(runinhibit.InhibitDir, "pkg.lock"), testutil.FileAbsent) + // Removing an absent lock file is not an error. + c.Assert(runinhibit.RemoveLockFile("pkg"), IsNil) +} diff -Nru snapd-2.47.1+20.10.1build1/cmd/snap-update-ns/change.go snapd-2.48+21.04/cmd/snap-update-ns/change.go --- snapd-2.47.1+20.10.1build1/cmd/snap-update-ns/change.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/cmd/snap-update-ns/change.go 2020-11-19 16:51:02.000000000 +0000 @@ -95,10 +95,10 @@ // In case we need to create something, some constants. const ( - mode = 0755 - uid = 0 - gid = 0 + uid = 0 + gid = 0 ) + mode := as.ModeForPath(path) // If the element doesn't exist we can attempt to create it. We will // create the parent directory and then the final element relative to it. diff -Nru snapd-2.47.1+20.10.1build1/cmd/snap-update-ns/system.go snapd-2.48+21.04/cmd/snap-update-ns/system.go --- snapd-2.47.1+20.10.1build1/cmd/snap-update-ns/system.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/cmd/snap-update-ns/system.go 2020-11-19 16:51:02.000000000 +0000 @@ -72,6 +72,19 @@ if snapName := snap.InstanceSnap(instanceName); snapName != instanceName { as.AddUnrestrictedPaths("/snap/" + snapName) } + // Allow snap-update-ns to write to host's /tmp directory. This is + // specifically here to allow two snaps to share X11 sockets that are placed + // in the /tmp/.X11-unix/ directory in the private /tmp directories provided + // by snap-confine. The X11 interface cannot offer a precise permission for + // the slot-side snap, as there is no mechanism to convey this information. + // As such, provide write access to all of /tmp. + as.AddUnrestrictedPaths("/var/lib/snapd/hostfs/tmp") + as.AddModeHint("/var/lib/snapd/hostfs/tmp/snap.*", 0700) + as.AddModeHint("/var/lib/snapd/hostfs/tmp/snap.*/tmp", 1777) + // This is to ensure that unprivileged users can create the socket. This + // permission only matters if the plug-side app constructs its mount + // namespace before the slot-side app is launched. + as.AddModeHint("/var/lib/snapd/hostfs/tmp/snap.*/tmp/.X11-unix", 1777) return as } diff -Nru snapd-2.47.1+20.10.1build1/cmd/snap-update-ns/system_test.go snapd-2.48+21.04/cmd/snap-update-ns/system_test.go --- snapd-2.47.1+20.10.1build1/cmd/snap-update-ns/system_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/cmd/snap-update-ns/system_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -52,12 +52,19 @@ // Non-instances can access /tmp, /var/snap and /snap/$SNAP_NAME upCtx := update.NewSystemProfileUpdateContext("foo", false) as := upCtx.Assumptions() - c.Check(as.UnrestrictedPaths(), DeepEquals, []string{"/tmp", "/var/snap", "/snap/foo"}) + c.Check(as.UnrestrictedPaths(), DeepEquals, []string{"/tmp", "/var/snap", "/snap/foo", "/var/lib/snapd/hostfs/tmp"}) + c.Check(as.ModeForPath("/stuff"), Equals, os.FileMode(0755)) + c.Check(as.ModeForPath("/tmp"), Equals, os.FileMode(0755)) + c.Check(as.ModeForPath("/var/lib/snapd/hostfs/tmp"), Equals, os.FileMode(0755)) + c.Check(as.ModeForPath("/var/lib/snapd/hostfs/tmp/snap.x11-server"), Equals, os.FileMode(0700)) + c.Check(as.ModeForPath("/var/lib/snapd/hostfs/tmp/snap.x11-server/tmp"), Equals, os.FileMode(1777)) + c.Check(as.ModeForPath("/var/lib/snapd/hostfs/tmp/snap.x11-server/foo"), Equals, os.FileMode(0755)) + c.Check(as.ModeForPath("/var/lib/snapd/hostfs/tmp/snap.x11-server/tmp/.X11-unix"), Equals, os.FileMode(1777)) // Instances can, in addition, access /snap/$SNAP_INSTANCE_NAME upCtx = update.NewSystemProfileUpdateContext("foo_instance", false) as = upCtx.Assumptions() - c.Check(as.UnrestrictedPaths(), DeepEquals, []string{"/tmp", "/var/snap", "/snap/foo_instance", "/snap/foo"}) + c.Check(as.UnrestrictedPaths(), DeepEquals, []string{"/tmp", "/var/snap", "/snap/foo_instance", "/snap/foo", "/var/lib/snapd/hostfs/tmp"}) } func (s *systemSuite) TestLoadDesiredProfile(c *C) { diff -Nru snapd-2.47.1+20.10.1build1/cmd/snap-update-ns/trespassing.go snapd-2.48+21.04/cmd/snap-update-ns/trespassing.go --- snapd-2.47.1+20.10.1build1/cmd/snap-update-ns/trespassing.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/cmd/snap-update-ns/trespassing.go 2020-11-19 16:51:02.000000000 +0000 @@ -21,6 +21,7 @@ import ( "fmt" + "os" "path/filepath" "strings" "syscall" @@ -42,6 +43,16 @@ // major:minor number is packed into one uint64 as in syscall.Stat_t.Dev // field. verifiedDevices map[uint64]bool + + // modeHints overrides implicit 0755 mode of directories created while + // ensuring source and target paths exist. + modeHints []ModeHint +} + +// ModeHint provides mode for directories created to satisfy mount changes. +type ModeHint struct { + PathGlob string + Mode os.FileMode } // AddUnrestrictedPaths adds a list of directories where writing is allowed @@ -52,6 +63,36 @@ as.unrestrictedPaths = append(as.unrestrictedPaths, paths...) } +// AddModeHint adds a path glob and mode used when creating path elements. +func (as *Assumptions) AddModeHint(pathGlob string, mode os.FileMode) { + as.modeHints = append(as.modeHints, ModeHint{PathGlob: pathGlob, Mode: mode}) +} + +// ModeForPath returns the mode for creating a directory at a given path. +// +// The default mode is 0755 but AddModeHint calls can influence the mode at a +// specific path. When matching path elements, "*" does not match the directory +// separator. In effect it can only be used as a wildcard for a specific +// directory name. This constraint makes hints easier to model in practice. +// +// When multiple hints match the given path, ModeForPath panics. +func (as *Assumptions) ModeForPath(path string) os.FileMode { + mode := os.FileMode(0755) + var foundHint *ModeHint + for _, hint := range as.modeHints { + if ok, _ := filepath.Match(hint.PathGlob, path); ok { + if foundHint == nil { + mode = hint.Mode + foundHint = &hint + } else { + panic(fmt.Errorf("cannot find unique mode for path %q: %q and %q both provide hints", + path, foundHint.PathGlob, foundHint.PathGlob)) + } + } + } + return mode +} + // isRestricted checks whether a path falls under restricted writing scheme. // // Provided path is the full, absolute path of the entity that needs to be diff -Nru snapd-2.47.1+20.10.1build1/daemon/api_console_conf.go snapd-2.48+21.04/daemon/api_console_conf.go --- snapd-2.47.1+20.10.1build1/daemon/api_console_conf.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48+21.04/daemon/api_console_conf.go 2020-11-19 16:51:02.000000000 +0000 @@ -0,0 +1,96 @@ +// -*- 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 daemon + +import ( + "encoding/json" + "io" + "net/http" + "time" + + "github.com/snapcore/snapd/logger" + "github.com/snapcore/snapd/overlord/auth" +) + +var ( + routineConsoleConfStartCmd = &Command{ + Path: "/v2/internal/console-conf-start", + POST: consoleConfStartRoutine, + } +) + +var delayTime = 20 * time.Minute + +type consoleConfRoutine struct{} + +// ConsoleConfStartRoutineResult is the result of running the console-conf start +// routine.. +type ConsoleConfStartRoutineResult struct { + ActiveAutoRefreshChanges []string `json:"active-auto-refreshes,omitempty"` + ActiveAutoRefreshSnaps []string `json:"active-auto-refresh-snaps,omitempty"` +} + +func consoleConfStartRoutine(c *Command, r *http.Request, _ *auth.UserState) Response { + // no body expected, error if we were provided anything + defer r.Body.Close() + var routineBody struct{} + decoder := json.NewDecoder(r.Body) + if err := decoder.Decode(&routineBody); err != nil && err != io.EOF { + return BadRequest("cannot decode request body into console-conf operation: %v", err) + } + + // now run the start routine first by trying to grab a lock on the refreshes + // for all snaps, which fails if there are any active changes refreshing + // snaps + st := c.d.overlord.State() + st.Lock() + defer st.Unlock() + + snapAutoRefreshChanges, err := c.d.overlord.SnapManager().EnsureAutoRefreshesAreDelayed(delayTime) + if err != nil { + return InternalError(err.Error()) + } + + logger.Debugf("Ensured that new auto refreshes are delayed by %s to allow console-conf to run", delayTime) + + if len(snapAutoRefreshChanges) == 0 { + // no changes yet, and we delayed the refresh successfully so + // console-conf is okay to run normally + return SyncResponse(&ConsoleConfStartRoutineResult{}, nil) + } + + chgIds := make([]string, 0, len(snapAutoRefreshChanges)) + snapNames := make([]string, 0) + for _, chg := range snapAutoRefreshChanges { + chgIds = append(chgIds, chg.ID()) + var updatedSnaps []string + err := chg.Get("snap-names", &updatedSnaps) + if err != nil { + return InternalError(err.Error()) + } + snapNames = append(snapNames, updatedSnaps...) + } + + // we have changes that the client should wait for before being ready + return SyncResponse(&ConsoleConfStartRoutineResult{ + ActiveAutoRefreshChanges: chgIds, + ActiveAutoRefreshSnaps: snapNames, + }, nil) +} diff -Nru snapd-2.47.1+20.10.1build1/daemon/api_console_conf_test.go snapd-2.48+21.04/daemon/api_console_conf_test.go --- snapd-2.47.1+20.10.1build1/daemon/api_console_conf_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48+21.04/daemon/api_console_conf_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -0,0 +1,116 @@ +// -*- 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 daemon + +import ( + "bytes" + "net/http" + "sort" + "time" + + "github.com/snapcore/snapd/overlord/configstate/config" + "github.com/snapcore/snapd/overlord/snapstate" + "github.com/snapcore/snapd/overlord/state" + "gopkg.in/check.v1" + . "gopkg.in/check.v1" +) + +var _ = Suite(&consoleConfSuite{}) + +type consoleConfSuite struct { + apiBaseSuite +} + +func (s *consoleConfSuite) TestPostConsoleConfStartRoutine(c *C) { + t0 := time.Now() + d := s.daemonWithOverlordMock(c) + snapMgr, err := snapstate.Manager(d.overlord.State(), d.overlord.TaskRunner()) + c.Assert(err, check.IsNil) + d.overlord.AddManager(snapMgr) + + st := d.overlord.State() + + body := bytes.NewBuffer(nil) + req, err := http.NewRequest("POST", "/v2/internal/console-conf-start", body) + c.Assert(err, IsNil) + + // no changes in state, no changes in response + rsp := consoleConfStartRoutine(routineConsoleConfStartCmd, req, nil).(*resp) + c.Check(rsp.Type, Equals, ResponseTypeSync) + c.Assert(rsp.Result, DeepEquals, &ConsoleConfStartRoutineResult{}) + + // we did set the refresh.hold time back 20 minutes though + st.Lock() + defer st.Unlock() + + tr := config.NewTransaction(st) + var t1 time.Time + err = tr.Get("core", "refresh.hold", &t1) + c.Assert(err, IsNil) + + c.Assert(t0.Add(20*time.Minute).After(t1), Equals, false) + + // if we add some changes to state that are in progress, then they are + // returned + + // now make some auto-refresh changes to make sure we get those figured out + chg0 := st.NewChange("auto-refresh", "auto-refresh-the-things") + chg0.AddTask(st.NewTask("nop", "do nothing")) + + // make it in doing state + chg0.SetStatus(state.DoingStatus) + chg0.Set("snap-names", []string{"doing-snap"}) + + // this one will be picked up too + chg1 := st.NewChange("auto-refresh", "auto-refresh-the-things") + chg1.AddTask(st.NewTask("nop", "do nothing")) + chg1.SetStatus(state.DoStatus) + chg1.Set("snap-names", []string{"do-snap"}) + + // this one won't, it's Done + chg2 := st.NewChange("auto-refresh", "auto-refresh-the-things") + chg2.AddTask(st.NewTask("nop", "do nothing")) + chg2.SetStatus(state.DoneStatus) + chg2.Set("snap-names", []string{"done-snap"}) + + // nor this one, it's Undone + chg3 := st.NewChange("auto-refresh", "auto-refresh-the-things") + chg3.AddTask(st.NewTask("nop", "do nothing")) + chg3.SetStatus(state.UndoneStatus) + chg3.Set("snap-names", []string{"undone-snap"}) + + st.Unlock() + defer st.Lock() + + req2, err := http.NewRequest("POST", "/v2/internal/console-conf-start", body) + c.Assert(err, IsNil) + rsp2 := consoleConfStartRoutine(routineConsoleConfStartCmd, req2, nil).(*resp) + c.Check(rsp2.Type, Equals, ResponseTypeSync) + c.Assert(rsp2.Result, FitsTypeOf, &ConsoleConfStartRoutineResult{}) + res := rsp2.Result.(*ConsoleConfStartRoutineResult) + sort.Strings(res.ActiveAutoRefreshChanges) + sort.Strings(res.ActiveAutoRefreshSnaps) + expChgs := []string{chg0.ID(), chg1.ID()} + sort.Strings(expChgs) + c.Assert(res, DeepEquals, &ConsoleConfStartRoutineResult{ + ActiveAutoRefreshChanges: expChgs, + ActiveAutoRefreshSnaps: []string{"do-snap", "doing-snap"}, + }) +} diff -Nru snapd-2.47.1+20.10.1build1/daemon/api.go snapd-2.48+21.04/daemon/api.go --- snapd-2.47.1+20.10.1build1/daemon/api.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/daemon/api.go 2020-11-19 16:51:02.000000000 +0000 @@ -113,6 +113,8 @@ serialModelCmd, systemsCmd, systemsActionCmd, + routineConsoleConfStartCmd, + systemRecoveryKeysCmd, } var servicestateControl = servicestate.Control @@ -569,7 +571,7 @@ Status: 400, }, nil) } - if e, ok := err.(*httputil.PerstistentNetworkError); ok { + if e, ok := err.(*httputil.PersistentNetworkError); ok { return SyncResponse(&resp{ Type: ResponseTypeError, Result: &errorResult{Message: e.Error(), Kind: client.ErrorKindDNSFailure}, @@ -796,6 +798,7 @@ JailMode bool `json:"jailmode"` Classic bool `json:"classic"` IgnoreValidation bool `json:"ignore-validation"` + IgnoreRunning bool `json:"ignore-running"` Unaliased bool `json:"unaliased"` Purge bool `json:"purge,omitempty"` // dropping support temporarely until flag confusion is sorted, @@ -831,6 +834,10 @@ if inst.Unaliased { flags.Unaliased = true } + if inst.IgnoreRunning { + flags.IgnoreRunning = true + } + return flags, nil } @@ -884,6 +891,7 @@ snapshotRestore = snapshotstate.Restore snapshotSave = snapshotstate.Save snapshotExport = snapshotstate.Export + snapshotImport = snapshotstate.Import assertstateRefreshSnapDeclarations = assertstate.RefreshSnapDeclarations ) @@ -1025,6 +1033,9 @@ if inst.IgnoreValidation { flags.IgnoreValidation = true } + if inst.IgnoreRunning { + flags.IgnoreRunning = true + } if inst.Amend { flags.Amend = true } @@ -1423,6 +1434,7 @@ flags.RemoveSnapPath = true flags.Unaliased = isTrue(form, "unaliased") + flags.IgnoreRunning = isTrue(form, "ignore-running") // find the file for the "snap" form field var snapBody multipart.File @@ -2399,7 +2411,7 @@ serviceNames[i] = appInfo.ServiceName() } - sysd := systemd.New(dirs.GlobalRootDir, systemd.SystemMode, progress.Null) + sysd := systemd.New(systemd.SystemMode, progress.Null) reader, err := sysd.LogReader(serviceNames, n, follow) if err != nil { return InternalError("cannot get logs: %v", err) diff -Nru snapd-2.47.1+20.10.1build1/daemon/api_snapshots.go snapd-2.48+21.04/daemon/api_snapshots.go --- snapd-2.47.1+20.10.1build1/daemon/api_snapshots.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/daemon/api_snapshots.go 2020-11-19 16:51:02.000000000 +0000 @@ -23,6 +23,7 @@ "context" "encoding/json" "fmt" + "io" "net/http" "strconv" "strings" @@ -88,6 +89,11 @@ } func changeSnapshots(c *Command, r *http.Request, user *auth.UserState) Response { + contentType := r.Header.Get("Content-Type") + if contentType == client.SnapshotExportMediaType { + return doSnapshotImport(c, r, user) + } + var action snapshotAction decoder := json.NewDecoder(r.Body) if err := decoder.Decode(&action); err != nil { @@ -173,3 +179,24 @@ return &snapshotExportResponse{SnapshotExport: export} } + +func doSnapshotImport(c *Command, r *http.Request, user *auth.UserState) Response { + defer r.Body.Close() + + expectedSize, err := strconv.ParseInt(r.Header.Get("Content-Length"), 10, 64) + if err != nil { + return BadRequest("cannot parse Content-Length: %v", err) + } + // ensure we don't read more than we expect + limitedBodyReader := io.LimitReader(r.Body, expectedSize) + + // XXX: check that we have enough space to import the compressed snapshots + st := c.d.overlord.State() + setID, snapNames, err := snapshotImport(context.TODO(), st, limitedBodyReader) + if err != nil { + return BadRequest(err.Error()) + } + + result := map[string]interface{}{"set-id": setID, "snaps": snapNames} + return SyncResponse(result, nil) +} diff -Nru snapd-2.47.1+20.10.1build1/daemon/api_snapshots_test.go snapd-2.48+21.04/daemon/api_snapshots_test.go --- snapd-2.47.1+20.10.1build1/daemon/api_snapshots_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/daemon/api_snapshots_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -20,10 +20,14 @@ package daemon_test import ( + "bytes" "context" "errors" "fmt" + "io" + "io/ioutil" "net/http" + "strconv" "strings" "gopkg.in/check.v1" @@ -380,3 +384,75 @@ c.Check(rsp.Result, check.DeepEquals, &daemon.ErrorResult{Message: `cannot export 1: boom`}) c.Check(snapshotExportCalled, check.Equals, 1) } + +func (s *snapshotSuite) TestImportSnapshot(c *check.C) { + data := []byte("mocked snapshot export data file") + + setID := uint64(3) + snapNames := []string{"baz", "bar", "foo"} + defer daemon.MockSnapshotImport(func(context.Context, *state.State, io.Reader) (uint64, []string, error) { + return setID, snapNames, nil + })() + + req, err := http.NewRequest("POST", "/v2/snapshot/import", bytes.NewReader(data)) + req.Header.Add("Content-Length", strconv.Itoa(len(data))) + c.Assert(err, check.IsNil) + req.Header.Set("Content-Type", client.SnapshotExportMediaType) + + rsp := daemon.ChangeSnapshots(daemon.SnapshotCmd, req, nil) + c.Check(rsp.Type, check.Equals, daemon.ResponseTypeSync) + c.Check(rsp.Status, check.Equals, 200) + c.Check(rsp.Result, check.DeepEquals, map[string]interface{}{"set-id": setID, "snaps": snapNames}) +} + +func (s *snapshotSuite) TestImportSnapshotError(c *check.C) { + defer daemon.MockSnapshotImport(func(context.Context, *state.State, io.Reader) (uint64, []string, error) { + return uint64(0), nil, errors.New("no") + })() + + data := []byte("mocked snapshot export data file") + req, err := http.NewRequest("POST", "/v2/snapshot/import", bytes.NewReader(data)) + req.Header.Add("Content-Length", strconv.Itoa(len(data))) + c.Assert(err, check.IsNil) + req.Header.Set("Content-Type", client.SnapshotExportMediaType) + + rsp := daemon.ChangeSnapshots(daemon.SnapshotCmd, req, nil) + c.Assert(rsp.Type, check.Equals, daemon.ResponseTypeError) + c.Check(rsp.Status, check.Equals, 400) + c.Check(rsp.ErrorResult().Message, check.Equals, "no") +} + +func (s *snapshotSuite) TestImportSnapshotNoContentLengthError(c *check.C) { + data := []byte("mocked snapshot export data file") + req, err := http.NewRequest("POST", "/v2/snapshot/import", bytes.NewReader(data)) + c.Assert(err, check.IsNil) + req.Header.Set("Content-Type", client.SnapshotExportMediaType) + + rsp := daemon.ChangeSnapshots(daemon.SnapshotCmd, req, nil) + c.Assert(rsp.Type, check.Equals, daemon.ResponseTypeError) + c.Check(rsp.Status, check.Equals, 400) + c.Check(rsp.ErrorResult().Message, check.Equals, `cannot parse Content-Length: strconv.ParseInt: parsing "": invalid syntax`) +} + +func (s *snapshotSuite) TestImportSnapshotLimits(c *check.C) { + var dataRead int + + defer daemon.MockSnapshotImport(func(ctx context.Context, st *state.State, r io.Reader) (uint64, []string, error) { + data, err := ioutil.ReadAll(r) + c.Assert(err, check.IsNil) + dataRead = len(data) + return uint64(0), nil, nil + })() + + data := []byte("much more data than expected from Content-Length") + req, err := http.NewRequest("POST", "/v2/snapshot/import", bytes.NewReader(data)) + // limit to 10 and check that this is really all that is read + req.Header.Add("Content-Length", "10") + c.Assert(err, check.IsNil) + req.Header.Set("Content-Type", client.SnapshotExportMediaType) + + rsp := daemon.ChangeSnapshots(daemon.SnapshotCmd, req, nil) + c.Assert(rsp.Type, check.Equals, daemon.ResponseTypeSync) + c.Check(rsp.Status, check.Equals, 200) + c.Check(dataRead, check.Equals, 10) +} diff -Nru snapd-2.47.1+20.10.1build1/daemon/api_system_recovery_keys.go snapd-2.48+21.04/daemon/api_system_recovery_keys.go --- snapd-2.47.1+20.10.1build1/daemon/api_system_recovery_keys.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48+21.04/daemon/api_system_recovery_keys.go 2020-11-19 16:51:02.000000000 +0000 @@ -0,0 +1,54 @@ +// -*- 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 daemon + +import ( + "net/http" + "path/filepath" + + "github.com/snapcore/snapd/client" + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/overlord/auth" + "github.com/snapcore/snapd/secboot" +) + +var systemRecoveryKeysCmd = &Command{ + Path: "/v2/system-recovery-keys", + GET: getSystemRecoveryKeys, + RootOnly: true, +} + +func getSystemRecoveryKeys(c *Command, r *http.Request, user *auth.UserState) Response { + var rsp client.SystemRecoveryKeysResponse + + rkey, err := secboot.RecoveryKeyFromFile(filepath.Join(dirs.SnapFDEDir, "recovery.key")) + if err != nil { + return InternalError(err.Error()) + } + rsp.RecoveryKey = rkey.String() + + reinstallKey, err := secboot.RecoveryKeyFromFile(filepath.Join(dirs.SnapFDEDir, "reinstall.key")) + if err != nil { + return InternalError(err.Error()) + } + rsp.ReinstallKey = reinstallKey.String() + + return SyncResponse(&rsp, nil) +} diff -Nru snapd-2.47.1+20.10.1build1/daemon/api_system_recovery_keys_test.go snapd-2.48+21.04/daemon/api_system_recovery_keys_test.go --- snapd-2.47.1+20.10.1build1/daemon/api_system_recovery_keys_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48+21.04/daemon/api_system_recovery_keys_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -0,0 +1,87 @@ +// -*- 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 daemon + +import ( + "encoding/hex" + "io/ioutil" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/client" + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/secboot" +) + +func mockSystemRecoveryKeys(c *C) { + // same inputs/outputs as secboot:crypt_test.go in this test + rkeystr, err := hex.DecodeString("e1f01302c5d43726a9b85b4a8d9c7f6e") + c.Assert(err, IsNil) + rkeyPath := filepath.Join(dirs.SnapFDEDir, "recovery.key") + err = os.MkdirAll(filepath.Dir(rkeyPath), 0755) + c.Assert(err, IsNil) + err = ioutil.WriteFile(rkeyPath, []byte(rkeystr), 0644) + c.Assert(err, IsNil) + + skeystr := "1234567890123456" + c.Assert(err, IsNil) + skeyPath := filepath.Join(dirs.SnapFDEDir, "reinstall.key") + err = ioutil.WriteFile(skeyPath, []byte(skeystr), 0644) + c.Assert(err, IsNil) +} + +func (s *apiSuite) TestSystemGetRecoveryKeysAsRootHappy(c *C) { + if (secboot.RecoveryKey{}).String() == "not-implemented" { + c.Skip("needs working secboot recovery key") + } + + s.daemon(c) + mockSystemRecoveryKeys(c) + + req, err := http.NewRequest("GET", "/v2/system-recovery-keys", nil) + c.Assert(err, IsNil) + + rsp := getSystemRecoveryKeys(systemRecoveryKeysCmd, req, nil).(*resp) + c.Assert(rsp.Status, Equals, 200) + srk := rsp.Result.(*client.SystemRecoveryKeysResponse) + c.Assert(srk, DeepEquals, &client.SystemRecoveryKeysResponse{ + RecoveryKey: "61665-00531-54469-09783-47273-19035-40077-28287", + ReinstallKey: "12849-13363-13877-14391-12345-12849-13363-13877", + }) +} + +func (s *apiSuite) TestSystemGetRecoveryAsUserErrors(c *C) { + s.daemon(c) + mockSystemRecoveryKeys(c) + + req, err := http.NewRequest("GET", "/v2/system-recovery-key", nil) + c.Assert(err, IsNil) + + req.RemoteAddr = "pid=100;uid=1000;socket=;" + rec := httptest.NewRecorder() + systemsActionCmd.ServeHTTP(rec, req) + + systemRecoveryKeysCmd.ServeHTTP(rec, req) + c.Assert(rec.Code, Equals, 401) +} diff -Nru snapd-2.47.1+20.10.1build1/daemon/api_systems_test.go snapd-2.48+21.04/daemon/api_systems_test.go --- snapd-2.47.1+20.10.1build1/daemon/api_systems_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/daemon/api_systems_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -487,7 +487,7 @@ // daemon is not started, only check whether reboot was scheduled as expected // reboot flag - c.Check(d.restartSystem, check.Equals, state.RestartSystemNow, check.Commentf(tc.comment)) + c.Check(d.requestedRestart, check.Equals, state.RestartSystemNow, check.Commentf(tc.comment)) // slow reboot schedule c.Check(cmd.Calls(), check.DeepEquals, [][]string{ {"shutdown", "-r", "+10", "reboot scheduled to update the system"}, diff -Nru snapd-2.47.1+20.10.1build1/daemon/api_test.go snapd-2.48+21.04/daemon/api_test.go --- snapd-2.47.1+20.10.1build1/daemon/api_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/daemon/api_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -163,7 +163,13 @@ func (s *apiBaseSuite) SnapAction(ctx context.Context, currentSnaps []*store.CurrentSnap, actions []*store.SnapAction, assertQuery store.AssertionQuery, user *auth.UserState, opts *store.RefreshOptions) ([]store.SnapActionResult, []store.AssertionResult, error) { s.pokeStateLock() if assertQuery != nil { - panic("no assertion query support") + toResolve, err := assertQuery.ToResolve() + if err != nil { + return nil, nil, err + } + if len(toResolve) != 0 { + panic("no assertion query support") + } } if ctx == nil { @@ -4047,6 +4053,43 @@ c.Check(summary, check.Equals, `Refresh "some-snap" snap`) } +func (s *apiSuite) TestRefreshIgnoreRunning(c *check.C) { + var calledFlags snapstate.Flags + installQueue := []string{} + + snapstateUpdate = func(s *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags) (*state.TaskSet, error) { + calledFlags = flags + installQueue = append(installQueue, name) + + t := s.NewTask("fake-refresh-snap", "Doing a fake install") + return state.NewTaskSet(t), nil + } + assertstateRefreshSnapDeclarations = func(s *state.State, userID int) error { + return nil + } + + d := s.daemon(c) + inst := &snapInstruction{ + Action: "refresh", + IgnoreRunning: true, + Snaps: []string{"some-snap"}, + } + + st := d.overlord.State() + st.Lock() + defer st.Unlock() + summary, _, err := inst.dispatch()(inst, st) + c.Check(err, check.IsNil) + + flags := snapstate.Flags{} + flags.IgnoreRunning = true + + c.Check(calledFlags, check.DeepEquals, flags) + c.Check(err, check.IsNil) + c.Check(installQueue, check.DeepEquals, []string{"some-snap"}) + c.Check(summary, check.Equals, `Refresh "some-snap" snap`) +} + func (s *apiSuite) TestRefreshCohort(c *check.C) { cohort := "" @@ -6745,6 +6788,33 @@ c.Check(calledFlags.Unaliased, check.Equals, true) } +func (s *apiSuite) TestInstallIgnoreRunning(c *check.C) { + var calledFlags snapstate.Flags + + snapstateInstall = func(ctx context.Context, s *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags) (*state.TaskSet, error) { + calledFlags = flags + + t := s.NewTask("fake-install-snap", "Doing a fake install") + return state.NewTaskSet(t), nil + } + + d := s.daemon(c) + inst := &snapInstruction{ + Action: "install", + // Install the snap without enabled automatic aliases + IgnoreRunning: true, + Snaps: []string{"fake"}, + } + + st := d.overlord.State() + st.Lock() + defer st.Unlock() + _, _, err := inst.dispatch()(inst, st) + c.Check(err, check.IsNil) + + c.Check(calledFlags.IgnoreRunning, check.Equals, true) +} + func (s *apiSuite) TestInstallPathUnaliased(c *check.C) { body := "" + "----hello--\r\n" + diff -Nru snapd-2.47.1+20.10.1build1/daemon/api_users.go snapd-2.48+21.04/daemon/api_users.go --- snapd-2.47.1+20.10.1build1/daemon/api_users.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/daemon/api_users.go 2020-11-19 16:51:02.000000000 +0000 @@ -315,6 +315,10 @@ } if !createData.ForceManaged { + if len(users) > 0 && createData.Automatic { + // no users created but no error with the automatic flag + return SyncResponse([]userResponseData{}, nil) + } if len(users) > 0 { return BadRequest("cannot create user: device already managed") } @@ -322,6 +326,11 @@ return BadRequest("cannot create user: device is a classic system") } } + if createData.Automatic { + // Automatic implies known/sudoers + createData.Known = true + createData.Sudoer = true + } var model *asserts.Model var serial *asserts.Serial @@ -521,6 +530,7 @@ Sudoer bool `json:"sudoer"` Known bool `json:"known"` ForceManaged bool `json:"force-managed"` + Automatic bool `json:"automatic"` // singleUserResultCompat indicates whether to preserve // backwards compatibility, which results in more clunky diff -Nru snapd-2.47.1+20.10.1build1/daemon/api_users_test.go snapd-2.48+21.04/daemon/api_users_test.go --- snapd-2.47.1+20.10.1build1/daemon/api_users_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/daemon/api_users_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -612,6 +612,17 @@ } func (s *userSuite) TestPostCreateUserFromAssertionAllKnown(c *check.C) { + expectSudoer := false + s.testPostCreateUserFromAssertion(c, `{"known":true}`, expectSudoer) +} + +func (s *userSuite) TestPostCreateUserFromAssertionAllAutomatic(c *check.C) { + // automatic implies "sudoder" + expectSudoer := true + s.testPostCreateUserFromAssertion(c, `{"automatic":true}`, expectSudoer) +} + +func (s *userSuite) testPostCreateUserFromAssertion(c *check.C, postData string, expectSudoer bool) { s.makeSystemUsers(c, []map[string]interface{}{goodUser, partnerUser, serialUser, badUser, badUserNoMatchingSerial, unknownUser}) created := map[string]bool{} // mock the calls that create the user @@ -627,7 +638,7 @@ c.Logf("unexpected username %q", username) c.Fail() } - c.Check(opts.Sudoer, check.Equals, false) + c.Check(opts.Sudoer, check.Equals, expectSudoer) c.Check(opts.Password, check.Equals, "$6$salt$hash") created[username] = true return nil @@ -642,7 +653,7 @@ } // do it! - buf := bytes.NewBufferString(`{"known":true}`) + buf := bytes.NewBufferString(postData) req, err := http.NewRequest("POST", "/v2/create-user", buf) c.Assert(err, check.IsNil) @@ -709,6 +720,29 @@ c.Check(rsp.Result.(*errorResult).Message, check.Matches, `cannot create user: device already managed`) } +func (s *userSuite) TestPostCreateUserAutomaticManagedDoesNotActOrError(c *check.C) { + s.makeSystemUsers(c, []map[string]interface{}{goodUser}) + + st := s.d.overlord.State() + st.Lock() + _, err := auth.NewUser(st, "username", "email@test.com", "macaroon", []string{"discharge"}) + st.Unlock() + c.Check(err, check.IsNil) + + // do it! + buf := bytes.NewBufferString(`{"automatic":true}`) + req, err := http.NewRequest("POST", "/v2/create-user", buf) + c.Assert(err, check.IsNil) + + rsp := postCreateUser(createUserCmd, req, nil).(*resp) + + // expecting an empty reply + expected := []userResponseData{} + c.Check(rsp.Type, check.Equals, ResponseTypeSync) + c.Check(rsp.Result, check.FitsTypeOf, expected) + c.Check(rsp.Result, check.DeepEquals, expected) +} + func (s *userSuite) TestPostCreateUserFromAssertionAllKnownNoModelError(c *check.C) { restore := release.MockOnClassic(false) defer restore() @@ -842,6 +876,7 @@ c.Check(rsp.Result, check.DeepEquals, expected) } +// XXX: wrong suite func (s *userSuite) TestSysInfoIsManaged(c *check.C) { st := s.d.overlord.State() st.Lock() @@ -858,6 +893,7 @@ c.Check(rsp.Result.(map[string]interface{})["managed"], check.Equals, true) } +// XXX: wrong suite func (s *userSuite) TestSysInfoWorksDegraded(c *check.C) { s.d.SetDegradedMode(fmt.Errorf("some error")) diff -Nru snapd-2.47.1+20.10.1build1/daemon/daemon.go snapd-2.48+21.04/daemon/daemon.go --- snapd-2.47.1+20.10.1build1/daemon/daemon.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/daemon/daemon.go 2020-11-19 16:51:02.000000000 +0000 @@ -20,7 +20,9 @@ package daemon import ( + "bytes" "context" + "encoding/json" "fmt" "net" "net/http" @@ -55,6 +57,12 @@ var systemdSdNotify = systemd.SdNotify +const ( + daemonRestartMsg = "system is restarting" + systemRestartMsg = "daemon is restarting" + socketRestartMsg = "daemon is stopping to wait for socket activation" +) + // A Daemon listens for requests and routes them to the right command type Daemon struct { Version string @@ -68,8 +76,8 @@ router *mux.Router standbyOpinions *standby.StandbyOpinions - // set to remember we need to restart the system - restartSystem state.RestartType + // set to what kind of restart was requested if any + requestedRestart state.RestartType // set to remember that we need to exit the daemon in a way that // prevents systemd from restarting it restartSocket bool @@ -259,14 +267,10 @@ if rsp, ok := rsp.(*resp); ok { _, rst := st.Restarting() - switch rst { - case state.RestartSystem, state.RestartSystemNow: - rsp.transmitMaintenance(client.ErrorKindSystemRestart, "system is restarting") - case state.RestartDaemon: - rsp.transmitMaintenance(client.ErrorKindDaemonRestart, "daemon is restarting") - case state.RestartSocket: - rsp.transmitMaintenance(client.ErrorKindDaemonRestart, "daemon is stopping to wait for socket activation") + if rst != state.RestartUnset { + rsp.Maintenance = maintenanceForRestartType(rst) } + if rsp.Type != ResponseTypeError { st.Lock() count, stamp := st.WarningsSummary() @@ -450,6 +454,13 @@ // enable standby handling d.initStandbyHandling() + // before serving actual connections remove the maintenance.json file as we + // are no longer down for maintenance, this state most closely corresponds + // to state.RestartUnset + if err := d.updateMaintenanceFile(state.RestartUnset); err != nil { + return err + } + // the loop runs in its own goroutine d.overlord.Loop() @@ -478,9 +489,14 @@ // HandleRestart implements overlord.RestartBehavior. func (d *Daemon) HandleRestart(t state.RestartType) { + d.mu.Lock() + defer d.mu.Unlock() + // die when asked to restart (systemd should get us back up!) etc switch t { case state.RestartDaemon: + // save the restart kind to write out a maintenance.json in a bit + d.requestedRestart = t case state.RestartSystem, state.RestartSystemNow: // try to schedule a fallback slow reboot already here // in case we get stuck shutting down @@ -488,19 +504,18 @@ logger.Noticef("%s", err) } - d.mu.Lock() - defer d.mu.Unlock() - // remember we need to restart the system - d.restartSystem = t + // save the restart kind to write out a maintenance.json in a bit + d.requestedRestart = t case state.RestartSocket: - d.mu.Lock() - defer d.mu.Unlock() + // save the restart kind to write out a maintenance.json in a bit + d.requestedRestart = t d.restartSocket = true case state.StopDaemon: logger.Noticef("stopping snapd as requested") default: logger.Noticef("internal error: restart handler called with unknown restart type: %v", t) } + d.tomb.Kill(nil) } @@ -511,6 +526,27 @@ rebootMaxTentatives = 3 ) +func (d *Daemon) updateMaintenanceFile(rst state.RestartType) error { + // for unset restart, just remove the maintenance.json file + if rst == state.RestartUnset { + err := os.Remove(dirs.SnapdMaintenanceFile) + // only return err if the error was something other than the file not + // existing + if err != nil && !os.IsNotExist(err) { + return err + } + return nil + } + + // otherwise marshal and write it out appropriately + b, err := json.Marshal(maintenanceForRestartType(rst)) + if err != nil { + return err + } + + return osutil.AtomicWrite(dirs.SnapdMaintenanceFile, bytes.NewBuffer(b), 0644, 0) +} + // Stop shuts down the Daemon func (d *Daemon) Stop(sigCh chan<- os.Signal) error { // we need to schedule/wait for a system restart again @@ -525,12 +561,23 @@ d.tomb.Kill(nil) + // check the state associated with a potential restart with the lock to + // prevent races d.mu.Lock() - restartSystem := d.restartSystem != state.RestartUnset - immediateReboot := d.restartSystem == state.RestartSystemNow + // needsFullReboot is whether the entire system will be rebooted or not as + // a consequence of this restart + needsFullReboot := (d.requestedRestart == state.RestartSystemNow || d.requestedRestart == state.RestartSystem) + immediateReboot := d.requestedRestart == state.RestartSystemNow restartSocket := d.restartSocket d.mu.Unlock() + // before not accepting any new client connections we need to write the + // maintenance.json file for potential clients to see after the daemon stops + // responding so they can read it correctly and handle the maintenance + if err := d.updateMaintenanceFile(d.requestedRestart); err != nil { + logger.Noticef("error writing maintenance file: %v", err) + } + d.snapdListener.Close() d.standbyOpinions.Stop() @@ -547,7 +594,7 @@ d.snapListener.Close() } - if restartSystem { + if needsFullReboot { // give time to polling clients to notice restart time.Sleep(rebootNoticeWait) } @@ -559,10 +606,9 @@ d.tomb.Kill(d.serve.Shutdown(ctx)) cancel() - if !restartSystem { + if !needsFullReboot { // tell systemd that we are stopping systemdSdNotify("STOPPING=1") - } if restartSocket { @@ -580,8 +626,7 @@ } d.overlord.Stop() - err := d.tomb.Wait() - if err != nil { + if err := d.tomb.Wait(); err != nil { if err == context.DeadlineExceeded { logger.Noticef("WARNING: cannot gracefully shut down in-flight snapd API activity within: %v", shutdownTimeout) // the process is shutting down anyway, so we may just @@ -592,7 +637,7 @@ // because we already scheduled a slow shutdown and // exiting here will just restart snapd (via systemd) // which will lead to confusing results. - if restartSystem { + if needsFullReboot { logger.Noticef("WARNING: cannot stop daemon: %v", err) } else { return err @@ -600,7 +645,7 @@ } } - if restartSystem { + if needsFullReboot { return d.doReboot(sigCh, immediateReboot, rebootWaitTimeout) } diff -Nru snapd-2.47.1+20.10.1build1/daemon/daemon_test.go snapd-2.48+21.04/daemon/daemon_test.go --- snapd-2.47.1+20.10.1build1/daemon/daemon_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/daemon/daemon_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -209,6 +209,27 @@ }) } +func (s *daemonSuite) TestMaintenanceJsonDeletedOnStart(c *check.C) { + // write a maintenance.json file that has that the system is restarting + maintErr := &errorResult{ + Kind: client.ErrorKindDaemonRestart, + Message: systemRestartMsg, + } + + b, err := json.Marshal(maintErr) + c.Assert(err, check.IsNil) + c.Assert(os.MkdirAll(filepath.Dir(dirs.SnapdMaintenanceFile), 0755), check.IsNil) + c.Assert(ioutil.WriteFile(dirs.SnapdMaintenanceFile, b, 0644), check.IsNil) + + d := newTestDaemon(c) + makeDaemonListeners(c, d) + + // after starting, maintenance.json should be removed + d.Start() + c.Assert(dirs.SnapdMaintenanceFile, testutil.FileAbsent) + d.Stop(nil) +} + func (s *daemonSuite) TestFillsWarnings(c *check.C) { d := newTestDaemon(c) @@ -590,7 +611,12 @@ d.snapListener = &witnessAcceptListener{Listener: l, accept: snapAccept} c.Assert(d.Start(), check.IsNil) - defer d.Stop(nil) + stoppedYet := false + defer func() { + if !stoppedYet { + d.Stop(nil) + } + }() snapdDone := make(chan struct{}) go func() { @@ -622,6 +648,11 @@ case <-time.After(2 * time.Second): c.Fatal("RequestRestart -> overlord -> Kill chain didn't work") } + + d.Stop(nil) + stoppedYet = true + + c.Assert(s.notified, check.DeepEquals, []string{"EXTEND_TIMEOUT_USEC=30000000", "READY=1", "STOPPING=1"}) } func (s *daemonSuite) TestGracefulStop(c *check.C) { @@ -877,7 +908,7 @@ defer func() { d.mu.Lock() - d.restartSystem = state.RestartUnset + d.requestedRestart = state.RestartUnset d.mu.Unlock() }() @@ -888,7 +919,7 @@ } d.mu.Lock() - rs := d.restartSystem + rs := d.requestedRestart d.mu.Unlock() c.Check(rs, check.Equals, restartKind) @@ -924,6 +955,17 @@ // should be good enough c.Check(rebootAt.Before(now.Add(10*time.Second)), check.Equals, true) } + + // finally check that maintenance.json was written appropriate for this + // restart reason + b, err := ioutil.ReadFile(dirs.SnapdMaintenanceFile) + c.Assert(err, check.IsNil) + + maintErr := &errorResult{} + c.Assert(json.Unmarshal(b, maintErr), check.IsNil) + + exp := maintenanceForRestartType(restartKind) + c.Assert(maintErr, check.DeepEquals, exp) } func (s *daemonSuite) TestRestartSystemGracefulWiring(c *check.C) { diff -Nru snapd-2.47.1+20.10.1build1/daemon/export_api_snapshots_test.go snapd-2.48+21.04/daemon/export_api_snapshots_test.go --- snapd-2.47.1+20.10.1build1/daemon/export_api_snapshots_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/daemon/export_api_snapshots_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -22,6 +22,7 @@ import ( "context" "encoding/json" + "io" "net/http" "gopkg.in/check.v1" @@ -80,6 +81,14 @@ } } +func MockSnapshotImport(newImport func(context.Context, *state.State, io.Reader) (uint64, []string, error)) (restore func()) { + oldImport := snapshotImport + snapshotImport = newImport + return func() { + snapshotImport = oldImport + } +} + func MustUnmarshalSnapInstruction(c *check.C, jinst string) *snapInstruction { var inst snapInstruction if err := json.Unmarshal([]byte(jinst), &inst); err != nil { diff -Nru snapd-2.47.1+20.10.1build1/daemon/response.go snapd-2.48+21.04/daemon/response.go --- snapd-2.47.1+20.10.1build1/daemon/response.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/daemon/response.go 2020-11-19 16:51:02.000000000 +0000 @@ -37,6 +37,7 @@ "github.com/snapcore/snapd/logger" "github.com/snapcore/snapd/overlord/snapshotstate" "github.com/snapcore/snapd/overlord/snapstate" + "github.com/snapcore/snapd/overlord/state" "github.com/snapcore/snapd/snap" "github.com/snapcore/snapd/store" "github.com/snapcore/snapd/systemd" @@ -67,11 +68,23 @@ Maintenance *errorResult `json:"maintenance,omitempty"` } -func (r *resp) transmitMaintenance(kind client.ErrorKind, message string) { - r.Maintenance = &errorResult{ - Kind: kind, - Message: message, +func maintenanceForRestartType(rst state.RestartType) *errorResult { + e := &errorResult{} + switch rst { + case state.RestartSystem, state.RestartSystemNow: + e.Kind = client.ErrorKindSystemRestart + e.Message = daemonRestartMsg + case state.RestartDaemon: + e.Kind = client.ErrorKindDaemonRestart + e.Message = systemRestartMsg + case state.RestartSocket: + e.Kind = client.ErrorKindDaemonRestart + e.Message = socketRestartMsg + case state.RestartUnset: + // shouldn't happen, maintenance for unset type should just be nil + panic("internal error: cannot marshal maintenance for RestartUnset") } + return e } func (r *resp) addWarningsToMeta(count int, stamp time.Time) { @@ -92,7 +105,7 @@ // JSON representation in the API in time for the release. // The right code style takes a bit more work and unifies // these fields inside resp. -// Increment the counter if you read this: 42 +// Increment the counter if you read this: 43 type Meta struct { Sources []string `json:"sources,omitempty"` SuggestedCurrency string `json:"suggested-currency,omitempty"` @@ -259,6 +272,7 @@ // ServeHTTP from the Response interface func (s snapshotExportResponse) ServeHTTP(w http.ResponseWriter, r *http.Request) { w.Header().Add("Content-Length", strconv.FormatInt(s.Size(), 10)) + w.Header().Add("Content-Type", client.SnapshotExportMediaType) if err := s.StreamTo(w); err != nil { logger.Debugf("cannot export snapshot: %v", err) } diff -Nru snapd-2.47.1+20.10.1build1/dbusutil/dbusutil.go snapd-2.48+21.04/dbusutil/dbusutil.go --- snapd-2.47.1+20.10.1build1/dbusutil/dbusutil.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/dbusutil/dbusutil.go 2020-11-19 16:51:02.000000000 +0000 @@ -60,7 +60,8 @@ // to use the session bus, we expect session bus daemon to have been started and // managed by the corresponding user session manager. // -// This function is mockable by either MockConnections or MockSessionBus. +// This function is mockable by either MockConnections or +// MockOnlySessionBusAvailable. var SessionBus = func() (*dbus.Conn, error) { if isSessionBusLikelyPresent() { return dbus.SessionBus() @@ -70,7 +71,8 @@ // SystemBus is like dbus.SystemBus and is provided for completeness. // -// This function is mockable by either MockConnections or MockSystemBus. +// This function is mockable by either MockConnections or +// MockOnlySystemBusAvailable. var SystemBus = func() (*dbus.Conn, error) { return dbus.SystemBus() } diff -Nru snapd-2.47.1+20.10.1build1/debian/changelog snapd-2.48+21.04/debian/changelog --- snapd-2.47.1+20.10.1build1/debian/changelog 2020-11-11 22:25:55.000000000 +0000 +++ snapd-2.48+21.04/debian/changelog 2020-11-19 16:51:02.000000000 +0000 @@ -1,17 +1,322 @@ -snapd (2.47.1+20.10.1build1) hirsute; urgency=medium +snapd (2.48+21.04) hirsute; urgency=medium - * No-change rebuild using new golang - - -- Steve Langasek Wed, 11 Nov 2020 22:25:55 +0000 - -snapd (2.47.1+20.10.1) groovy; urgency=medium - - * cherry-pick PR#9516 to fix docker and multipass snaps - with apparmor3 (LP: #1898038) + * New upstream release, LP: #1904098 + - osutil: add KernelCommandLineKeyValue + - devicestate: implement boot.HasFDESetupHook + - boot/makebootable.go: set snapd_recovery_mode=install at image- + build time + - bootloader: use ForGadget when installing boot config + - interfaces/raw_usb: allow read access to /proc/tty/drivers + - boot: add scaffolding for "fde-setup" hook support for sealing + - tests: fix basic20 test on arm devices + - seed: make a shared seed system label validation helper + - snap: add new "fde-setup" hooktype + - cmd/snap-bootstrap, secboot, tests: misc cleanups, add spread test + - secboot,cmd/snap-bootstrap: fix degraded mode cases with better + device handling + - boot,dirs,c/snap-bootstrap: avoid InstallHost* at the cost of some + messiness + - tests/nested/manual/refresh-revert-fundamentals: temporarily + disable secure boot + - snap-bootstrap,secboot: call BlockPCRProtectionPolicies in all + boot modes + - many: address degraded recover mode feedback, cleanups + - tests: Use systemd-run on tests part2 + - tests: set the opensuse tumbleweed system as manual in spread.yaml + - secboot: call BlockPCRProtectionPolicies even if the TPM is + disabled + - vendor: update to current secboot + - cmd/snap-bootstrap,o/devicestate: use a secret to pair data and + save + - spread.yaml: increase number of workers on 20.10 + - snap: add new `snap recovery --show-keys` option + - tests: minor test tweaks suggested in the review of 9607 + - snapd-generator: set standard snapfuse options when generating + units for containers + - tests: enable lxd test on ubuntu-core-20 and 16.04-32 + - interfaces: share /tmp/.X11-unix/ from host or provider + - tests: enable main lxd test on 20.10 + - cmd/s-b/initramfs-mounts: refactor recover mode to implement + degraded mode + - gadget/install: add progress logging + - packaging: keep secboot/encrypt_dummy.go in debian + - interfaces/udev: use distro specific path to snap-device-helper + - o/devistate: fix chaining of tasks related to regular snaps when + preseeding + - gadget, overlord/devicestate: validate that system supports + encrypted data before install + - interfaces/fwupd: enforce the confined fwupd to align Ubuntu Core + ESP layout + - many: add /v2/system-recovery-keys API and client + - secboot, many: return UnlockMethod from Unlock* methods for future + usage + - many: mv keys to ubuntu-boot, move model file, rename keyring + prefix for secboot + - tests: using systemd-run instead of manually create a systemd unit + - part 1 + - secboot, cmd/snap-bootstrap: enable or disable activation with + recovery key + - secboot: refactor Unlock...IfEncrypted to take keyfile + check + disks first + - secboot: add LockTPMSealedKeys() to lock access to keys + independently + - gadget: correct sfdisk arguments + - bootloader/assets/grub: adjust fwsetup menuentry label + - tests: new boot state tool + - spread: use the official image for Ubuntu 20.10, no longer an + unstable system + - tests/lib/nested: enable snapd logging to console for core18 + - osutil/disks: re-implement partition searching for disk w/ non- + adjacent parts + - tests: using the nested-state tool in nested tests + - many: seal a fallback object to the recovery boot chain + - gadget, gadget/install: move helpers to install package, refactor + unit tests + - dirs: add "gentoo" to altDirDistros + - update-pot: include file locations in translation template, and + extract strings from desktop files + - gadget/many: drop usage of gpt attr 59 for indicating creation of + partitions + - gadget/quantity: tweak test name + - snap: fix failing unittest for quantity.FormatDuration() + - gadget/quantity: introduce a new package that captures quantities + - o/devicestate,a/sysdb: make a backup of the device serial to save + - tests: fix rare interaction of tests.session and specific tests + - features: enable classic-preserves-xdg-runtime-dir + - tests/nested/core20/save: check the bind mount and size bump + - o/devicetate,dirs: keep device keys in ubuntu-save/save for UC20 + - tests: rename hasHooks to hasInterfaceHooks in the ifacestate + tests + - o/devicestate: unit test tweaks + - boot: store the TPM{PolicyAuthKey,LockoutAuth}File in ubuntu-save + - testutil, cmd/snap/version: fix misc little errors + - overlord/devicestate: bind mount ubuntu-save under + /var/lib/snapd/save on startup + - gadget/internal: tune ext4 setting for smaller filesystems + - tests/nested/core20/save: a test that verifies ubuntu-save is + present and set up + - tests: update google sru backend to support groovy + - o/ifacestate: handle interface hooks when preseeding + - tests: re-enable the apt hooks test + - interfaces,snap: use correct type: {os,snapd} for test data + - secboot: set metadata and keyslots sizes when formatting LUKS2 + volumes + - tests: improve uc20-create-partitions-reinstall test + - client, daemon, cmd/snap: cleanups from #9489 + more unit tests + - cmd/snap-bootstrap: mount ubuntu-save during boot if present + - secboot: fix doc comment on helper for unlocking volume with key + - tests: add spread test for refreshing from an old snapd and core18 + - o/snapstate: generate snapd snap wrappers again after restart on + refresh + - secboot: version bump, unlock volume with key + - tests/snap-advise-command: re-enable test + - cmd/snap, snapmgr, tests: cleanups after #9418 + - interfaces: deny connected x11 plugs access to ICE + - daemon,client: write and read a maintenance.json file for when + snapd is shut down + - many: update to secboot v1 (part 1) + - osutil/disks/mockdisk: panic if same mountpoint shows up again + with diff opts + - tests/nested/core20/gadget,kernel-reseal: add sanity checks to the + reseal tests + - many: implement snap routine console-conf-start for synchronizing + auto-refreshes + - dirs, boot: add ubuntu-save directories and related locations + - usersession: fix typo in test name + - overlord/snapstate: refactor ihibitRefresh + - overlord/snapstate: stop warning about inhibited refreshes + - cmd/snap: do not hardcode snapshot age value + - overlord,usersession: initial notifications of pending refreshes + - tests: add a unit test for UpdateMany where a single snap fails + - o/snapstate/catalogrefresh.go: don't refresh catalog in install + mode uc20 + - tests: also check snapst.Current in undo-unlink tests + - tests: new nested tool + - o/snapstate: implement undo handler for unlink-snap + - tests: clean systems.sh helper and migrate last set of tests + - tests: moving the lib section from systems.sh helper to os.query + tool + - tests/uc20-create-partitions: don't check for grub.cfg + - packaging: make sure that static binaries are indeed static, fix + openSUSE + - many: have install return encryption keys for data and save, + improve tests + - overlord: add link participant for linkage transitions + - tests: lxd smoke test + - tests: add tests for fsck; cmd/s-b/initramfs-mounts: fsck ubuntu- + seed too + - tests: moving main suite from systems.sh to os.query tool + - tests: moving the core test suite from systems.sh to os.query tool + - cmd/snap-confine: mask host's apparmor config + - o/snapstate: move setting updated SnapState after error paths + - tests: add value to INSTANCE_KEY/regular + - spread, tests: tweaks for openSUSE + - cmd/snap-confine: update path to snap-device-helper in AppArmor + profile + - tests: new os.query tool + - overlord/snapshotstate/backend: specify tar format for snapshots + - tests/nested/manual/minimal-smoke: use 384MB of RAM for nested + UC20 + - client,daemon,snap: auto-import does not error on managed devices + - interfaces: PTP hardware clock interface + - tests: use tests.backup tool + - many: verify that unit tests work with nosecboot tag and without + secboot package + - wrappers: do not error out on read-only /etc/dbus-1/session.d + filesystem on core18 + - snapshots: import of a snapshot set + - tests: more output for sbuild test + - o/snapstate: re-order remove tasks for individual snap revisions + to remove current last + - boot: skip some unit tests when running as root + - o/assertstate: introduce + ValidationTrackingKey/ValidationSetTracking and basic methods + - many: allow ignoring running apps for specific request + - tests: allow the searching test to fail under load + - overlord/snapstate: inhibit startup while unlinked + - seed/seedwriter/writer.go: check DevModeConfinement for dangerous + features + - tests/main/sudo-env: snap bin is available on Fedora + - boot, overlord/devicestate: list trusted and managed assets + upfront + - gadget, gadget/install: support for ubuntu-save, create one during + install if needed + - spread-shellcheck: temporary workaround for deadlock, drop + unnecessary test + - snap: support different exit-code in the snap command + - logger: use strutil.KernelCommandLineSplit in + debugEnabledOnKernelCmdline + - logger: fix snapd.debug=1 parsing + - overlord: increase refresh postpone limit to 14 days + - spread-shellcheck: use single thread pool executor + - gadget/install,secboot: add debug messages + - spread-shellcheck: speed up spread-shellcheck even more + - spread-shellcheck: process paths from arguments in parallel + - tests: tweak error from tests.cleanup + - spread: remove workaround for openSUSE go issue + - o/configstate: create /etc/sysctl.d when applying early config + defaults + - tests: new tests.backup tool + - tests: add tests.cleanup pop sub-command + - tests: migration of the main suite to snaps-state tool part 6 + - tests: fix journal-state test + - cmd/snap-bootstrap/initramfs-mounts: split off new helper for misc + recover files + - cmd/snap-bootstrap/initramfs-mounts: also copy /etc/machine-id for + same IP addr + - packaging/{ubuntu,debian}: add liblzo2-dev as a dependency for + building snapd + - boot, gadget, bootloader: observer preserves managed bootloader + configs + - tests/nested/manual: add uc20 grade signed cloud-init test + - o/snapstate/autorefresh.go: eliminate race when launching + autorefresh + - daemon,snapshotstate: do not return "size" from Import() + - daemon: limit reading from snapshot import to Content-Length + - many: set/expect Content-Length header when importing snapshots + - github: switch from ::set-env command to environment file + - tests: migration of the main suite to snaps-state tool part 5 + - client: cleanup the Client.raw* and Client.do* method families + - tests: moving main suite to snaps-state tool part 4 + - client,daemon,snap: use constant for snapshot content-type + - many: fix typos and repeated "the" + - secboot: fix tpm connection leak when it's not enabled + - many: scaffolding for snapshots import API + - run-checks: run spread-shellcheck too + - interfaces: update network-manager interface to allow + ObjectManager access from unconfined clients + - tests: move core and regression suites to snaps-state tool + - tests: moving interfaces tests to snaps-state tool + - gadget: preserve files when indicated by content change observer + - tests: moving smoke test suite and some tests from main suite to + snaps-state tool + - o/snapshotstate: pass set id to backend.Open, update tests + - asserts/snapasserts: introduce ValidationSets + - o/snapshotstate: improve allocation of new set IDs + - boot: look at the gadget for run mode bootloader when making the + system bootable + - cmd/snap: allow snap help vs --all to diverge purposefully + - usersession/userd: separate bus name ownership from defining + interfaces + - o/snapshotstate: set snapshot set id from its filename + - o/snapstate: move remove-related tests to snapstate_remove_test.go + - desktop/notification: switch ExpireTimeout to time.Duration + - desktop/notification: add unit tests + - snap: snap help output refresh + - tests/nested/manual/preseed: include a system-usernames snap when + preseeding + - tests: fix sudo-env test + - tests: fix nested core20 shellcheck bug + - tests/lib: move to new directory when restoring PWD, cleanup + unpacked unpacked snap directories + - desktop/notification: add bindings for FDO notifications + - dbustest: fix stale comment references + - many: move ManagedAssetsBootloader into TrustedAssetsBootloader, + drop former + - snap-repair: add uc20 support + - tests: print all the serial logs for the nested test + - o/snapstate/check_snap_test.go: mock osutil.Find{U,G}id to avoid + bug in test + - cmd/snap/auto-import: stop importing system user assertions from + initramfs mnts + - osutil/group.go: treat all non-nil errs from user.Lookup{Group,} + as Unknown* + - asserts: deserialize grouping only once in Pool.AddBatch if needed + - gadget: allow content observer to have opinions about a change + - tests: new snaps-state command - part1 + - o/assertstate: support refreshing any number of snap-declarations + - boot: use test helpers + - tests/core/snap-debug-bootvars: also check snap_mode + - many/apparmor: adjust rules for reading profile/ execing new + profiles for new kernel + - tests/core/snap-debug-bootvars: spread test for snap debug boot- + vars + - tests/lib/nested.sh: more little tweaks + - tests/nested/manual/grade-signed-above-testkeys-boot: enable kvm + - cmd/s-b/initramfs-mounts: use ConfigureTargetSystem for install, + recover modes + - overlord: explicitly set refresh-app-awareness in tests + - kernel: remove "edition" from kernel.yaml and add "update" + - spread: drop vendor from the packed project archive + - boot: fix debug bootloader variables dump on UC20 systems + - wrappers, systemd: allow empty root dir and conditionally do not + pass --root to systemctl + - tests/nested/manual: add test for grades above signed booting with + testkeys + - tests/nested: misc robustness fixes + - o/assertstate,asserts: use bulk refresh to refresh snap- + declarations + - tests/lib/prepare.sh: stop patching the uc20 initrd since it has + been updated now + - tests/nested/manual/refresh-revert-fundamentals: re-enable test + - update-pot: ignore .go files inside .git when running xgettext-go + - tests: disable part of the lxd test completely on 16.04. + - o/snapshotstate: tweak comment regarding snapshot filename + - o/snapstate: improve snapshot iteration + - bootloader: lk cleanups + - tests: update to support nested kvm without reboots on UC20 + - tests/nested/manual/preseed: disable system-key check for 20.04 + image + - spread.yaml: add ubuntu-20.10-64 to qemu + - store: handle v2 error when fetching assertions + - gadget: resolve device mapper devices for fallback device lookup + - tests/nested/cloud-init-many: simplify tests and unify + helpers/seed inputs + - tests: copy /usr/lib/snapd/info to correct directory + - check-pr-title.py * : allow "*" in the first part of the title + - many: typos and small test tweak + - tests/main/lxd: disable cgroup combination for 16.04 that is + failing a lot + - tests: make nested signing helpers less confusing + - tests: misc nested changes + - tests/nested/manual/refresh-revert-fundamentals: disable + temporarily + - tests/lib/cla_check: default to Python 3, tweaks, formatting + - tests/lib/cl_check.py: use python3 compatible code - -- Michael Vogt Mon, 19 Oct 2020 20:24:02 +0200 + -- Michael Vogt Thu, 19 Nov 2020 17:51:02 +0100 -snapd (2.47.1+20.10) groovy; urgency=medium +snapd (2.47.1) xenial; urgency=medium * New upstream release, LP: #1895929 - o/configstate: create /etc/sysctl.d when applying early config @@ -4517,7 +4822,7 @@ - logger: try to not have double dates - debian: use deb-systemd-invoke instead of systemctl directly - tests: run all main tests on core18 - - many: finish sharing a single TaskRunner with all the the managers + - many: finish sharing a single TaskRunner with all the managers - interfaces/repo: added AllHotplugInterfaces helper - snapstate: ensure kernel-track is honored on switch/refresh - overlord/ifacestate: support implicit slots on snapd diff -Nru snapd-2.47.1+20.10.1build1/debian/copyright snapd-2.48+21.04/debian/copyright --- snapd-2.47.1+20.10.1build1/debian/copyright 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/debian/copyright 2020-11-19 16:51:02.000000000 +0000 @@ -6,7 +6,7 @@ Copyright: Copyright (C) 2014,2015 Canonical, Ltd. License: GPL-3 This program is free software: you can redistribute it and/or modify it - under the terms of the the GNU General Public License version 3, as + 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 diff -Nru snapd-2.47.1+20.10.1build1/desktop/notification/caps.go snapd-2.48+21.04/desktop/notification/caps.go --- snapd-2.47.1+20.10.1build1/desktop/notification/caps.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48+21.04/desktop/notification/caps.go 2020-11-19 16:51:02.000000000 +0000 @@ -0,0 +1,67 @@ +// -*- 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 notification + +const ( + // ActionIconsCapability indicates that the server supports using icons + // instead of text for displaying actions. Using icons for actions must be + // enabled on a per-notification basis using the "action-icons" hint. + ActionIconsCapability ServerCapability = "action-icons" + // ActionsCapability indicates that the server will provide the specified + // actions to the user. Even if this cap is missing, actions may still be + // specified by the client, however the server is free to ignore them. + ActionsCapability ServerCapability = "actions" + // BodyCapability indicates that the server supports body text. Some + // implementations may only show the summary (for instance, onscreen + // displays, marquee/scrollers). + BodyCapability ServerCapability = "body" + // BodyHyperlinksCapability indicates that the server supports hyperlinks in + // the notifications. + BodyHyperlinksCapability ServerCapability = "body-hyperlinks" + // BodyImagesCapability indicates that the server supports images in the + // notifications. + BodyImagesCapability ServerCapability = "body-images" + // BodyMarkupCapability indicates that the server supports markup in the + // body text. If marked up text is sent to a server that does not give this + // cap, the markup will show through as regular text so must be stripped + // clientside. + BodyMarkupCapability ServerCapability = "body-markup" + // IconMultiCapability indicates that the server will render an animation of + // all the frames in a given image array. The client may still specify + // multiple frames even if this cap and/or "icon-static" is missing, however + // the server is free to ignore them and use only the primary frame. + IconMultiCapability ServerCapability = "icon-multi" + // IconStaticCapability indicates that the server supports display of + // exactly one frame of any given image array. This value is mutually + // exclusive with "icon-multi", it is a protocol error for the server to + // specify both. + IconStaticCapability ServerCapability = "icon-static" + // PersistenceCapability indicates that the server supports persistence of + // notifications. Notifications will be retained until they are acknowledged + // or removed by the user or recalled by the sender. The presence of this + // capability allows clients to depend on the server to ensure a + // notification is seen and eliminate the need for the client to display a + // reminding function (such as a status icon) of its own. + PersistenceCapability ServerCapability = "persistence" + // SoundCapability indicates that the server supports sounds on + // notifications. If returned, the server must support the "sound-file" and + // "suppress-sound" hints. + SoundCapability ServerCapability = "sound" +) diff -Nru snapd-2.47.1+20.10.1build1/desktop/notification/export_test.go snapd-2.48+21.04/desktop/notification/export_test.go --- snapd-2.47.1+20.10.1build1/desktop/notification/export_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48+21.04/desktop/notification/export_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -0,0 +1,24 @@ +// -*- 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 notification + +var ( + ProcessSignal = processSignal +) diff -Nru snapd-2.47.1+20.10.1build1/desktop/notification/fdo.go snapd-2.48+21.04/desktop/notification/fdo.go --- snapd-2.47.1+20.10.1build1/desktop/notification/fdo.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48+21.04/desktop/notification/fdo.go 2020-11-19 16:51:02.000000000 +0000 @@ -0,0 +1,199 @@ +// -*- 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 notification + +import ( + "context" + "fmt" + + "github.com/godbus/dbus" + + "github.com/snapcore/snapd/logger" +) + +const ( + dBusName = "org.freedesktop.Notifications" + dBusObjectPath = "/org/freedesktop/Notifications" + dBusInterfaceName = "org.freedesktop.Notifications" +) + +// Server holds a connection to a notification server interactions. +type Server struct { + conn *dbus.Conn + obj dbus.BusObject +} + +// New returns new connection to a freedesktop.org message notification server. +// +// Each server offers specific capabilities. It is advised to provide graceful +// degradation of functionality, depending on the supported capabilities, so +// that the notification messages are useful on a wide range of desktop +// environments. +func New(conn *dbus.Conn) *Server { + return &Server{ + conn: conn, + obj: conn.Object(dBusName, dBusObjectPath), + } +} + +// ServerInformation returns the information about the notification server. +func (srv *Server) ServerInformation() (name, vendor, version, specVersion string, err error) { + call := srv.obj.Call(dBusInterfaceName+".GetServerInformation", 0) + if err := call.Store(&name, &vendor, &version, &specVersion); err != nil { + return "", "", "", "", err + } + return name, vendor, version, specVersion, nil +} + +// ServerCapabilities returns the list of notification capabilities provided by the session. +func (srv *Server) ServerCapabilities() ([]ServerCapability, error) { + call := srv.obj.Call(dBusInterfaceName+".GetCapabilities", 0) + var caps []ServerCapability + if err := call.Store(&caps); err != nil { + return nil, err + } + return caps, nil +} + +// SendNotification sends a new notification or updates an existing +// notification. In both cases the ID of the notification, as assigned by the +// server, is returned. The ID can be used to cancel a notification, update it +// or react to invoked user actions. +func (srv *Server) SendNotification(msg *Message) (ID, error) { + call := srv.obj.Call(dBusInterfaceName+".Notify", 0, + msg.AppName, msg.ReplacesID, msg.Icon, msg.Summary, msg.Body, + flattenActions(msg.Actions), mapHints(msg.Hints), + int32(msg.ExpireTimeout.Nanoseconds()/1e6)) + var id ID + if err := call.Store(&id); err != nil { + return 0, err + } + return id, nil +} + +func flattenActions(actions []Action) []string { + result := make([]string, len(actions)*2) + for i, action := range actions { + result[i*2] = action.ActionKey + result[i*2+1] = action.LocalizedText + } + return result +} + +func mapHints(hints []Hint) map[string]dbus.Variant { + result := make(map[string]dbus.Variant, len(hints)) + for _, hint := range hints { + result[hint.Name] = dbus.MakeVariant(hint.Value) + } + return result +} + +// CloseNotification closes a notification message with the given ID. +func (srv *Server) CloseNotification(id ID) error { + call := srv.obj.Call(dBusInterfaceName+".CloseNotification", 0, id) + return call.Store() +} + +// ObserveNotifications blocks and processes message notification signals. +// +// The bus connection is configured to deliver signals from the notification +// server. All received signals are dispatched to the provided observer. This +// process continues until stopped by the context, or if an error occurs. +func (srv *Server) ObserveNotifications(ctx context.Context, observer Observer) (err error) { + // TODO: upgrade godbus and use un-buffered channel. + ch := make(chan *dbus.Signal, 10) + defer close(ch) + + srv.conn.Signal(ch) + defer srv.conn.RemoveSignal(ch) + + matchRules := []dbus.MatchOption{ + dbus.WithMatchSender(dBusName), + dbus.WithMatchObjectPath(dBusObjectPath), + dbus.WithMatchInterface(dBusInterfaceName), + } + if err := srv.conn.AddMatchSignal(matchRules...); err != nil { + return err + } + defer func() { + if err := srv.conn.RemoveMatchSignal(matchRules...); err != nil { + // XXX: this should not fail for us in practice but we don't want + // to clobber the actual error being returned from the function in + // general, so ignore RemoveMatchSignal errors and just log them + // instead. + logger.Noticef("Cannot remove D-Bus signal matcher: %v", err) + } + }() + + for { + select { + case <-ctx.Done(): + return ctx.Err() + case sig := <-ch: + if err := processSignal(sig, observer); err != nil { + return err + } + } + } +} + +func processSignal(sig *dbus.Signal, observer Observer) error { + switch sig.Name { + case dBusInterfaceName + ".NotificationClosed": + if err := processNotificationClosed(sig, observer); err != nil { + return fmt.Errorf("cannot process NotificationClosed signal: %v", err) + } + case dBusInterfaceName + ".ActionInvoked": + if err := processActionInvoked(sig, observer); err != nil { + return fmt.Errorf("cannot process ActionInvoked signal: %v", err) + } + } + return nil +} + +func processNotificationClosed(sig *dbus.Signal, observer Observer) error { + if len(sig.Body) != 2 { + return fmt.Errorf("unexpected number of body elements: %d", len(sig.Body)) + } + id, ok := sig.Body[0].(uint32) + if !ok { + return fmt.Errorf("expected first body element to be uint32, got %T", sig.Body[0]) + } + reason, ok := sig.Body[1].(uint32) + if !ok { + return fmt.Errorf("expected second body element to be uint32, got %T", sig.Body[1]) + } + return observer.NotificationClosed(ID(id), CloseReason(reason)) +} + +func processActionInvoked(sig *dbus.Signal, observer Observer) error { + if len(sig.Body) != 2 { + return fmt.Errorf("unexpected number of body elements: %d", len(sig.Body)) + } + id, ok := sig.Body[0].(uint32) + if !ok { + return fmt.Errorf("expected first body element to be uint32, got %T", sig.Body[0]) + } + actionKey, ok := sig.Body[1].(string) + if !ok { + return fmt.Errorf("expected second body element to be string, got %T", sig.Body[1]) + } + return observer.ActionInvoked(ID(id), actionKey) +} diff -Nru snapd-2.47.1+20.10.1build1/desktop/notification/fdo_test.go snapd-2.48+21.04/desktop/notification/fdo_test.go --- snapd-2.47.1+20.10.1build1/desktop/notification/fdo_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48+21.04/desktop/notification/fdo_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -0,0 +1,640 @@ +// -*- 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 notification_test + +import ( + "context" + "fmt" + "sync" + "time" + + "github.com/godbus/dbus" + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/dbusutil" + "github.com/snapcore/snapd/dbusutil/dbustest" + "github.com/snapcore/snapd/desktop/notification" + "github.com/snapcore/snapd/logger" + "github.com/snapcore/snapd/testutil" +) + +type fdoSuite struct { + testutil.BaseTest +} + +var _ = Suite(&fdoSuite{}) + +func (s *fdoSuite) connectWithHandler(c *C, handler dbustest.DBusHandlerFunc) *notification.Server { + conn, err := dbustest.Connection(handler) + c.Assert(err, IsNil) + restore := dbusutil.MockOnlySessionBusAvailable(conn) + s.AddCleanup(restore) + return notification.New(conn) +} + +func (s *fdoSuite) checkGetServerInformationRequest(c *C, msg *dbus.Message) { + c.Assert(msg.Type, Equals, dbus.TypeMethodCall) + c.Check(msg.Flags, Equals, dbus.Flags(0)) + c.Check(msg.Headers, DeepEquals, map[dbus.HeaderField]dbus.Variant{ + dbus.FieldDestination: dbus.MakeVariant("org.freedesktop.Notifications"), + dbus.FieldPath: dbus.MakeVariant(dbus.ObjectPath("/org/freedesktop/Notifications")), + dbus.FieldInterface: dbus.MakeVariant("org.freedesktop.Notifications"), + dbus.FieldMember: dbus.MakeVariant("GetServerInformation"), + }) + c.Check(msg.Body, HasLen, 0) +} + +func (s *fdoSuite) checkGetCapabilitiesRequest(c *C, msg *dbus.Message) { + c.Assert(msg.Type, Equals, dbus.TypeMethodCall) + c.Check(msg.Flags, Equals, dbus.Flags(0)) + c.Check(msg.Headers, DeepEquals, map[dbus.HeaderField]dbus.Variant{ + dbus.FieldDestination: dbus.MakeVariant("org.freedesktop.Notifications"), + dbus.FieldPath: dbus.MakeVariant(dbus.ObjectPath("/org/freedesktop/Notifications")), + dbus.FieldInterface: dbus.MakeVariant("org.freedesktop.Notifications"), + dbus.FieldMember: dbus.MakeVariant("GetCapabilities"), + }) + c.Check(msg.Body, HasLen, 0) +} + +func (s *fdoSuite) checkNotifyRequest(c *C, msg *dbus.Message) { + c.Assert(msg.Type, Equals, dbus.TypeMethodCall) + c.Check(msg.Flags, Equals, dbus.Flags(0)) + c.Check(msg.Headers, DeepEquals, map[dbus.HeaderField]dbus.Variant{ + dbus.FieldDestination: dbus.MakeVariant("org.freedesktop.Notifications"), + dbus.FieldPath: dbus.MakeVariant(dbus.ObjectPath("/org/freedesktop/Notifications")), + dbus.FieldInterface: dbus.MakeVariant("org.freedesktop.Notifications"), + dbus.FieldMember: dbus.MakeVariant("Notify"), + dbus.FieldSignature: dbus.MakeVariant(dbus.SignatureOf( + "", uint32(0), "", "", "", []string{}, map[string]dbus.Variant{}, int32(0), + )), + }) + c.Check(msg.Body, HasLen, 8) +} + +func (s *fdoSuite) checkCloseNotificationRequest(c *C, msg *dbus.Message) { + c.Assert(msg.Type, Equals, dbus.TypeMethodCall) + c.Check(msg.Flags, Equals, dbus.Flags(0)) + c.Check(msg.Headers, DeepEquals, map[dbus.HeaderField]dbus.Variant{ + dbus.FieldDestination: dbus.MakeVariant("org.freedesktop.Notifications"), + dbus.FieldPath: dbus.MakeVariant(dbus.ObjectPath("/org/freedesktop/Notifications")), + dbus.FieldInterface: dbus.MakeVariant("org.freedesktop.Notifications"), + dbus.FieldMember: dbus.MakeVariant("CloseNotification"), + dbus.FieldSignature: dbus.MakeVariant(dbus.SignatureOf(uint32(0))), + }) + c.Check(msg.Body, HasLen, 1) +} + +func (s *fdoSuite) nameHasNoOwnerResponse(c *C, msg *dbus.Message) *dbus.Message { + return &dbus.Message{ + Type: dbus.TypeError, + Headers: map[dbus.HeaderField]dbus.Variant{ + dbus.FieldReplySerial: dbus.MakeVariant(msg.Serial()), + dbus.FieldSender: dbus.MakeVariant(":1"), // This does not matter. + // dbus.FieldDestination is provided automatically by DBus test helper. + dbus.FieldErrorName: dbus.MakeVariant("org.freedesktop.DBus.Error.NameHasNoOwner"), + }, + } +} + +func (s *fdoSuite) TestServerInformationSuccess(c *C) { + srv := s.connectWithHandler(c, func(msg *dbus.Message, n int) ([]*dbus.Message, error) { + switch n { + case 0: + s.checkGetServerInformationRequest(c, msg) + responseSig := dbus.SignatureOf("", "", "", "") + response := &dbus.Message{ + Type: dbus.TypeMethodReply, + Headers: map[dbus.HeaderField]dbus.Variant{ + dbus.FieldReplySerial: dbus.MakeVariant(msg.Serial()), + dbus.FieldSender: dbus.MakeVariant(":1"), // This does not matter. + // dbus.FieldDestination is provided automatically by DBus test helper. + dbus.FieldSignature: dbus.MakeVariant(responseSig), + }, + Body: []interface{}{"name", "vendor", "version", "specVersion"}, + } + return []*dbus.Message{response}, nil + } + return nil, fmt.Errorf("unexpected message #%d: %s", n, msg) + }) + name, vendor, version, specVersion, err := srv.ServerInformation() + c.Assert(err, IsNil) + c.Check(name, Equals, "name") + c.Check(vendor, Equals, "vendor") + c.Check(version, Equals, "version") + c.Check(specVersion, Equals, "specVersion") +} + +func (s *fdoSuite) TestServerInformationError(c *C) { + srv := s.connectWithHandler(c, func(msg *dbus.Message, n int) ([]*dbus.Message, error) { + switch n { + case 0: + s.checkGetServerInformationRequest(c, msg) + response := s.nameHasNoOwnerResponse(c, msg) + return []*dbus.Message{response}, nil + } + return nil, fmt.Errorf("unexpected message #%d: %s", n, msg) + }) + _, _, _, _, err := srv.ServerInformation() + c.Assert(err, ErrorMatches, "org.freedesktop.DBus.Error.NameHasNoOwner") +} + +func (s *fdoSuite) TestServerCapabilitiesSuccess(c *C) { + srv := s.connectWithHandler(c, func(msg *dbus.Message, n int) ([]*dbus.Message, error) { + switch n { + case 0: + s.checkGetCapabilitiesRequest(c, msg) + responseSig := dbus.SignatureOf([]string{}) + response := &dbus.Message{ + Type: dbus.TypeMethodReply, + Headers: map[dbus.HeaderField]dbus.Variant{ + dbus.FieldReplySerial: dbus.MakeVariant(msg.Serial()), + dbus.FieldSender: dbus.MakeVariant(":1"), // This does not matter. + // dbus.FieldDestination is provided automatically by DBus test helper. + dbus.FieldSignature: dbus.MakeVariant(responseSig), + }, + Body: []interface{}{ + []string{"cap-foo", "cap-bar"}, + }, + } + return []*dbus.Message{response}, nil + } + return nil, fmt.Errorf("unexpected message #%d: %s", n, msg) + }) + caps, err := srv.ServerCapabilities() + c.Assert(err, IsNil) + c.Check(caps, DeepEquals, []notification.ServerCapability{"cap-foo", "cap-bar"}) +} + +func (s *fdoSuite) TestServerCapabilitiesError(c *C) { + srv := s.connectWithHandler(c, func(msg *dbus.Message, n int) ([]*dbus.Message, error) { + switch n { + case 0: + s.checkGetCapabilitiesRequest(c, msg) + response := s.nameHasNoOwnerResponse(c, msg) + return []*dbus.Message{response}, nil + } + return nil, fmt.Errorf("unexpected message #%d: %s", n, msg) + }) + _, err := srv.ServerCapabilities() + c.Assert(err, ErrorMatches, "org.freedesktop.DBus.Error.NameHasNoOwner") +} + +func (s *fdoSuite) TestSendNotificationSuccess(c *C) { + srv := s.connectWithHandler(c, func(msg *dbus.Message, n int) ([]*dbus.Message, error) { + switch n { + case 0: + s.checkNotifyRequest(c, msg) + c.Check(msg.Body[0], Equals, "app-name") + c.Check(msg.Body[1], Equals, uint32(42)) + c.Check(msg.Body[2], Equals, "icon") + c.Check(msg.Body[3], Equals, "summary") + c.Check(msg.Body[4], Equals, "body") + c.Check(msg.Body[5], DeepEquals, []string{"key-1", "text-1", "key-2", "text-2"}) + c.Check(msg.Body[6], DeepEquals, map[string]dbus.Variant{ + "hint-str": dbus.MakeVariant("str"), + "hint-bool": dbus.MakeVariant(true), + }) + c.Check(msg.Body[7], Equals, int32(1000)) + responseSig := dbus.SignatureOf(uint32(0)) + response := &dbus.Message{ + Type: dbus.TypeMethodReply, + Headers: map[dbus.HeaderField]dbus.Variant{ + dbus.FieldReplySerial: dbus.MakeVariant(msg.Serial()), + dbus.FieldSender: dbus.MakeVariant(":1"), // This does not matter. + // dbus.FieldDestination is provided automatically by DBus test helper. + dbus.FieldSignature: dbus.MakeVariant(responseSig), + }, + Body: []interface{}{uint32(7)}, + } + return []*dbus.Message{response}, nil + } + return nil, fmt.Errorf("unexpected message #%d: %s", n, msg) + }) + id, err := srv.SendNotification(¬ification.Message{ + AppName: "app-name", + Icon: "icon", + Summary: "summary", + Body: "body", + ExpireTimeout: time.Second * 1, + ReplacesID: notification.ID(42), + Actions: []notification.Action{ + {ActionKey: "key-1", LocalizedText: "text-1"}, + {ActionKey: "key-2", LocalizedText: "text-2"}, + }, + Hints: []notification.Hint{ + {Name: "hint-str", Value: "str"}, + {Name: "hint-bool", Value: true}, + }, + }) + c.Assert(err, IsNil) + c.Check(id, Equals, notification.ID(7)) +} + +func (s *fdoSuite) TestSendNotificationWithServerDecitedExpireTimeout(c *C) { + srv := s.connectWithHandler(c, func(msg *dbus.Message, n int) ([]*dbus.Message, error) { + switch n { + case 0: + s.checkNotifyRequest(c, msg) + c.Check(msg.Body[7], Equals, int32(-1)) + responseSig := dbus.SignatureOf(uint32(0)) + response := &dbus.Message{ + Type: dbus.TypeMethodReply, + Headers: map[dbus.HeaderField]dbus.Variant{ + dbus.FieldReplySerial: dbus.MakeVariant(msg.Serial()), + dbus.FieldSender: dbus.MakeVariant(":1"), // This does not matter. + // dbus.FieldDestination is provided automatically by DBus test helper. + dbus.FieldSignature: dbus.MakeVariant(responseSig), + }, + Body: []interface{}{uint32(7)}, + } + return []*dbus.Message{response}, nil + } + return nil, fmt.Errorf("unexpected message #%d: %s", n, msg) + }) + id, err := srv.SendNotification(¬ification.Message{ + ExpireTimeout: notification.ServerSelectedExpireTimeout, + }) + c.Assert(err, IsNil) + c.Check(id, Equals, notification.ID(7)) +} + +func (s *fdoSuite) TestSendNotificationError(c *C) { + srv := s.connectWithHandler(c, func(msg *dbus.Message, n int) ([]*dbus.Message, error) { + switch n { + case 0: + s.checkNotifyRequest(c, msg) + response := s.nameHasNoOwnerResponse(c, msg) + return []*dbus.Message{response}, nil + } + return nil, fmt.Errorf("unexpected message #%d: %s", n, msg) + }) + _, err := srv.SendNotification(¬ification.Message{}) + c.Assert(err, ErrorMatches, "org.freedesktop.DBus.Error.NameHasNoOwner") +} + +func (s *fdoSuite) TestCloseNotificationSuccess(c *C) { + srv := s.connectWithHandler(c, func(msg *dbus.Message, n int) ([]*dbus.Message, error) { + switch n { + case 0: + s.checkCloseNotificationRequest(c, msg) + c.Check(msg.Body[0], Equals, uint32(42)) + response := &dbus.Message{ + Type: dbus.TypeMethodReply, + Headers: map[dbus.HeaderField]dbus.Variant{ + dbus.FieldReplySerial: dbus.MakeVariant(msg.Serial()), + dbus.FieldSender: dbus.MakeVariant(":1"), // This does not matter. + // dbus.FieldDestination is provided automatically by DBus test helper. + }, + } + return []*dbus.Message{response}, nil + } + return nil, fmt.Errorf("unexpected message #%d: %s", n, msg) + }) + err := srv.CloseNotification(notification.ID(42)) + c.Assert(err, IsNil) +} + +func (s *fdoSuite) TestCloseNotificationError(c *C) { + srv := s.connectWithHandler(c, func(msg *dbus.Message, n int) ([]*dbus.Message, error) { + switch n { + case 0: + s.checkCloseNotificationRequest(c, msg) + response := s.nameHasNoOwnerResponse(c, msg) + return []*dbus.Message{response}, nil + } + return nil, fmt.Errorf("unexpected message #%d: %s", n, msg) + }) + err := srv.CloseNotification(notification.ID(42)) + c.Assert(err, ErrorMatches, "org.freedesktop.DBus.Error.NameHasNoOwner") +} + +type testObserver struct { + notificationClosed func(notification.ID, notification.CloseReason) error + actionInvoked func(notification.ID, string) error +} + +func (o *testObserver) NotificationClosed(id notification.ID, reason notification.CloseReason) error { + if o.notificationClosed != nil { + return o.notificationClosed(id, reason) + } + return nil +} + +func (o *testObserver) ActionInvoked(id notification.ID, actionKey string) error { + if o.actionInvoked != nil { + return o.actionInvoked(id, actionKey) + } + return nil +} + +func (s *fdoSuite) checkAddMatchRequest(c *C, msg *dbus.Message) { + c.Assert(msg.Type, Equals, dbus.TypeMethodCall) + c.Check(msg.Flags, Equals, dbus.Flags(0)) + c.Check(msg.Headers, DeepEquals, map[dbus.HeaderField]dbus.Variant{ + dbus.FieldDestination: dbus.MakeVariant("org.freedesktop.DBus"), + dbus.FieldPath: dbus.MakeVariant(dbus.ObjectPath("/org/freedesktop/DBus")), + dbus.FieldInterface: dbus.MakeVariant("org.freedesktop.DBus"), + dbus.FieldMember: dbus.MakeVariant("AddMatch"), + dbus.FieldSignature: dbus.MakeVariant(dbus.SignatureOf("")), + }) +} + +func (s *fdoSuite) checkRemoveMatchRequest(c *C, msg *dbus.Message) { + c.Assert(msg.Type, Equals, dbus.TypeMethodCall) + c.Check(msg.Flags, Equals, dbus.Flags(0)) + c.Check(msg.Headers, DeepEquals, map[dbus.HeaderField]dbus.Variant{ + dbus.FieldDestination: dbus.MakeVariant("org.freedesktop.DBus"), + dbus.FieldPath: dbus.MakeVariant(dbus.ObjectPath("/org/freedesktop/DBus")), + dbus.FieldInterface: dbus.MakeVariant("org.freedesktop.DBus"), + dbus.FieldMember: dbus.MakeVariant("RemoveMatch"), + dbus.FieldSignature: dbus.MakeVariant(dbus.SignatureOf("")), + }) +} + +func (s *fdoSuite) addMatchResponse(c *C, msg *dbus.Message) *dbus.Message { + return &dbus.Message{ + Type: dbus.TypeMethodReply, + Headers: map[dbus.HeaderField]dbus.Variant{ + dbus.FieldReplySerial: dbus.MakeVariant(msg.Serial()), + dbus.FieldSender: dbus.MakeVariant(":1"), // This does not matter. + // dbus.FieldDestination is provided automatically by DBus test helper. + }, + } +} + +func (s *fdoSuite) removeMatchResponse(c *C, msg *dbus.Message) *dbus.Message { + return &dbus.Message{ + Type: dbus.TypeMethodReply, + Headers: map[dbus.HeaderField]dbus.Variant{ + dbus.FieldReplySerial: dbus.MakeVariant(msg.Serial()), + dbus.FieldSender: dbus.MakeVariant(":1"), // This does not matter. + // dbus.FieldDestination is provided automatically by DBus test helper. + }, + } +} + +func (s *fdoSuite) TestObserveNotificationsContextAndSignalWatch(c *C) { + ctx, cancel := context.WithCancel(context.TODO()) + msgsSeen := 0 + addMatchSeen := make(chan struct{}, 1) + defer close(addMatchSeen) + srv := s.connectWithHandler(c, func(msg *dbus.Message, n int) ([]*dbus.Message, error) { + msgsSeen++ + switch n { + case 0: + s.checkAddMatchRequest(c, msg) + c.Check(msg.Body, HasLen, 1) + c.Check(msg.Body[0], Equals, "type='signal',sender='org.freedesktop.Notifications',path='/org/freedesktop/Notifications',interface='org.freedesktop.Notifications'") + response := s.addMatchResponse(c, msg) + addMatchSeen <- struct{}{} + return []*dbus.Message{response}, nil + case 1: + s.checkRemoveMatchRequest(c, msg) + c.Check(msg.Body, HasLen, 1) + c.Check(msg.Body[0], Equals, "type='signal',sender='org.freedesktop.Notifications',path='/org/freedesktop/Notifications',interface='org.freedesktop.Notifications'") + response := s.removeMatchResponse(c, msg) + return []*dbus.Message{response}, nil + default: + return nil, fmt.Errorf("unexpected message #%d: %s", n, msg) + } + }) + + var wg sync.WaitGroup + wg.Add(1) + go func() { + err := srv.ObserveNotifications(ctx, &testObserver{}) + c.Assert(err, ErrorMatches, "context canceled") + wg.Done() + }() + // Wait for the signal that we saw the AddMatch message and then stop. + <-addMatchSeen + cancel() + // Wait for ObserveNotifications to return + wg.Wait() + c.Check(msgsSeen, Equals, 2) +} + +func (s *fdoSuite) TestObserveNotificationsAddWatchError(c *C) { + srv := s.connectWithHandler(c, func(msg *dbus.Message, n int) ([]*dbus.Message, error) { + switch n { + case 0: + s.checkAddMatchRequest(c, msg) + response := s.nameHasNoOwnerResponse(c, msg) + return []*dbus.Message{response}, nil + default: + return nil, fmt.Errorf("unexpected message #%d: %s", n, msg) + } + }) + err := srv.ObserveNotifications(context.TODO(), &testObserver{}) + c.Assert(err, ErrorMatches, "org.freedesktop.DBus.Error.NameHasNoOwner") +} + +func (s *fdoSuite) TestObserveNotificationsRemoveWatchError(c *C) { + logBuffer, restore := logger.MockLogger() + defer restore() + + ctx, cancel := context.WithCancel(context.TODO()) + msgsSeen := 0 + addMatchSeen := make(chan struct{}, 1) + defer close(addMatchSeen) + srv := s.connectWithHandler(c, func(msg *dbus.Message, n int) ([]*dbus.Message, error) { + msgsSeen++ + switch n { + case 0: + s.checkAddMatchRequest(c, msg) + response := s.addMatchResponse(c, msg) + addMatchSeen <- struct{}{} + return []*dbus.Message{response}, nil + case 1: + s.checkRemoveMatchRequest(c, msg) + response := s.nameHasNoOwnerResponse(c, msg) + return []*dbus.Message{response}, nil + default: + return nil, fmt.Errorf("unexpected message #%d: %s", n, msg) + } + }) + + var wg sync.WaitGroup + wg.Add(1) + go func() { + err := srv.ObserveNotifications(ctx, &testObserver{}) + // The error from RemoveWatch is not clobbering the return value of ObserveNotifications. + c.Assert(err, ErrorMatches, "context canceled") + c.Check(logBuffer.String(), testutil.Contains, "Cannot remove D-Bus signal matcher: org.freedesktop.DBus.Error.NameHasNoOwner\n") + wg.Done() + }() + // Wait for the signal that we saw the AddMatch message and then stop. + <-addMatchSeen + cancel() + // Wait for ObserveNotifications to return + wg.Wait() + c.Check(msgsSeen, Equals, 2) +} + +func (s *fdoSuite) TestObserveNotificationsProcessingError(c *C) { + msgsSeen := 0 + srv := s.connectWithHandler(c, func(msg *dbus.Message, n int) ([]*dbus.Message, error) { + msgsSeen++ + switch n { + case 0: + s.checkAddMatchRequest(c, msg) + response := s.addMatchResponse(c, msg) + sig := &dbus.Message{ + Type: dbus.TypeSignal, + Headers: map[dbus.HeaderField]dbus.Variant{ + dbus.FieldPath: dbus.MakeVariant(dbus.ObjectPath("/org/freedesktop/Notifications")), + dbus.FieldInterface: dbus.MakeVariant("org.freedesktop.Notifications"), + dbus.FieldMember: dbus.MakeVariant("ActionInvoked"), + dbus.FieldSender: dbus.MakeVariant("org.freedesktop.Notifications"), + dbus.FieldSignature: dbus.MakeVariant(dbus.SignatureOf(uint32(0), "")), + }, + Body: []interface{}{uint32(42), "action-key"}, + } + // Send the DBus response for the method call and an additional signal. + return []*dbus.Message{response, sig}, nil + case 1: + s.checkRemoveMatchRequest(c, msg) + response := s.removeMatchResponse(c, msg) + return []*dbus.Message{response}, nil + default: + return nil, fmt.Errorf("unexpected message #%d: %s", n, msg) + } + }) + err := srv.ObserveNotifications(context.TODO(), &testObserver{ + actionInvoked: func(id notification.ID, actionKey string) error { + c.Check(id, Equals, notification.ID(42)) + c.Check(actionKey, Equals, "action-key") + return fmt.Errorf("boom") + }, + }) + c.Assert(err, ErrorMatches, "cannot process ActionInvoked signal: boom") + c.Check(msgsSeen, Equals, 2) +} + +func (s *fdoSuite) TestProcessActionInvokedSignalSuccess(c *C) { + called := false + err := notification.ProcessSignal(&dbus.Signal{ + // Sender and Path are not used + Name: "org.freedesktop.Notifications.ActionInvoked", + Body: []interface{}{uint32(42), "action-key"}, + }, &testObserver{ + actionInvoked: func(id notification.ID, actionKey string) error { + called = true + c.Check(id, Equals, notification.ID(42)) + c.Check(actionKey, Equals, "action-key") + return nil + }, + }) + c.Assert(err, IsNil) + c.Assert(called, Equals, true) +} + +func (s *fdoSuite) TestProcessActionInvokedSignalError(c *C) { + err := notification.ProcessSignal(&dbus.Signal{ + Name: "org.freedesktop.Notifications.ActionInvoked", + Body: []interface{}{uint32(42), "action-key"}, + }, &testObserver{ + actionInvoked: func(id notification.ID, actionKey string) error { + return fmt.Errorf("boom") + }, + }) + c.Assert(err, ErrorMatches, "cannot process ActionInvoked signal: boom") +} + +func (s *fdoSuite) TestProcessActionInvokedSignalBodyParseErrors(c *C) { + err := notification.ProcessSignal(&dbus.Signal{ + Name: "org.freedesktop.Notifications.ActionInvoked", + Body: []interface{}{uint32(42), "action-key", "unexpected"}, + }, &testObserver{}) + c.Assert(err, ErrorMatches, "cannot process ActionInvoked signal: unexpected number of body elements: 3") + + err = notification.ProcessSignal(&dbus.Signal{ + Name: "org.freedesktop.Notifications.ActionInvoked", + Body: []interface{}{uint32(42)}, + }, &testObserver{}) + c.Assert(err, ErrorMatches, "cannot process ActionInvoked signal: unexpected number of body elements: 1") + + err = notification.ProcessSignal(&dbus.Signal{ + Name: "org.freedesktop.Notifications.ActionInvoked", + Body: []interface{}{uint32(42), true}, + }, &testObserver{}) + c.Assert(err, ErrorMatches, "cannot process ActionInvoked signal: expected second body element to be string, got bool") + + err = notification.ProcessSignal(&dbus.Signal{ + Name: "org.freedesktop.Notifications.ActionInvoked", + Body: []interface{}{true, "action-key"}, + }, &testObserver{}) + c.Assert(err, ErrorMatches, "cannot process ActionInvoked signal: expected first body element to be uint32, got bool") +} + +func (s *fdoSuite) TestProcessNotificationClosedSignalSuccess(c *C) { + called := false + err := notification.ProcessSignal(&dbus.Signal{ + Name: "org.freedesktop.Notifications.NotificationClosed", + Body: []interface{}{uint32(42), uint32(2)}, + }, &testObserver{ + notificationClosed: func(id notification.ID, reason notification.CloseReason) error { + called = true + c.Check(id, Equals, notification.ID(42)) + c.Check(reason, Equals, notification.CloseReason(2)) + return nil + }, + }) + c.Assert(err, IsNil) + c.Assert(called, Equals, true) +} + +func (s *fdoSuite) TestProcessNotificationClosedSignalError(c *C) { + err := notification.ProcessSignal(&dbus.Signal{ + Name: "org.freedesktop.Notifications.NotificationClosed", + Body: []interface{}{uint32(42), uint32(2)}, + }, &testObserver{ + notificationClosed: func(id notification.ID, reason notification.CloseReason) error { + return fmt.Errorf("boom") + }, + }) + c.Assert(err, ErrorMatches, "cannot process NotificationClosed signal: boom") +} + +func (s *fdoSuite) TestProcessNotificationClosedSignalBodyParseErrors(c *C) { + err := notification.ProcessSignal(&dbus.Signal{ + Name: "org.freedesktop.Notifications.NotificationClosed", + Body: []interface{}{uint32(42), uint32(2), "unexpected"}, + }, &testObserver{}) + c.Assert(err, ErrorMatches, "cannot process NotificationClosed signal: unexpected number of body elements: 3") + + err = notification.ProcessSignal(&dbus.Signal{ + Name: "org.freedesktop.Notifications.NotificationClosed", + Body: []interface{}{uint32(42)}, + }, &testObserver{}) + c.Assert(err, ErrorMatches, "cannot process NotificationClosed signal: unexpected number of body elements: 1") + + err = notification.ProcessSignal(&dbus.Signal{ + Name: "org.freedesktop.Notifications.NotificationClosed", + Body: []interface{}{uint32(42), true}, + }, &testObserver{}) + c.Assert(err, ErrorMatches, "cannot process NotificationClosed signal: expected second body element to be uint32, got bool") + + err = notification.ProcessSignal(&dbus.Signal{ + Name: "org.freedesktop.Notifications.NotificationClosed", + Body: []interface{}{true, uint32(2)}, + }, &testObserver{}) + c.Assert(err, ErrorMatches, "cannot process NotificationClosed signal: expected first body element to be uint32, got bool") +} diff -Nru snapd-2.47.1+20.10.1build1/desktop/notification/hints.go snapd-2.48+21.04/desktop/notification/hints.go --- snapd-2.47.1+20.10.1build1/desktop/notification/hints.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48+21.04/desktop/notification/hints.go 2020-11-19 16:51:02.000000000 +0000 @@ -0,0 +1,206 @@ +// -*- 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 notification + +import "fmt" + +// WithActionIcons returns a hint asking the server to use action key as icon names. +// +// A server that has the "action-icons" capability will attempt to interpret any +// action key as a named icon. The localized display name will be used to +// annotate the icon for accessibility purposes. The icon name should be +// compliant with the Freedesktop.org Icon Naming Specification. +// +// Requires server version >= 1.2 +func WithActionIcons() Hint { + t := true + return Hint{Name: "action-icons", Value: &t} +} + +// Urgency describes the importance of a notification message. +// +// Specification: https://developer.gnome.org/notification-spec/#urgency-levels +type Urgency byte + +const ( + // LowUrgency indicates that a notification message is below normal priority. + LowUrgency Urgency = 0 + // NormalUrgency indicates that a notification message has the regular priority. + NormalUrgency Urgency = 1 + // CriticalUrgency indicates that a notification message is above normal priority. + CriticalUrgency Urgency = 2 +) + +// String implements the Stringer interface. +func (u Urgency) String() string { + switch u { + case LowUrgency: + return "low" + case NormalUrgency: + return "normal" + case CriticalUrgency: + return "critical" + default: + return fmt.Sprintf("Urgency(%d)", byte(u)) + } +} + +// WithUrgency returns a hint asking the server to set message urgency. +// +// Notification servers may show messages with higher urgency before messages +// with lower urgency. In addition some urgency levels may not be shown when the +// user has enabled a do-not-distrub mode. +func WithUrgency(u Urgency) Hint { + return Hint{Name: "urgency", Value: &u} +} + +// Category is a string indicating the category of a notification message. +// +// Specification: https://developer.gnome.org/notification-spec/#categories +type Category string + +const ( + // DeviceCategory is a generic notification category related to hardware devices. + DeviceCategory Category = "device" + // DeviceAddedCategory indicates that a device was added to the system. + DeviceAddedCategory Category = "device.added" + // DeviceErrorCategory indicates that a device error occurred. + DeviceErrorCategory Category = "device.error" + // DeviceRemovedCategory indicates that a device was removed from the system. + DeviceRemovedCategory Category = "device.removed" + + // EmailCategory is a generic notification category related to electronic mail. + EmailCategory Category = "email" + //EmailArrivedCategory indicates that an e-mail has arrived. + EmailArrivedCategory Category = "email.arrived" + // EmailBouncedCategory indicates that an e-mail message has bounced. + EmailBouncedCategory Category = "email.bounced" + + // InstantMessageCategory is a generic notification category related to instant messages. + InstantMessageCategory Category = "im" + // InstantMessageErrorCategory indicates that an instant message error occurred. + InstantMessageErrorCategory Category = "im.error" + // InstantMessageReceivedCategory indicates that an instant mesage has been received. + InstantMessageReceivedCategory Category = "im.received" + + // NetworkCategory is a generic notification category related to network. + NetworkCategory Category = "network" + // NetworkConnectedCategory indicates that a network connection has been established. + NetworkConnectedCategory Category = "network.connected" + // NetworkDisconnectedCategory indicates that a network connection has been lost. + NetworkDisconnectedCategory Category = "network.disconnected" + // NetworkErrorCategory indicates that a network error occurred. + NetworkErrorCategory Category = "network.error" + + // PresenceCategory is a generic notification category related to on-line presence. + PresenceCategory Category = "presence" + // PresenceOfflineCategory indicates that a contact disconnected from the network. + PresenceOfflineCategory Category = "presence.offline" + // PresenceOnlineCategory indicates that a contact connected to the network. + PresenceOnlineCategory Category = "presence.online" + + // TransferCategory is a generic notification category for file transfers or downloads. + TransferCategory Category = "transfer" + // TransferCompleteCategory indicates that a file transfer has completed. + TransferCompleteCategory Category = "transfer.complete" + // TransferErrorCategory indicates that a file transfer error occurred. + TransferErrorCategory Category = "transfer.error" +) + +// WithCategory returns a hint asking the server to set message category. +func WithCategory(c Category) Hint { + return Hint{Name: "category", Value: &c} +} + +// WithDesktopEntry returns a hint asking the server to associate a desktop file with a message. +// +// The desktopEntryName is the name of the desktop file without the ".desktop" +// extension. The server may use this information to derive correct icon, for +// logging, etc. +func WithDesktopEntry(desktopEntryName string) Hint { + return Hint{Name: "desktop-entry", Value: &desktopEntryName} +} + +// WithTransient returns a hint asking the server to bypass message persistence. +// +// When set the server will treat the notification as transient and by-pass the +// server's persistence capability, if it should exist. +// +// Requires server version >= 1.2 +func WithTransient() Hint { + t := true + return Hint{Name: "transient", Value: &t} +} + +// WithResident returns a hint asking the server to keep the message after an action is invoked. +// +// When set the server will not automatically remove the notification when an +// action has been invoked. The notification will remain resident in the server +// until it is explicitly removed by the user or by the sender. This hint is +// likely only useful when the server has the "persistence" capability. +// +// Requires server version >= 1.2 +func WithResident() Hint { + t := true + return Hint{Name: "resident", Value: &t} +} + +// WithPointToX returns a hint asking the server to point the notification at a specific X coordinate. +// +// The coordinate is in desktop pixel units. Both X and Y hints must be included in the message. +func WithPointToX(x int) Hint { + return Hint{Name: "x", Value: &x} +} + +// WithPointToY returns a hint asking the server to point the notification at a specific Y coordinate. +// +// The coordinate is in desktop pixel units. Both X and Y hints must be included in the message. +func WithPointToY(y int) Hint { + return Hint{Name: "y", Value: &y} +} + +// WithImageFile returns a hint asking the server display an image loaded from file. +// +// When multiple hits related to images are used, the following priority list applies: +// 1) "image-data" (not implemented in Go). +// 2) "image-path", as provided by WithImageFile. +// 3) Message.Icon field. +func WithImageFile(path string) Hint { + // The hint name is image-path but the function is called WithImageFile for consistency with WithSoundFile. + return Hint{Name: "image-path", Value: &path} +} + +// TODO: add WithImageData + +// WithSoundFile returns a hint asking the server to play a sound loaded from file. +func WithSoundFile(path string) Hint { + return Hint{Name: "sound-file", Value: &path} +} + +// WithSoundName returns a hint asking the server to play a sound from the sound theme. +func WithSoundName(name string) Hint { + return Hint{Name: "sound-name", Value: &name} +} + +// WithSuppressSound returns a hint asking the server not to play any notification sounds. +func WithSuppressSound() Hint { + t := true + return Hint{Name: "suppress-sound", Value: &t} +} diff -Nru snapd-2.47.1+20.10.1build1/desktop/notification/hints_test.go snapd-2.48+21.04/desktop/notification/hints_test.go --- snapd-2.47.1+20.10.1build1/desktop/notification/hints_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48+21.04/desktop/notification/hints_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -0,0 +1,101 @@ +// -*- 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 notification_test + +import ( + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/desktop/notification" +) + +type hintsSuite struct{} + +var _ = Suite(&hintsSuite{}) + +func (s *hintsSuite) TestWithActionIcons(c *C) { + val := true + c.Check(notification.WithActionIcons(), DeepEquals, notification.Hint{Name: "action-icons", Value: &val}) +} + +func (s *hintsSuite) TestWithUrgency(c *C) { + val := notification.LowUrgency + c.Check(notification.WithUrgency(val), DeepEquals, notification.Hint{Name: "urgency", Value: &val}) +} + +func (s *hintsSuite) TestWithCategory(c *C) { + val := notification.DeviceCategory + c.Check(notification.WithCategory(val), DeepEquals, notification.Hint{Name: "category", Value: &val}) +} + +func (s *hintsSuite) TestWithDesktopEntry(c *C) { + val := "desktop-name" + c.Check(notification.WithDesktopEntry(val), DeepEquals, notification.Hint{Name: "desktop-entry", Value: &val}) +} + +func (s *hintsSuite) TestWithTransient(c *C) { + val := true + c.Check(notification.WithTransient(), DeepEquals, notification.Hint{Name: "transient", Value: &val}) +} + +func (s *hintsSuite) TestWithResident(c *C) { + val := true + c.Check(notification.WithResident(), DeepEquals, notification.Hint{Name: "resident", Value: &val}) +} + +func (s *hintsSuite) TestWithPointToX(c *C) { + val := 10 + c.Check(notification.WithPointToX(val), DeepEquals, notification.Hint{Name: "x", Value: &val}) +} + +func (s *hintsSuite) TestWithPointToY(c *C) { + val := 10 + c.Check(notification.WithPointToY(val), DeepEquals, notification.Hint{Name: "y", Value: &val}) +} + +func (s *hintsSuite) TestWithImageFile(c *C) { + val := "/path/to/img" + c.Check(notification.WithImageFile(val), DeepEquals, notification.Hint{Name: "image-path", Value: &val}) +} + +func (s *hintsSuite) TestWithSoundFile(c *C) { + val := "/path/to/snd" + c.Check(notification.WithSoundFile(val), DeepEquals, notification.Hint{Name: "sound-file", Value: &val}) +} + +func (s *hintsSuite) TestWithSoundName(c *C) { + val := "sound" + c.Check(notification.WithSoundName(val), DeepEquals, notification.Hint{Name: "sound-name", Value: &val}) +} + +func (s *hintsSuite) TestWithSuppressSound(c *C) { + val := true + c.Check(notification.WithSuppressSound(), DeepEquals, notification.Hint{Name: "suppress-sound", Value: &val}) +} + +type urgencySuite struct{} + +var _ = Suite(&urgencySuite{}) + +func (s *urgencySuite) TestString(c *C) { + c.Check(notification.LowUrgency.String(), Equals, "low") + c.Check(notification.NormalUrgency.String(), Equals, "normal") + c.Check(notification.CriticalUrgency.String(), Equals, "critical") + c.Check(notification.Urgency(13).String(), Equals, "Urgency(13)") +} diff -Nru snapd-2.47.1+20.10.1build1/desktop/notification/notify.go snapd-2.48+21.04/desktop/notification/notify.go --- snapd-2.47.1+20.10.1build1/desktop/notification/notify.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48+21.04/desktop/notification/notify.go 2020-11-19 16:51:02.000000000 +0000 @@ -0,0 +1,152 @@ +// -*- 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 notification implements bindings to D-Bus notification +// specification, version 1.2, as documented at https://developer.gnome.org/notification-spec/ +package notification + +import ( + "fmt" + "time" +) + +// Message describes a single notification message. +// +// The notification should be related to a specific application, as indicated by +// AppName. In practice this should be the name of the desktop file and could be +// also accompanied by an appropriate hint indicating which icon to use. +// +// Message must include a summary and should include a body. The body may use +// HTML-like markup language to include bold, italic or underline text, as well +// as to include images and hyperlinks. +// +// A notification can automatically expire after the given number of +// milliseconds. This is separate from the notification being visible or +// invisible on-screen. Expired notifications are removed from persistent +// message roster, if one is supported. Two special values are recognized. When +// the expiration timeout is zero a message never expires. When the expiration +// timeout is -1 a message expires after a server-defined duration which may +// vary for the type of the notification message sent. +// +// A notification may replace an existing notification by setting the ReplacesID +// to a non-zero value. This only works if the notification server was not +// re-started and should be used for as long as the sender process is alive, as +// sending the identifier across session startup boundary has no chance of +// working correctly. +// +// A notification may optionally carry a number of hints that further customize it +// in a specific way. Refer to various hint constructors for details. +// +// A notification may optionally also carry one of several actions. If +// supported, actions can be invoked by the user, broadcasting a notification +// response back to the session. This mechanism only works if there is someone +// listening for the action being triggered. +// +// In all cases, the specific notification must take into account the +// capabilities of the server. For instance, if a server does not support body +// markup, then such markup is not automatically stripped by either the client +// or the server. +type Message struct { + AppName string + Icon string + Summary string + Body string + ExpireTimeout time.Duration // Effective resolution in milliseconds with 31-bit range. + ReplacesID ID + Actions []Action + Hints []Hint +} + +// ServerSelectedExpireTimeout requests the server to pick an expiration timeout +// appropriate for the message type. +const ServerSelectedExpireTimeout = time.Millisecond * -1 + +// ID is the opaque identifier of a notification assigned by the server. +// +// Notifications with known identifiers can be closed or updated. The identifier +// is valid within one desktop session and should not be used unless the calling +// process initially sent the message. +type ID uint32 + +// Action describes a single notification action. +// +// ActionKey is returned in an D-Bus signal when an action is activated by the +// user. The text must be localized for the appropriate language. +type Action struct { + ActionKey string + LocalizedText string +} + +// Hint describes supplementeary information that may be used by the server. +// +// Various helpers create hint objects of specifc purpose. +// +// Specification: https://developer.gnome.org/notification-spec/#hints +type Hint struct { + Name string + Value interface{} +} + +// ServerCapability describes a single capability of the notification server. +type ServerCapability string + +// CloseReason indicates why a notification message was closed. +type CloseReason uint32 + +const ( + // CloseReasonExpired indicates that a notification message has expired. + CloseReasonExpired CloseReason = 1 + // CloseReasonDismissed indicates that a notification message was dismissed by the user. + CloseReasonDismissed CloseReason = 2 + // CloseReasonClosed indicates that a notification message was closed with an API call. + CloseReasonClosed CloseReason = 3 + // CloseReasonUndefined indicates that no other well-known reason applies. + CloseReasonUndefined CloseReason = 4 +) + +// String implements the Stringer interface. +func (r CloseReason) String() string { + switch r { + case CloseReasonExpired: + return "expired" + case CloseReasonDismissed: + return "dismissed" + case CloseReasonClosed: + return "closed" + case CloseReasonUndefined: + return "undefined" + default: + return fmt.Sprintf("CloseReason(%d)", uint32(r)) + } +} + +// Observer is an interface for observing interactions with notification messages. +// +// An observer can be used to either observe a notification being closed or +// dismissed or to react to actions being invoked by the user. Practical +// implementations must remember the ID of the message they have sent to filter +// out other notifications. +type Observer interface { + // NotificationClosed is called when a notification is either closed or removed + // from the persistent roster. + NotificationClosed(id ID, reason CloseReason) error + // ActionInvoked is caliled when one of the notification message actions is + // clicked by the user. + ActionInvoked(id ID, actionKey string) error +} diff -Nru snapd-2.47.1+20.10.1build1/desktop/notification/notify_test.go snapd-2.48+21.04/desktop/notification/notify_test.go --- snapd-2.47.1+20.10.1build1/desktop/notification/notify_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48+21.04/desktop/notification/notify_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -0,0 +1,42 @@ +// -*- 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 notification_test + +import ( + "testing" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/desktop/notification" +) + +func Test(t *testing.T) { TestingT(t) } + +type notifySuite struct{} + +var _ = Suite(¬ifySuite{}) + +func (s *notifySuite) TestCloseReasonString(c *C) { + c.Check(notification.CloseReasonExpired.String(), Equals, "expired") + c.Check(notification.CloseReasonDismissed.String(), Equals, "dismissed") + c.Check(notification.CloseReasonClosed.String(), Equals, "closed") + c.Check(notification.CloseReasonUndefined.String(), Equals, "undefined") + c.Check(notification.CloseReason(42).String(), Equals, "CloseReason(42)") +} diff -Nru snapd-2.47.1+20.10.1build1/dirs/dirs.go snapd-2.48+21.04/dirs/dirs.go --- snapd-2.47.1+20.10.1build1/dirs/dirs.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/dirs/dirs.go 2020-11-19 16:51:02.000000000 +0000 @@ -58,6 +58,8 @@ SnapRunLockDir string SnapBootstrapRunDir string + SnapdMaintenanceFile string + SnapdStoreSSLCertsDir string SnapSeedDir string @@ -101,6 +103,8 @@ SnapModeenvFile string SnapBootAssetsDir string SnapFDEDir string + SnapSaveDir string + SnapDeviceSaveDir string CloudMetaDataFile string CloudInstanceDataFile string @@ -267,6 +271,17 @@ return filepath.Join(SnapDeviceDirUnder(rootdir), "fde") } +// SnapSaveDirUnder returns the path to device save directory under rootdir. +func SnapSaveDirUnder(rootdir string) string { + return filepath.Join(rootdir, snappyDir, "save") +} + +// SnapFDEDirUnderSave returns the path to full disk encryption state directory +// inside the given save tree dir. +func SnapFDEDirUnderSave(savedir string) string { + return filepath.Join(savedir, "device/fde") +} + // AddRootDirCallback registers a callback for whenever the global root // directory (set by SetRootDir) is changed to enable updates to variables in // other packages that depend on its location. @@ -287,6 +302,7 @@ "arch", "archlinux", "fedora", + "gentoo", "manjaro", "manjaro-arm", } @@ -308,6 +324,7 @@ SnapSeccompDir = filepath.Join(SnapSeccompBase, "bpf") SnapMountPolicyDir = filepath.Join(rootdir, snappyDir, "mount") SnapMetaDir = filepath.Join(rootdir, snappyDir, "meta") + SnapdMaintenanceFile = filepath.Join(rootdir, snappyDir, "maintenance.json") SnapBlobDir = SnapBlobDirUnder(rootdir) // ${snappyDir}/desktop is added to $XDG_DATA_DIRS. // Subdirectories are interpreted according to the relevant @@ -346,6 +363,8 @@ SnapModeenvFile = SnapModeenvFileUnder(rootdir) SnapBootAssetsDir = SnapBootAssetsDirUnder(rootdir) SnapFDEDir = SnapFDEDirUnder(rootdir) + SnapSaveDir = SnapSaveDirUnder(rootdir) + SnapDeviceSaveDir = filepath.Join(SnapSaveDir, "device") SnapRepairDir = filepath.Join(rootdir, snappyDir, "repair") SnapRepairStateFile = filepath.Join(SnapRepairDir, "repair.json") diff -Nru snapd-2.47.1+20.10.1build1/features/features.go snapd-2.48+21.04/features/features.go --- snapd-2.47.1+20.10.1build1/features/features.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/features/features.go 2020-11-19 16:51:02.000000000 +0000 @@ -98,8 +98,9 @@ // featuresEnabledWhenUnset contains a set of features that are enabled when not explicitly configured. var featuresEnabledWhenUnset = map[SnapdFeature]bool{ - Layouts: true, - RobustMountNamespaceUpdates: true, + Layouts: true, + RobustMountNamespaceUpdates: true, + ClassicPreservesXdgRuntimeDir: true, } // featuresExported contains a set of features that are exported outside of snapd. diff -Nru snapd-2.47.1+20.10.1build1/features/features_test.go snapd-2.48+21.04/features/features_test.go --- snapd-2.47.1+20.10.1build1/features/features_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/features/features_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -108,7 +108,7 @@ c.Check(features.SnapdSnap.IsEnabledWhenUnset(), Equals, false) c.Check(features.PerUserMountNamespace.IsEnabledWhenUnset(), Equals, false) c.Check(features.RefreshAppAwareness.IsEnabledWhenUnset(), Equals, false) - c.Check(features.ClassicPreservesXdgRuntimeDir.IsEnabledWhenUnset(), Equals, false) + c.Check(features.ClassicPreservesXdgRuntimeDir.IsEnabledWhenUnset(), Equals, true) c.Check(features.RobustMountNamespaceUpdates.IsEnabledWhenUnset(), Equals, true) c.Check(features.UserDaemons.IsEnabledWhenUnset(), Equals, false) c.Check(features.DbusActivation.IsEnabledWhenUnset(), Equals, false) diff -Nru snapd-2.47.1+20.10.1build1/gadget/device_darwin.go snapd-2.48+21.04/gadget/device_darwin.go --- snapd-2.47.1+20.10.1build1/gadget/device_darwin.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/gadget/device_darwin.go 2020-11-19 16:51:02.000000000 +0000 @@ -21,6 +21,8 @@ import ( "errors" + + "github.com/snapcore/snapd/gadget/quantity" ) var errNotImplemented = errors.New("not implemented") @@ -29,7 +31,7 @@ return "", errNotImplemented } -func findDeviceForStructureWithFallback(ps *LaidOutStructure) (string, Size, error) { +func findDeviceForStructureWithFallback(ps *LaidOutStructure) (string, quantity.Size, error) { return "", 0, errNotImplemented } diff -Nru snapd-2.47.1+20.10.1build1/gadget/device_linux.go snapd-2.48+21.04/gadget/device_linux.go --- snapd-2.47.1+20.10.1build1/gadget/device_linux.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/gadget/device_linux.go 2020-11-19 16:51:02.000000000 +0000 @@ -27,6 +27,7 @@ "strings" "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/gadget/quantity" "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/osutil/disks" ) @@ -111,7 +112,7 @@ // // Returns the device name and an offset at which the structure content starts // within the device or an error. -func findDeviceForStructureWithFallback(ps *LaidOutStructure) (dev string, offs Size, err error) { +func findDeviceForStructureWithFallback(ps *LaidOutStructure) (dev string, offs quantity.Size, err error) { if ps.HasFilesystem() { return "", 0, fmt.Errorf("internal error: cannot use with filesystem structures") } diff -Nru snapd-2.47.1+20.10.1build1/gadget/device_test.go snapd-2.48+21.04/gadget/device_test.go --- snapd-2.47.1+20.10.1build1/gadget/device_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/gadget/device_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -31,6 +31,7 @@ "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/gadget" + "github.com/snapcore/snapd/gadget/quantity" "github.com/snapcore/snapd/osutil" ) @@ -330,7 +331,7 @@ }) c.Check(err, ErrorMatches, `device not found`) c.Check(found, Equals, "") - c.Check(offs, Equals, gadget.Size(0)) + c.Check(offs, Equals, quantity.Size(0)) } func (d *deviceSuite) TestDeviceFindFallbackBadWritable(c *C) { @@ -347,14 +348,14 @@ found, offs, err := gadget.FindDeviceForStructureWithFallback(ps) c.Check(err, ErrorMatches, `lstat .*/dev/fakedevice0p1: no such file or directory`) c.Check(found, Equals, "") - c.Check(offs, Equals, gadget.Size(0)) + c.Check(offs, Equals, quantity.Size(0)) c.Assert(ioutil.WriteFile(filepath.Join(d.dir, "dev/fakedevice0p1"), nil, 064), IsNil) found, offs, err = gadget.FindDeviceForStructureWithFallback(ps) c.Check(err, ErrorMatches, `unexpected number of matches \(0\) for /sys/block/\*/fakedevice0p1`) c.Check(found, Equals, "") - c.Check(offs, Equals, gadget.Size(0)) + c.Check(offs, Equals, quantity.Size(0)) err = os.MkdirAll(filepath.Join(d.dir, "/sys/block/fakedevice0/fakedevice0p1"), 0755) c.Assert(err, IsNil) @@ -362,7 +363,7 @@ found, offs, err = gadget.FindDeviceForStructureWithFallback(ps) c.Check(err, ErrorMatches, `device .*/dev/fakedevice0 does not exist`) c.Check(found, Equals, "") - c.Check(offs, Equals, gadget.Size(0)) + c.Check(offs, Equals, quantity.Size(0)) } func (d *deviceSuite) TestDeviceFindFallbackHappyWritable(c *C) { @@ -400,9 +401,9 @@ c.Check(err, IsNil) c.Check(found, Equals, filepath.Join(d.dir, "/dev/fakedevice0")) if ps.Type != "mbr" { - c.Check(offs, Equals, gadget.Size(123)) + c.Check(offs, Equals, quantity.Size(123)) } else { - c.Check(offs, Equals, gadget.Size(0)) + c.Check(offs, Equals, quantity.Size(0)) } } } @@ -422,7 +423,7 @@ found, offs, err := gadget.FindDeviceForStructureWithFallback(psNamed) c.Check(err, Equals, gadget.ErrDeviceNotFound) c.Check(found, Equals, "") - c.Check(offs, Equals, gadget.Size(0)) + c.Check(offs, Equals, quantity.Size(0)) } func (d *deviceSuite) TestDeviceFindFallbackNotForFilesystem(c *C) { @@ -440,7 +441,7 @@ found, offs, err := gadget.FindDeviceForStructureWithFallback(psFs) c.Check(err, ErrorMatches, "internal error: cannot use with filesystem structures") c.Check(found, Equals, "") - c.Check(offs, Equals, gadget.Size(0)) + c.Check(offs, Equals, quantity.Size(0)) } func (d *deviceSuite) TestDeviceFindFallbackBadMountInfo(c *C) { @@ -457,7 +458,7 @@ found, offs, err := gadget.FindDeviceForStructureWithFallback(psFs) c.Check(err, ErrorMatches, "cannot read mount info: .*") c.Check(found, Equals, "") - c.Check(offs, Equals, gadget.Size(0)) + c.Check(offs, Equals, quantity.Size(0)) } func (d *deviceSuite) TestDeviceFindFallbackPassThrough(c *C) { @@ -472,7 +473,7 @@ found, offs, err := gadget.FindDeviceForStructureWithFallback(ps) c.Check(err, ErrorMatches, `candidate .*/dev/disk/by-partlabel/foo is not a symlink`) c.Check(found, Equals, "") - c.Check(offs, Equals, gadget.Size(0)) + c.Check(offs, Equals, quantity.Size(0)) // create a proper symlink err = os.Remove(filepath.Join(d.dir, "/dev/disk/by-partlabel/foo")) @@ -484,7 +485,7 @@ found, offs, err = gadget.FindDeviceForStructureWithFallback(ps) c.Assert(err, IsNil) c.Check(found, Equals, filepath.Join(d.dir, "/dev/fakedevice")) - c.Check(offs, Equals, gadget.Size(0)) + c.Check(offs, Equals, quantity.Size(0)) } func (d *deviceSuite) TestDeviceFindMountPointErrorsWithBare(c *C) { diff -Nru snapd-2.47.1+20.10.1build1/gadget/export_test.go snapd-2.48+21.04/gadget/export_test.go --- snapd-2.47.1+20.10.1build1/gadget/export_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/gadget/export_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -59,7 +59,6 @@ FindDeviceForStructureWithFallback = findDeviceForStructureWithFallback FindMountPointForStructure = findMountPointForStructure - ParseSize = parseSize ParseRelativeOffset = parseRelativeOffset ) diff -Nru snapd-2.47.1+20.10.1build1/gadget/gadget.go snapd-2.48+21.04/gadget/gadget.go --- snapd-2.47.1+20.10.1build1/gadget/gadget.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/gadget/gadget.go 2020-11-19 16:51:02.000000000 +0000 @@ -23,7 +23,6 @@ "errors" "fmt" "io/ioutil" - "math" "os" "path/filepath" "regexp" @@ -34,6 +33,7 @@ "github.com/snapcore/snapd/asserts" "github.com/snapcore/snapd/gadget/edition" + "github.com/snapcore/snapd/gadget/quantity" "github.com/snapcore/snapd/metautil" "github.com/snapcore/snapd/snap" "github.com/snapcore/snapd/snap/naming" @@ -50,6 +50,7 @@ SystemBoot = "system-boot" SystemData = "system-data" SystemSeed = "system-seed" + SystemSave = "system-save" bootImage = "system-boot-image" bootSelect = "system-boot-select" @@ -107,13 +108,13 @@ // Label provides the filesystem label Label string `yaml:"filesystem-label"` // Offset defines a starting offset of the structure - Offset *Size `yaml:"offset"` + Offset *quantity.Size `yaml:"offset"` // OffsetWrite describes a 32-bit address, within the volume, at which // the offset of current structure will be written. The position may be // specified as a byte offset relative to the start of a named structure OffsetWrite *RelativeOffset `yaml:"offset-write"` // Size of the structure - Size Size `yaml:"size"` + Size quantity.Size `yaml:"size"` // Type of the structure, which can be 2-hex digit MBR partition, // 36-char GUID partition, comma separated , for hybrid // partitioning schemes, or 'bare' when the structure is not considered @@ -186,14 +187,14 @@ // for a 'bare' type structure Image string `yaml:"image"` // Offset the image is written at - Offset *Size `yaml:"offset"` + Offset *quantity.Size `yaml:"offset"` // OffsetWrite describes a 32-bit address, within the volume, at which // the offset of current image will be written. The position may be // specified as a byte offset relative to the start of a named structure OffsetWrite *RelativeOffset `yaml:"offset-write"` // Size of the image, when empty size is calculated by looking at the // image - Size Size `yaml:"size"` + Size quantity.Size `yaml:"size"` Unpack bool `yaml:"unpack"` } @@ -405,6 +406,7 @@ SystemSeed *VolumeStructure SystemData *VolumeStructure SystemBoot *VolumeStructure + SystemSave *VolumeStructure } func validateVolume(name string, vol *Volume, model Model) error { @@ -423,12 +425,12 @@ structures := make([]LaidOutStructure, len(vol.Structure)) state := &validationState{} - previousEnd := Size(0) + previousEnd := quantity.Size(0) for idx, s := range vol.Structure { if err := validateVolumeStructure(&s, vol); err != nil { return fmt.Errorf("invalid structure %v: %v", fmtIndexAndName(idx, s.Name), err) } - var start Size + var start quantity.Size if s.Offset != nil { start = *s.Offset } else { @@ -471,6 +473,11 @@ return fmt.Errorf("cannot have more than one partition with system-boot role") } state.SystemBoot = &vol.Structure[idx] + case SystemSave: + if state.SystemSave != nil { + return fmt.Errorf("cannot have more than one partition with system-save role") + } + state.SystemSave = &vol.Structure[idx] } previousEnd = end @@ -489,7 +496,7 @@ func ensureVolumeConsistencyNoConstraints(state *validationState) error { switch { case state.SystemSeed == nil && state.SystemData == nil: - return nil + // happy so far case state.SystemSeed != nil && state.SystemData == nil: return fmt.Errorf("the system-seed role requires system-data to be defined") case state.SystemSeed == nil && state.SystemData != nil: @@ -501,6 +508,11 @@ return err } } + if state.SystemSave != nil { + if err := ensureSystemSaveConsistency(state); err != nil { + return err + } + } return nil } @@ -510,7 +522,6 @@ if wantsSystemSeed(model) { return fmt.Errorf("model requires system-seed partition, but no system-seed or system-data partition found") } - return nil case state.SystemSeed != nil && state.SystemData == nil: return fmt.Errorf("the system-seed role requires system-data to be defined") case state.SystemSeed == nil && state.SystemData != nil: @@ -519,7 +530,7 @@ return fmt.Errorf("model requires system-seed structure, but none was found") } // without SystemSeed, system-data label must be implicit or writable - if state.SystemData != nil && state.SystemData.Label != "" && state.SystemData.Label != implicitSystemDataLabel { + if state.SystemData.Label != "" && state.SystemData.Label != implicitSystemDataLabel { return fmt.Errorf("system-data structure must have an implicit label or %q, not %q", implicitSystemDataLabel, state.SystemData.Label) } @@ -532,6 +543,11 @@ return err } } + if state.SystemSave != nil { + if err := ensureSystemSaveConsistency(state); err != nil { + return err + } + } return nil } @@ -549,12 +565,21 @@ if state.SystemSeed.Label != "" { return fmt.Errorf("system-seed structure must not have a label") } + return nil +} +func ensureSystemSaveConsistency(state *validationState) error { + if state.SystemData == nil || state.SystemSeed == nil { + return fmt.Errorf("system-save requires system-seed and system-data structures") + } + if state.SystemSave.Label != "" { + return fmt.Errorf("system-save structure must not have a label") + } return nil } func validateCrossVolumeStructure(structures []LaidOutStructure, knownStructures map[string]*LaidOutStructure) error { - previousEnd := Size(0) + previousEnd := quantity.Size(0) // cross structure validation: // - relative offsets that reference other structures by name // - laid out structure overlap @@ -598,6 +623,28 @@ return nil } +var ( + reservedLabels = []string{ + ubuntuBootLabel, ubuntuSeedLabel, + ubuntuDataLabel, ubuntuSaveLabel, + } +) + +func validateReservedLabels(vs *VolumeStructure) error { + if vs.Role != "" { + // structure specifies a role, its labels will be checked later + return nil + } + if vs.Label == "" { + return nil + } + if strutil.ListContains(reservedLabels, vs.Label) { + // a structure without a role uses one of reserved labels + return fmt.Errorf("label %q is reserved", vs.Label) + } + return nil +} + func validateVolumeStructure(vs *VolumeStructure, vol *Volume) error { if vs.Size == 0 { return errors.New("missing size") @@ -635,6 +682,10 @@ return err } + if err := validateReservedLabels(vs); err != nil { + return err + } + // TODO: validate structure size against sector-size; ubuntu-image uses // a tmp file to find out the default sector size of the device the tmp // file is created on @@ -720,7 +771,7 @@ } switch vsRole { - case SystemData, SystemSeed: + case SystemData, SystemSeed, SystemSave: // roles have cross dependencies, consistency checks are done at // the volume level case schemaMBR: @@ -783,91 +834,14 @@ return nil } -// Size describes the size of a structure item or an offset within the -// structure. -type Size uint64 - const ( - SizeKiB = Size(1 << 10) - SizeMiB = Size(1 << 20) - SizeGiB = Size(1 << 30) - // SizeMBR is the maximum byte size of a structure of role 'mbr' - SizeMBR = Size(446) + SizeMBR = quantity.Size(446) // SizeLBA48Pointer is the byte size of a pointer value written at the // location described by 'offset-write' - SizeLBA48Pointer = Size(4) + SizeLBA48Pointer = quantity.Size(4) ) -func (s *Size) UnmarshalYAML(unmarshal func(interface{}) error) error { - var gs string - if err := unmarshal(&gs); err != nil { - return errors.New(`cannot unmarshal gadget size`) - } - - var err error - *s, err = parseSize(gs) - if err != nil { - return fmt.Errorf("cannot parse size %q: %v", gs, err) - } - return err -} - -// parseSize parses a string expressing size in gadget declaration. The -// accepted format is one of: | M | G. -func parseSize(gs string) (Size, error) { - number, unit, err := strutil.SplitUnit(gs) - if err != nil { - return 0, err - } - if number < 0 { - return 0, errors.New("size cannot be negative") - } - var size Size - switch unit { - case "M": - // MiB - size = Size(number) * SizeMiB - case "G": - // GiB - size = Size(number) * SizeGiB - case "": - // straight bytes - size = Size(number) - default: - return 0, fmt.Errorf("invalid suffix %q", unit) - } - return size, nil -} - -func (s *Size) String() string { - if s == nil { - return "unspecified" - } - return fmt.Sprintf("%d", *s) -} - -// IECString formats the size using multiples from IEC units (i.e. kibibytes, -// mebibytes), that is as multiples of 1024. Printed values are truncated to 2 -// decimal points. -func (s *Size) IECString() string { - maxFloat := float64(1023.5) - r := float64(*s) - unit := "B" - for _, rangeUnit := range []string{"KiB", "MiB", "GiB", "TiB", "PiB"} { - if r < maxFloat { - break - } - r /= 1024 - unit = rangeUnit - } - precision := 0 - if math.Floor(r) != r { - precision = 2 - } - return fmt.Sprintf("%.*f %s", precision, r, unit) -} - // RelativeOffset describes an offset where structure data is written at. // The position can be specified as byte-offset relative to the start of another // named structure. @@ -876,7 +850,7 @@ // address write will be calculated. RelativeTo string // Offset is a 32-bit value - Offset Size + Offset quantity.Size } func (r *RelativeOffset) String() string { @@ -904,11 +878,11 @@ return nil, errors.New("missing offset") } - size, err := parseSize(sizeSpec) + size, err := quantity.ParseSize(sizeSpec) if err != nil { return nil, fmt.Errorf("cannot parse offset %q: %v", sizeSpec, err) } - if size > 4*SizeGiB { + if size > 4*quantity.SizeGiB { return nil, fmt.Errorf("offset above 4G limit") } @@ -969,12 +943,6 @@ // PositionedVolumeFromGadget takes a gadget rootdir and positions the // partitions as specified. func PositionedVolumeFromGadget(gadgetRoot string) (*LaidOutVolume, error) { - // TODO:UC20: since this is unconstrained via the model, it returns an - // err == nil and an empty info when the gadgetRoot does not - // actually contain the required gadget.yaml file (for example - // when you have a typo in the args to snap-bootstrap - // create-partitions). anyways just verify this more because - // otherwise it's unhelpful :-/ info, err := ReadInfo(gadgetRoot, nil) if err != nil { return nil, err @@ -985,7 +953,7 @@ } constraints := LayoutConstraints{ - NonMBRStartOffset: 1 * SizeMiB, + NonMBRStartOffset: 1 * quantity.SizeMiB, SectorSize: 512, } diff -Nru snapd-2.47.1+20.10.1build1/gadget/gadget_test.go snapd-2.48+21.04/gadget/gadget_test.go --- snapd-2.47.1+20.10.1build1/gadget/gadget_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/gadget/gadget_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -34,6 +34,7 @@ "github.com/snapcore/snapd/asserts" "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/gadget" + "github.com/snapcore/snapd/gadget/quantity" "github.com/snapcore/snapd/snap/snapfile" "github.com/snapcore/snapd/snap/snaptest" ) @@ -287,8 +288,8 @@ func TestRun(t *testing.T) { TestingT(t) } -func mustParseGadgetSize(c *C, s string) gadget.Size { - gs, err := gadget.ParseSize(s) +func mustParseGadgetSize(c *C, s string) quantity.Size { + gs, err := quantity.ParseSize(s) c.Assert(err, IsNil) return gs } @@ -432,8 +433,8 @@ }) } -func asSizePtr(size gadget.Size) *gadget.Size { - gsz := gadget.Size(size) +func asSizePtr(size quantity.Size) *quantity.Size { + gsz := quantity.Size(size) return &gsz } @@ -673,37 +674,6 @@ c.Check(err, ErrorMatches, `cannot parse gadget metadata: "edition" must be a positive number, not "-5"`) } -func (s *gadgetYamlTestSuite) TestUnmarshalGadgetSize(c *C) { - type foo struct { - Size gadget.Size `yaml:"size"` - } - - for i, tc := range []struct { - s string - sz gadget.Size - err string - }{ - {"1234", 1234, ""}, - {"1234M", 1234 * gadget.SizeMiB, ""}, - {"1234G", 1234 * gadget.SizeGiB, ""}, - {"0", 0, ""}, - {"a0M", 0, `cannot parse size "a0M": no numerical prefix.*`}, - {"-123", 0, `cannot parse size "-123": size cannot be negative`}, - {"123a", 0, `cannot parse size "123a": invalid suffix "a"`}, - } { - c.Logf("tc: %v", i) - - var f foo - err := yaml.Unmarshal([]byte(fmt.Sprintf("size: %s", tc.s)), &f) - if tc.err != "" { - c.Check(err, ErrorMatches, tc.err) - } else { - c.Check(err, IsNil) - c.Check(f.Size, Equals, tc.sz) - } - } -} - func (s *gadgetYamlTestSuite) TestUnmarshalGadgetRelativeOffset(c *C) { type foo struct { OffsetWrite gadget.RelativeOffset `yaml:"offset-write"` @@ -715,13 +685,13 @@ err string }{ {"1234", &gadget.RelativeOffset{Offset: 1234}, ""}, - {"1234M", &gadget.RelativeOffset{Offset: 1234 * gadget.SizeMiB}, ""}, - {"4096M", &gadget.RelativeOffset{Offset: 4096 * gadget.SizeMiB}, ""}, + {"1234M", &gadget.RelativeOffset{Offset: 1234 * quantity.SizeMiB}, ""}, + {"4096M", &gadget.RelativeOffset{Offset: 4096 * quantity.SizeMiB}, ""}, {"0", &gadget.RelativeOffset{}, ""}, {"mbr+0", &gadget.RelativeOffset{RelativeTo: "mbr"}, ""}, - {"foo+1234M", &gadget.RelativeOffset{RelativeTo: "foo", Offset: 1234 * gadget.SizeMiB}, ""}, - {"foo+1G", &gadget.RelativeOffset{RelativeTo: "foo", Offset: 1 * gadget.SizeGiB}, ""}, - {"foo+1G", &gadget.RelativeOffset{RelativeTo: "foo", Offset: 1 * gadget.SizeGiB}, ""}, + {"foo+1234M", &gadget.RelativeOffset{RelativeTo: "foo", Offset: 1234 * quantity.SizeMiB}, ""}, + {"foo+1G", &gadget.RelativeOffset{RelativeTo: "foo", Offset: 1 * quantity.SizeGiB}, ""}, + {"foo+1G", &gadget.RelativeOffset{RelativeTo: "foo", Offset: 1 * quantity.SizeGiB}, ""}, {"foo+4097M", nil, `cannot parse relative offset "foo\+4097M": offset above 4G limit`}, {"foo+", nil, `cannot parse relative offset "foo\+": missing offset`}, {"foo+++12", nil, `cannot parse relative offset "foo\+\+\+12": cannot parse offset "\+\+12": .*`}, @@ -894,6 +864,10 @@ validSystemSeed := uuidType + ` role: system-seed ` + validSystemSave := uuidType + ` +role: system-save +size: 5M +` emptyRole := uuidType + ` role: system-boot size: 123M @@ -930,8 +904,8 @@ {mustParseStructure(c, bogusRole), vol, `invalid role "foobar": unsupported role`}, // the system-seed role {mustParseStructure(c, validSystemSeed), vol, ""}, - {mustParseStructure(c, validSystemSeed), vol, ""}, - {mustParseStructure(c, validSystemSeed), vol, ""}, + // system-save role + {mustParseStructure(c, validSystemSave), vol, ""}, // mbr {mustParseStructure(c, mbrTooLarge), mbrVol, `invalid role "mbr": mbr structures cannot be larger than 446 bytes`}, {mustParseStructure(c, mbrBadOffset), mbrVol, `invalid role "mbr": mbr structure must start at offset 0`}, @@ -1045,8 +1019,8 @@ func (s *gadgetYamlTestSuite) TestValidateVolumeDuplicateFsLabel(c *C) { err := gadget.ValidateVolume("name", &gadget.Volume{ Structure: []gadget.VolumeStructure{ - {Label: "foo", Type: "21686148-6449-6E6F-744E-656564454123", Size: gadget.SizeMiB}, - {Label: "foo", Type: "21686148-6449-6E6F-744E-656564454649", Size: gadget.SizeMiB}, + {Label: "foo", Type: "21686148-6449-6E6F-744E-656564454123", Size: quantity.SizeMiB}, + {Label: "foo", Type: "21686148-6449-6E6F-744E-656564454649", Size: quantity.SizeMiB}, }, }, nil) c.Assert(err, ErrorMatches, `filesystem label "foo" is not unique`) @@ -1072,13 +1046,13 @@ Role: gadget.SystemData, Label: x.label, Type: "21686148-6449-6E6F-744E-656564454123", - Size: gadget.SizeMiB, + Size: quantity.SizeMiB, }, { Name: "data2", Role: gadget.SystemData, Label: x.label, Type: "21686148-6449-6E6F-744E-656564454649", - Size: gadget.SizeMiB, + Size: quantity.SizeMiB, }}, }, constraints) c.Assert(err, ErrorMatches, x.errMsg) @@ -1091,12 +1065,12 @@ Name: "boot1", Label: "system-boot", Type: "EF,C12A7328-F81F-11D2-BA4B-00A0C93EC93B", - Size: gadget.SizeMiB, + Size: quantity.SizeMiB, }, { Name: "boot2", Label: "system-boot", Type: "EF,C12A7328-F81F-11D2-BA4B-00A0C93EC93B", - Size: gadget.SizeMiB, + Size: quantity.SizeMiB, }}, }, nil) c.Assert(err, ErrorMatches, `filesystem label "system-boot" is not unique`) @@ -1314,6 +1288,37 @@ c.Check(err, IsNil) } +func (s *gadgetYamlTestSuite) TestValidateStructureReservedLabels(c *C) { + + gv := &gadget.Volume{} + + for _, tc := range []struct { + role, label, err string + }{ + {label: "ubuntu-seed", err: `label "ubuntu-seed" is reserved`}, + {label: "ubuntu-boot", err: `label "ubuntu-boot" is reserved`}, + {label: "ubuntu-data", err: `label "ubuntu-data" is reserved`}, + {label: "ubuntu-save", err: `label "ubuntu-save" is reserved`}, + // these are ok + {role: "system-boot", label: "ubuntu-boot"}, + {label: "random-ubuntu-label"}, + } { + err := gadget.ValidateVolumeStructure(&gadget.VolumeStructure{ + Type: "21686148-6449-6E6F-744E-656564454649", + Role: tc.role, + Filesystem: "ext4", + Label: tc.label, + Size: 10 * 1024, + }, gv) + if tc.err == "" { + c.Check(err, IsNil) + } else { + c.Check(err, ErrorMatches, tc.err) + } + } + +} + func (s *gadgetYamlTestSuite) TestValidateLayoutOverlapPreceding(c *C) { overlappingGadgetYaml := ` volumes: @@ -1532,6 +1537,26 @@ vs.SystemSeed = &gadget.VolumeStructure{} err = gadget.EnsureVolumeConsistency(vs, nil) c.Assert(err, ErrorMatches, "the system-seed role requires system-data to be defined") + + // Check system-save + vsWithSave := &gadget.ValidationState{ + SystemData: &gadget.VolumeStructure{}, + SystemSeed: &gadget.VolumeStructure{}, + SystemSave: &gadget.VolumeStructure{}, + } + err = gadget.EnsureVolumeConsistency(vsWithSave, nil) + c.Assert(err, IsNil) + // use illegal label on system-save + vsWithSave.SystemSave.Label = "foo" + err = gadget.EnsureVolumeConsistency(vsWithSave, nil) + c.Assert(err, ErrorMatches, "system-save structure must not have a label") + // complains when either system-seed or system-data is missing + vsWithSave.SystemSeed = nil + err = gadget.EnsureVolumeConsistency(vsWithSave, nil) + c.Assert(err, ErrorMatches, "system-save requires system-seed and system-data structures") + vsWithSave.SystemData = nil + err = gadget.EnsureVolumeConsistency(vsWithSave, nil) + c.Assert(err, ErrorMatches, "system-save requires system-seed and system-data structures") } func (s *gadgetYamlTestSuite) TestGadgetConsistencyWithoutConstraints(c *C) { @@ -1596,29 +1621,40 @@ structure:` for i, tc := range []struct { - role string - label string - systemSeed bool - err string + addSeed bool + dataLabel string + requireSeed bool + addSave bool + saveLabel string + err string }{ // when constraints are nil, the system-seed role and ubuntu-data label on the // system-data structure should be consistent - {"system-seed", "", true, ""}, - {"system-seed", "", false, `.* model does not support the system-seed role`}, - {"system-seed", "writable", true, ".* system-data structure must not have a label"}, - {"system-seed", "writable", false, `.* model does not support the system-seed role`}, - {"system-seed", "ubuntu-data", true, ".* system-data structure must not have a label"}, - {"system-seed", "ubuntu-data", false, `.* model does not support the system-seed role`}, - {"", "writable", true, `.* model requires system-seed structure, but none was found`}, - {"", "writable", false, ""}, - {"", "ubuntu-data", true, `.* model requires system-seed structure, but none was found`}, - {"", "ubuntu-data", false, `.* must have an implicit label or "writable", not "ubuntu-data"`}, + {addSeed: true, requireSeed: true}, + {addSeed: true, err: `.* model does not support the system-seed role`}, + {addSeed: true, dataLabel: "writable", requireSeed: true, + err: ".* system-data structure must not have a label"}, + {addSeed: true, dataLabel: "writable", + err: `.* model does not support the system-seed role`}, + {addSeed: true, dataLabel: "ubuntu-data", requireSeed: true, + err: ".* system-data structure must not have a label"}, + {addSeed: true, dataLabel: "ubuntu-data", + err: `.* model does not support the system-seed role`}, + {dataLabel: "writable", requireSeed: true, + err: `.* model requires system-seed structure, but none was found`}, + {dataLabel: "writable"}, + {dataLabel: "ubuntu-data", requireSeed: true, + err: `.* model requires system-seed structure, but none was found`}, + {dataLabel: "ubuntu-data", err: `.* must have an implicit label or "writable", not "ubuntu-data"`}, + {addSave: true, err: `.* system-save requires system-seed and system-data structures`}, + {addSeed: true, requireSeed: true, addSave: true, saveLabel: "foo", + err: `.* system-save structure must not have a label`}, } { - c.Logf("tc: %v %v %v %v", i, tc.role, tc.label, tc.systemSeed) + c.Logf("tc: %v %v %v %v", i, tc.addSeed, tc.dataLabel, tc.requireSeed) b := &bytes.Buffer{} fmt.Fprintf(b, bloader) - if tc.role == "system-seed" { + if tc.addSeed { fmt.Fprintf(b, ` - name: Recovery size: 10M @@ -1631,14 +1667,26 @@ size: 10M type: 83 role: system-data - filesystem-label: %s`, tc.label) + filesystem-label: %s`, tc.dataLabel) + if tc.addSave { + fmt.Fprintf(b, ` + - name: Save + size: 10M + type: 83 + role: system-save`) + if tc.saveLabel != "" { + fmt.Fprintf(b, ` + filesystem-label: %s`, tc.saveLabel) + + } + } err := ioutil.WriteFile(s.gadgetYamlPath, b.Bytes(), 0644) c.Assert(err, IsNil) constraints := &modelConstraints{ classic: false, - systemSeed: tc.systemSeed, + systemSeed: tc.requireSeed, } _, err = gadget.ReadInfo(s.dir, constraints) @@ -2002,27 +2050,3 @@ err = gadget.IsCompatible(gi, giNew) c.Check(err, IsNil) } - -type gadgetSizeTestSuite struct{} - -var _ = Suite(&gadgetSizeTestSuite{}) - -func (s *gadgetSizeTestSuite) TestIECString(c *C) { - for _, tc := range []struct { - size gadget.Size - exp string - }{ - {512, "512 B"}, - {1000, "1000 B"}, - {1030, "1.01 KiB"}, - {gadget.SizeKiB + 512, "1.50 KiB"}, - {123 * gadget.SizeKiB, "123 KiB"}, - {512 * gadget.SizeKiB, "512 KiB"}, - {578 * gadget.SizeMiB, "578 MiB"}, - {1*gadget.SizeGiB + 123*gadget.SizeMiB, "1.12 GiB"}, - {1024 * gadget.SizeGiB, "1 TiB"}, - {2 * 1024 * 1024 * 1024 * gadget.SizeGiB, "2048 PiB"}, - } { - c.Check(tc.size.IECString(), Equals, tc.exp) - } -} diff -Nru snapd-2.47.1+20.10.1build1/gadget/install/content.go snapd-2.48+21.04/gadget/install/content.go --- snapd-2.47.1+20.10.1build1/gadget/install/content.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/gadget/install/content.go 2020-11-19 16:51:02.000000000 +0000 @@ -28,6 +28,7 @@ "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/gadget" "github.com/snapcore/snapd/gadget/internal" + "github.com/snapcore/snapd/logger" ) var contentMountpoint string @@ -40,7 +41,8 @@ // to the filesystem type defined in the gadget. func makeFilesystem(ds *gadget.OnDiskStructure) error { if ds.HasFilesystem() { - if err := internal.Mkfs(ds.VolumeStructure.Filesystem, ds.Node, ds.VolumeStructure.Label); err != nil { + logger.Debugf("create %s filesystem on %s with label %q", ds.VolumeStructure.Filesystem, ds.Node, ds.VolumeStructure.Label) + if err := internal.Mkfs(ds.VolumeStructure.Filesystem, ds.Node, ds.VolumeStructure.Label, ds.Size); err != nil { return err } if err := udevTrigger(ds.Node); err != nil { diff -Nru snapd-2.47.1+20.10.1build1/gadget/install/content_test.go snapd-2.48+21.04/gadget/install/content_test.go --- snapd-2.47.1+20.10.1build1/gadget/install/content_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/gadget/install/content_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -31,6 +31,7 @@ "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/gadget" "github.com/snapcore/snapd/gadget/install" + "github.com/snapcore/snapd/gadget/quantity" "github.com/snapcore/snapd/testutil" ) @@ -185,7 +186,7 @@ } func (m *mockWriteObserver) Observe(op gadget.ContentOperation, sourceStruct *gadget.LaidOutStructure, - targetRootDir, relativeTargetPath string, data *gadget.ContentChange) (bool, error) { + targetRootDir, relativeTargetPath string, data *gadget.ContentChange) (gadget.ContentChangeAction, error) { if m.content == nil { m.content = make(map[string][]*mockContentChange) } @@ -193,7 +194,7 @@ &mockContentChange{path: relativeTargetPath, change: data}) m.c.Assert(sourceStruct, NotNil) m.c.Check(sourceStruct, DeepEquals, m.expectedStruct) - return true, m.observeErr + return gadget.ChangeApply, m.observeErr } func (s *contentTestSuite) TestWriteFilesystemContent(c *C) { @@ -288,7 +289,7 @@ Image: "pc-core.img", }, StartOffset: 2, - Size: gadget.Size(len("pc-core.img content")), + Size: quantity.Size(len("pc-core.img content")), }, } diff -Nru snapd-2.47.1+20.10.1build1/gadget/install/encrypt_test.go snapd-2.48+21.04/gadget/install/encrypt_test.go --- snapd-2.47.1+20.10.1build1/gadget/install/encrypt_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/gadget/install/encrypt_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -1,4 +1,5 @@ // -*- Mode: Go; indent-tabs-mode: t -*- +// +build !nosecboot /* * Copyright (C) 2020 Canonical Ltd diff -Nru snapd-2.47.1+20.10.1build1/gadget/install/export_secboot_test.go snapd-2.48+21.04/gadget/install/export_secboot_test.go --- snapd-2.47.1+20.10.1build1/gadget/install/export_secboot_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48+21.04/gadget/install/export_secboot_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -0,0 +1,47 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- +// +build !nosecboot + +/* + * 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 install + +import ( + "github.com/snapcore/snapd/secboot" +) + +var ( + EnsureLayoutCompatibility = ensureLayoutCompatibility + DeviceFromRole = deviceFromRole + NewEncryptedDevice = newEncryptedDevice +) + +func MockSecbootFormatEncryptedDevice(f func(key secboot.EncryptionKey, label, node string) error) (restore func()) { + old := secbootFormatEncryptedDevice + secbootFormatEncryptedDevice = f + return func() { + secbootFormatEncryptedDevice = old + } +} + +func MockSecbootAddRecoveryKey(f func(key secboot.EncryptionKey, rkey secboot.RecoveryKey, node string) error) (restore func()) { + old := secbootAddRecoveryKey + secbootAddRecoveryKey = f + return func() { + secbootAddRecoveryKey = old + } +} diff -Nru snapd-2.47.1+20.10.1build1/gadget/install/export_test.go snapd-2.48+21.04/gadget/install/export_test.go --- snapd-2.47.1+20.10.1build1/gadget/install/export_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/gadget/install/export_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -23,14 +23,9 @@ "time" "github.com/snapcore/snapd/gadget" - "github.com/snapcore/snapd/secboot" ) var ( - EnsureLayoutCompatibility = ensureLayoutCompatibility - DeviceFromRole = deviceFromRole - NewEncryptedDevice = newEncryptedDevice - MakeFilesystem = makeFilesystem WriteContent = writeContent MountFilesystem = mountFilesystem @@ -38,6 +33,8 @@ CreateMissingPartitions = createMissingPartitions RemoveCreatedPartitions = removeCreatedPartitions EnsureNodesExist = ensureNodesExist + + CreatedDuringInstall = createdDuringInstall ) func MockContentMountpoint(new string) (restore func()) { @@ -71,19 +68,3 @@ ensureNodesExist = old } } - -func MockSecbootFormatEncryptedDevice(f func(key secboot.EncryptionKey, label, node string) error) (restore func()) { - old := secbootFormatEncryptedDevice - secbootFormatEncryptedDevice = f - return func() { - secbootFormatEncryptedDevice = old - } -} - -func MockSecbootAddRecoveryKey(f func(key secboot.EncryptionKey, rkey secboot.RecoveryKey, node string) error) (restore func()) { - old := secbootAddRecoveryKey - secbootAddRecoveryKey = f - return func() { - secbootAddRecoveryKey = old - } -} diff -Nru snapd-2.47.1+20.10.1build1/gadget/install/install_dummy.go snapd-2.48+21.04/gadget/install/install_dummy.go --- snapd-2.47.1+20.10.1build1/gadget/install/install_dummy.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/gadget/install/install_dummy.go 2020-11-19 16:51:02.000000000 +0000 @@ -22,8 +22,10 @@ import ( "fmt" + + "github.com/snapcore/snapd/gadget" ) -func Run(gadgetRoot, device string, options Options, _ SystemInstallObserver) error { - return fmt.Errorf("build without secboot support") +func Run(gadgetRoot, device string, options Options, _ gadget.ContentObserver) (*InstalledSystemSideData, error) { + return nil, fmt.Errorf("build without secboot support") } diff -Nru snapd-2.47.1+20.10.1build1/gadget/install/install.go snapd-2.48+21.04/gadget/install/install.go --- snapd-2.47.1+20.10.1build1/gadget/install/install.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/gadget/install/install.go 2020-11-19 16:51:02.000000000 +0000 @@ -27,11 +27,13 @@ "github.com/snapcore/snapd/boot" "github.com/snapcore/snapd/gadget" + "github.com/snapcore/snapd/logger" "github.com/snapcore/snapd/secboot" ) const ( ubuntuDataLabel = "ubuntu-data" + ubuntuSaveLabel = "ubuntu-save" ) func deviceFromRole(lv *gadget.LaidOutVolume, role string) (device string, err error) { @@ -51,14 +53,19 @@ // Run bootstraps the partitions of a device, by either creating // missing ones or recreating installed ones. -func Run(gadgetRoot, device string, options Options, observer SystemInstallObserver) error { +func Run(gadgetRoot, device string, options Options, observer gadget.ContentObserver) (*InstalledSystemSideData, error) { + logger.Noticef("installing a new system") + logger.Noticef(" gadget data from: %v", gadgetRoot) + if options.Encrypt { + logger.Noticef(" encryption: on") + } if gadgetRoot == "" { - return fmt.Errorf("cannot use empty gadget root directory") + return nil, fmt.Errorf("cannot use empty gadget root directory") } lv, err := gadget.PositionedVolumeFromGadget(gadgetRoot) if err != nil { - return fmt.Errorf("cannot layout the volume: %v", err) + return nil, fmt.Errorf("cannot layout the volume: %v", err) } // XXX: the only situation where auto-detect is not desired is @@ -68,135 +75,179 @@ if device == "" { device, err = deviceFromRole(lv, gadget.SystemSeed) if err != nil { - return fmt.Errorf("cannot find device to create partitions on: %v", err) + return nil, fmt.Errorf("cannot find device to create partitions on: %v", err) } } diskLayout, err := gadget.OnDiskVolumeFromDevice(device) if err != nil { - return fmt.Errorf("cannot read %v partitions: %v", device, err) + return nil, fmt.Errorf("cannot read %v partitions: %v", device, err) } // check if the current partition table is compatible with the gadget, // ignoring partitions added by the installer (will be removed later) if err := ensureLayoutCompatibility(lv, diskLayout); err != nil { - return fmt.Errorf("gadget and %v partition table not compatible: %v", device, err) + return nil, fmt.Errorf("gadget and %v partition table not compatible: %v", device, err) } // remove partitions added during a previous install attempt - if err := removeCreatedPartitions(diskLayout); err != nil { - return fmt.Errorf("cannot remove partitions from previous install: %v", err) + if err := removeCreatedPartitions(lv, diskLayout); err != nil { + return nil, fmt.Errorf("cannot remove partitions from previous install: %v", err) } // at this point we removed any existing partition, nuke any // of the existing sealed key files placed outside of the // encrypted partitions (LP: #1879338) - sealedKeyFiles, _ := filepath.Glob(filepath.Join(boot.InitramfsEncryptionKeyDir, "*.sealed-key")) + sealedKeyFiles, _ := filepath.Glob(filepath.Join(boot.InitramfsSeedEncryptionKeyDir, "*.sealed-key")) for _, keyFile := range sealedKeyFiles { if err := os.Remove(keyFile); err != nil && !os.IsNotExist(err) { - return fmt.Errorf("cannot cleanup obsolete key file: %v", keyFile) + return nil, fmt.Errorf("cannot cleanup obsolete key file: %v", keyFile) } } created, err := createMissingPartitions(diskLayout, lv) if err != nil { - return fmt.Errorf("cannot create the partitions: %v", err) + return nil, fmt.Errorf("cannot create the partitions: %v", err) } - // We're currently generating a single encryption key, this may change later - // if we create multiple encrypted partitions. - var key secboot.EncryptionKey - var rkey secboot.RecoveryKey - - if options.Encrypt { - key, err = secboot.NewEncryptionKey() + makeKeySet := func() (*EncryptionKeySet, error) { + key, err := secboot.NewEncryptionKey() if err != nil { - return fmt.Errorf("cannot create encryption key: %v", err) + return nil, fmt.Errorf("cannot create encryption key: %v", err) } - rkey, err = secboot.NewRecoveryKey() + rkey, err := secboot.NewRecoveryKey() if err != nil { - return fmt.Errorf("cannot create recovery key: %v", err) + return nil, fmt.Errorf("cannot create recovery key: %v", err) } + return &EncryptionKeySet{ + Key: key, + RecoveryKey: rkey, + }, nil + } + roleNeedsEncryption := func(role string) bool { + return role == gadget.SystemData || role == gadget.SystemSave } + var keysForRoles map[string]*EncryptionKeySet for _, part := range created { - if options.Encrypt && part.Role == gadget.SystemData { - dataPart, err := newEncryptedDevice(&part, key, ubuntuDataLabel) + roleFmt := "" + if part.Role != "" { + roleFmt = fmt.Sprintf("role %v", part.Role) + } + logger.Noticef("created new partition %v for structure %v (size %v) %s", + part.Node, part, part.Size.IECString(), roleFmt) + if options.Encrypt && roleNeedsEncryption(part.Role) { + keys, err := makeKeySet() + if err != nil { + return nil, err + } + logger.Noticef("encrypting partition device %v", part.Node) + dataPart, err := newEncryptedDevice(&part, keys.Key, part.Label) if err != nil { - return err + return nil, err } - if err := dataPart.AddRecoveryKey(key, rkey); err != nil { - return err + if err := dataPart.AddRecoveryKey(keys.Key, keys.RecoveryKey); err != nil { + return nil, err } // update the encrypted device node part.Node = dataPart.Node + if keysForRoles == nil { + keysForRoles = map[string]*EncryptionKeySet{} + } + keysForRoles[part.Role] = keys + logger.Noticef("encrypted device %v", part.Node) } if err := makeFilesystem(&part); err != nil { - return err + return nil, err } if err := writeContent(&part, gadgetRoot, observer); err != nil { - return err + return nil, err } if options.Mount && part.Label != "" && part.HasFilesystem() { if err := mountFilesystem(&part, boot.InitramfsRunMntDir); err != nil { - return err + return nil, err } } } - if !options.Encrypt { - return nil - } - - // ensure directories - for _, p := range []string{boot.InitramfsEncryptionKeyDir, boot.InstallHostFDEDataDir} { - if err := os.MkdirAll(p, 0755); err != nil { - return err - } - } - - // Write the recovery key - recoveryKeyFile := filepath.Join(boot.InstallHostFDEDataDir, "recovery.key") - if err := rkey.Save(recoveryKeyFile); err != nil { - return fmt.Errorf("cannot store recovery key: %v", err) - } + return &InstalledSystemSideData{ + KeysForRoles: keysForRoles, + }, nil +} - if observer != nil { - observer.ChosenEncryptionKey(key) +// isCreatableAtInstall returns whether the gadget structure would be created at +// install - currently that is only ubuntu-save, ubuntu-data, and ubuntu-boot +func isCreatableAtInstall(gv *gadget.VolumeStructure) bool { + // a structure is creatable at install if it is one of the roles for + // system-save, system-data, or system-boot + switch gv.Role { + case gadget.SystemSave, gadget.SystemData, gadget.SystemBoot: + return true + default: + return false } - - return nil } func ensureLayoutCompatibility(gadgetLayout *gadget.LaidOutVolume, diskLayout *gadget.OnDiskVolume) error { - eq := func(ds gadget.OnDiskStructure, gs gadget.LaidOutStructure) bool { + eq := func(ds gadget.OnDiskStructure, gs gadget.LaidOutStructure) (bool, string) { dv := ds.VolumeStructure gv := gs.VolumeStructure nameMatch := gv.Name == dv.Name if gadgetLayout.Schema == "mbr" { - // partitions have no names in MBR + // partitions have no names in MBR so bypass the name check nameMatch = true } - // Previous installation may have failed before filesystem creation or partition may be encrypted - check := nameMatch && ds.StartOffset == gs.StartOffset && (ds.CreatedDuringInstall || dv.Filesystem == gv.Filesystem) + // Previous installation may have failed before filesystem creation or + // partition may be encrypted, so if the on disk offset matches the + // gadget offset, and the gadget structure is creatable during install, + // then they are equal + // otherwise, if they are not created during installation, the + // filesystem must be the same + check := nameMatch && ds.StartOffset == gs.StartOffset && (isCreatableAtInstall(gv) || dv.Filesystem == gv.Filesystem) + sizeMatches := dv.Size == gv.Size if gv.Role == gadget.SystemData { // system-data may have been expanded - return check && dv.Size >= gv.Size + sizeMatches = dv.Size >= gv.Size + } + if check && sizeMatches { + return true, "" + } + switch { + case !nameMatch: + // don't return a reason if the names don't match + return false, "" + case ds.StartOffset != gs.StartOffset: + return false, fmt.Sprintf("start offsets do not match (disk: %d (%s) and gadget: %d (%s))", ds.StartOffset, ds.StartOffset.IECString(), gs.StartOffset, gs.StartOffset.IECString()) + case !isCreatableAtInstall(gv) && dv.Filesystem != gv.Filesystem: + return false, "filesystems do not match and the partition is not creatable at install" + case dv.Size < gv.Size: + return false, "on disk size is smaller than gadget size" + case gv.Role != gadget.SystemData && dv.Size > gv.Size: + return false, "on disk size is larger than gadget size (and the role should not be expanded)" + default: + return false, "some other logic condition (should be impossible?)" } - return check && dv.Size == gv.Size } - contains := func(haystack []gadget.LaidOutStructure, needle gadget.OnDiskStructure) bool { + + contains := func(haystack []gadget.LaidOutStructure, needle gadget.OnDiskStructure) (bool, string) { + reasonAbsent := "" for _, h := range haystack { - if eq(needle, h) { - return true + matches, reasonNotMatches := eq(needle, h) + if matches { + return true, "" + } + // this has the effect of only returning the last non-empty reason + // string + if reasonNotMatches != "" { + reasonAbsent = reasonNotMatches } } - return false + return false, reasonAbsent } if gadgetLayout.Size > diskLayout.Size { @@ -214,8 +265,14 @@ // Check if all existing device partitions are also in gadget for _, ds := range diskLayout.Structure { - if !contains(gadgetLayout.LaidOutStructure, ds) { - return fmt.Errorf("cannot find disk partition %s (starting at %d) in gadget", ds.Node, ds.StartOffset) + present, reasonAbsent := contains(gadgetLayout.LaidOutStructure, ds) + if !present { + if reasonAbsent != "" { + // use the right format so that it can be + // appended to the error message + reasonAbsent = fmt.Sprintf(": %s", reasonAbsent) + } + return fmt.Errorf("cannot find disk partition %s (starting at %d) in gadget%s", ds.Node, ds.StartOffset, reasonAbsent) } } diff -Nru snapd-2.47.1+20.10.1build1/gadget/install/install_test.go snapd-2.48+21.04/gadget/install/install_test.go --- snapd-2.47.1+20.10.1build1/gadget/install/install_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/gadget/install/install_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -31,6 +31,7 @@ "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/gadget" "github.com/snapcore/snapd/gadget/install" + "github.com/snapcore/snapd/gadget/quantity" "github.com/snapcore/snapd/testutil" ) @@ -57,8 +58,9 @@ } func (s *installSuite) TestInstallRunError(c *C) { - err := install.Run("", "", install.Options{}, nil) + sys, err := install.Run("", "", install.Options{}, nil) c.Assert(err, ErrorMatches, "cannot use empty gadget root directory") + c.Check(sys, IsNil) } const mockGadgetYaml = `volumes: @@ -100,9 +102,9 @@ LaidOutStructure: gadget.LaidOutStructure{ VolumeStructure: &gadget.VolumeStructure{ Name: "BIOS Boot", - Size: 1 * gadget.SizeMiB, + Size: 1 * quantity.SizeMiB, }, - StartOffset: 1 * gadget.SizeMiB, + StartOffset: 1 * quantity.SizeMiB, }, Node: "/dev/node2", }, @@ -110,7 +112,7 @@ ID: "anything", Device: "/dev/node", Schema: "gpt", - Size: 2 * gadget.SizeGiB, + Size: 2 * quantity.SizeGiB, SectorSize: 512, } @@ -131,10 +133,10 @@ LaidOutStructure: gadget.LaidOutStructure{ VolumeStructure: &gadget.VolumeStructure{ Name: "Extra partition", - Size: 10 * gadget.SizeMiB, + Size: 10 * quantity.SizeMiB, Label: "extra", }, - StartOffset: 2 * gadget.SizeMiB, + StartOffset: 2 * quantity.SizeMiB, }, Node: "/dev/node3", }, @@ -145,7 +147,7 @@ // layout is not compatible if the device is too small smallDeviceLayout := mockDeviceLayout - smallDeviceLayout.Size = 100 * gadget.SizeMiB + smallDeviceLayout.Size = 100 * quantity.SizeMiB // sanity check c.Check(gadgetLayoutWithExtras.Size > smallDeviceLayout.Size, Equals, true) err = install.EnsureLayoutCompatibility(gadgetLayoutWithExtras, &smallDeviceLayout) @@ -187,9 +189,9 @@ // partition names have no // meaning in MBR schema Name: "different BIOS Boot", - Size: 1 * gadget.SizeMiB, + Size: 1 * quantity.SizeMiB, }, - StartOffset: 1 * gadget.SizeMiB, + StartOffset: 1 * quantity.SizeMiB, }, Node: "/dev/node2", }, @@ -197,7 +199,7 @@ ID: "anything", Device: "/dev/node", Schema: "dos", - Size: 2 * gadget.SizeGiB, + Size: 2 * quantity.SizeGiB, SectorSize: 512, } gadgetLayout := layoutFromYaml(c, mockMBRGadgetYaml) @@ -215,12 +217,12 @@ VolumeStructure: &gadget.VolumeStructure{ // name is ignored with MBR schema Name: "Extra partition", - Size: 1200 * gadget.SizeMiB, + Size: 1200 * quantity.SizeMiB, Label: "extra", Filesystem: "ext4", Type: "83", }, - StartOffset: 2 * gadget.SizeMiB, + StartOffset: 2 * quantity.SizeMiB, }, Node: "/dev/node3", }, @@ -234,44 +236,75 @@ VolumeStructure: &gadget.VolumeStructure{ // name is ignored with MBR schema Name: "Extra extra partition", - Size: 1 * gadget.SizeMiB, + Size: 1 * quantity.SizeMiB, }, - StartOffset: 1202 * gadget.SizeMiB, + StartOffset: 1202 * quantity.SizeMiB, }, Node: "/dev/node4", }, ) err = install.EnsureLayoutCompatibility(gadgetLayoutWithExtras, &deviceLayoutWithExtras) - c.Assert(err, ErrorMatches, `cannot find disk partition /dev/node4 .* in gadget`) + c.Assert(err, ErrorMatches, `cannot find disk partition /dev/node4 \(starting at 1260388352\) in gadget: start offsets do not match \(disk: 1260388352 \(1.17 GiB\) and gadget: 2097152 \(2 MiB\)\)`) } func (s *installSuite) TestLayoutCompatibilityWithCreatedPartitions(c *C) { gadgetLayoutWithExtras := layoutFromYaml(c, mockGadgetYaml+mockExtraStructure) deviceLayout := mockDeviceLayout + // device matches gadget except for the filesystem type deviceLayout.Structure = append(deviceLayout.Structure, gadget.OnDiskStructure{ LaidOutStructure: gadget.LaidOutStructure{ VolumeStructure: &gadget.VolumeStructure{ Name: "Writable", - Size: 1200 * gadget.SizeMiB, + Size: 1200 * quantity.SizeMiB, Label: "writable", Filesystem: "something_else", }, - StartOffset: 2 * gadget.SizeMiB, + StartOffset: 2 * quantity.SizeMiB, }, - Node: "/dev/node3", - CreatedDuringInstall: true, + Node: "/dev/node3", }, ) err := install.EnsureLayoutCompatibility(gadgetLayoutWithExtras, &deviceLayout) c.Assert(err, IsNil) - // compare layouts without partitions created at install time (should fail) - deviceLayout.Structure[len(deviceLayout.Structure)-1].CreatedDuringInstall = false + // we are going to manipulate last structure, which has system-data role + c.Assert(gadgetLayoutWithExtras.Structure[len(deviceLayout.Structure)-1].Role, Equals, gadget.SystemData) + + // change the role for the laid out volume to not be a partition role that + // is created at install time (note that the duplicated seed role here is + // technically incorrect, you can't have duplicated roles, but this + // demonstrates that a structure that otherwise fits the bill but isn't a + // role that is created during install will fail the filesystem match check) + gadgetLayoutWithExtras.Structure[len(deviceLayout.Structure)-1].Role = gadget.SystemSeed + + // now we fail to find the /dev/node3 structure from the gadget on disk err = install.EnsureLayoutCompatibility(gadgetLayoutWithExtras, &deviceLayout) - c.Assert(err, ErrorMatches, `cannot find disk partition /dev/node3.* in gadget`) + c.Assert(err, ErrorMatches, `cannot find disk partition /dev/node3 \(starting at 2097152\) in gadget: filesystems do not match and the partition is not creatable at install`) + + // undo the role change + gadgetLayoutWithExtras.Structure[len(deviceLayout.Structure)-1].Role = gadget.SystemData + + // change the gadget size to be bigger than the on disk size + gadgetLayoutWithExtras.Structure[len(deviceLayout.Structure)-1].Size = 10000000 * quantity.SizeMiB + // now we fail to find the /dev/node3 structure from the gadget on disk because the gadget says it must be bigger + err = install.EnsureLayoutCompatibility(gadgetLayoutWithExtras, &deviceLayout) + c.Assert(err, ErrorMatches, `cannot find disk partition /dev/node3 \(starting at 2097152\) in gadget: on disk size is smaller than gadget size`) + + // change the gadget size to be smaller than the on disk size and the role to be one that is not expanded + gadgetLayoutWithExtras.Structure[len(deviceLayout.Structure)-1].Size = 1 * quantity.SizeMiB + gadgetLayoutWithExtras.Structure[len(deviceLayout.Structure)-1].Role = gadget.SystemBoot + + // now we fail because the gadget says it should be smaller and it can't be expanded + err = install.EnsureLayoutCompatibility(gadgetLayoutWithExtras, &deviceLayout) + c.Assert(err, ErrorMatches, `cannot find disk partition /dev/node3 \(starting at 2097152\) in gadget: on disk size is larger than gadget size \(and the role should not be expanded\)`) + + // but a smaller partition on disk for SystemData role is okay + gadgetLayoutWithExtras.Structure[len(deviceLayout.Structure)-1].Role = gadget.SystemData + err = install.EnsureLayoutCompatibility(gadgetLayoutWithExtras, &deviceLayout) + c.Assert(err, IsNil) } func (s *installSuite) TestSchemaCompatibility(c *C) { diff -Nru snapd-2.47.1+20.10.1build1/gadget/install/params.go snapd-2.48+21.04/gadget/install/params.go --- snapd-2.47.1+20.10.1build1/gadget/install/params.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/gadget/install/params.go 2020-11-19 16:51:02.000000000 +0000 @@ -20,7 +20,6 @@ package install import ( - "github.com/snapcore/snapd/gadget" "github.com/snapcore/snapd/secboot" ) @@ -31,9 +30,15 @@ Encrypt bool } -type SystemInstallObserver interface { - gadget.ContentObserver - // ChosenEncryptionKey stores the encrypted data partition key to be sealed - // at the end of the installation process. - ChosenEncryptionKey(secboot.EncryptionKey) +// EncryptionKeySet is a set of encryption keys. +type EncryptionKeySet struct { + Key secboot.EncryptionKey + RecoveryKey secboot.RecoveryKey +} + +// InstalledSystemSideData carries side data of an installed system, eg. secrets +// to access its partitions. +type InstalledSystemSideData struct { + // KeysForRoles contains key sets for the relevant structure roles. + KeysForRoles map[string]*EncryptionKeySet } diff -Nru snapd-2.47.1+20.10.1build1/gadget/install/partition.go snapd-2.48+21.04/gadget/install/partition.go --- snapd-2.47.1+20.10.1build1/gadget/install/partition.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/gadget/install/partition.go 2020-11-19 16:51:02.000000000 +0000 @@ -44,6 +44,8 @@ return created, nil } + logger.Debugf("create partitions on %s: %s", dl.Device, buf.String()) + // Write the partition table. By default sfdisk will try to re-read the // partition table with the BLKRRPART ioctl but will fail because the // kernel side rescan removes and adds partitions and we have partitions @@ -68,10 +70,10 @@ } // removeCreatedPartitions removes partitions added during a previous install. -func removeCreatedPartitions(dl *gadget.OnDiskVolume) error { +func removeCreatedPartitions(lv *gadget.LaidOutVolume, dl *gadget.OnDiskVolume) error { indexes := make([]string, 0, len(dl.Structure)) for i, s := range dl.Structure { - if s.CreatedDuringInstall { + if wasCreatedDuringInstall(lv, s) { logger.Noticef("partition %s was created during previous install", s.Node) indexes = append(indexes, strconv.Itoa(i+1)) } @@ -81,6 +83,7 @@ } // Delete disk partitions + logger.Debugf("delete disk partitions %v", indexes) cmd := exec.Command("sfdisk", append([]string{"--no-reread", "--delete", dl.Device}, indexes...)...) if output, err := cmd.CombinedOutput(); err != nil { return osutil.OutputErr(output, err) @@ -97,7 +100,7 @@ } // Ensure all created partitions were removed - if remaining := gadget.CreatedDuringInstall(dl); len(remaining) > 0 { + if remaining := createdDuringInstall(lv, dl); len(remaining) > 0 { return fmt.Errorf("cannot remove partitions: %s", strings.Join(remaining, ", ")) } @@ -150,3 +153,45 @@ } return nil } + +// wasCreatedDuringInstall returns if the OnDiskStructure was created during +// install by referencing the gadget volume. A structure is only considered to +// be created during install if it is a role that is created during install and +// the start offsets match. We specifically don't look at anything on the +// structure such as filesystem information since this may be incomplete due to +// a failed installation, or due to the partial layout that is created by some +// ARM tools (i.e. ptool and fastboot) when flashing images to internal MMC. +func wasCreatedDuringInstall(lv *gadget.LaidOutVolume, s gadget.OnDiskStructure) bool { + // for a structure to have been created during install, it must be one of + // the system-boot, system-data, or system-save roles from the gadget, and + // as such the on disk structure must exist in the exact same location as + // the role from the gadget, so only return true if the provided structure + // has the exact same StartOffset as one of those roles + for _, gs := range lv.LaidOutStructure { + // TODO: how to handle ubuntu-save here? maybe a higher level function + // should decide whether to delete it or not? + switch gs.Role { + case gadget.SystemSave, gadget.SystemData, gadget.SystemBoot: + // then it was created during install or is to be created during + // install, see if the offset matches the provided on disk structure + // has + if s.StartOffset == gs.StartOffset { + return true + } + } + } + + return false +} + +// createdDuringInstall returns a list of partitions created during the +// install process. +func createdDuringInstall(lv *gadget.LaidOutVolume, layout *gadget.OnDiskVolume) (created []string) { + created = make([]string, 0, len(layout.Structure)) + for _, s := range layout.Structure { + if wasCreatedDuringInstall(lv, s) { + created = append(created, s.Node) + } + } + return created +} diff -Nru snapd-2.47.1+20.10.1build1/gadget/install/partition_test.go snapd-2.48+21.04/gadget/install/partition_test.go --- snapd-2.47.1+20.10.1build1/gadget/install/partition_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/gadget/install/partition_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -30,13 +30,16 @@ "github.com/snapcore/snapd/gadget" "github.com/snapcore/snapd/gadget/install" + "github.com/snapcore/snapd/gadget/quantity" "github.com/snapcore/snapd/testutil" ) type partitionTestSuite struct { testutil.BaseTest - dir string + dir string + gadgetRoot string + cmdPartx *testutil.MockCmd } var _ = Suite(&partitionTestSuite{}) @@ -45,6 +48,15 @@ s.BaseTest.SetUpTest(c) s.dir = c.MkDir() + s.gadgetRoot = filepath.Join(c.MkDir(), "gadget") + + s.cmdPartx = testutil.MockCommand(c, "partx", "") + s.AddCleanup(s.cmdPartx.Restore) + + cmdSfdisk := testutil.MockCommand(c, "sfdisk", `echo "sfdisk was not mocked"; exit 1`) + s.AddCleanup(cmdSfdisk.Restore) + cmdLsblk := testutil.MockCommand(c, "lsblk", `echo "lsblk was not mocked"; exit 1`) + s.AddCleanup(cmdLsblk.Restore) } const ( @@ -90,8 +102,7 @@ "size": 2457600, "type": "C12A7328-F81F-11D2-BA4B-00A0C93EC93B", "uuid": "44C3D5C3-CAE1-4306-83E8-DF437ACDB32F", - "name": "Recovery", - "attrs": "GUID:59" + "name": "Recovery" }`) } @@ -104,8 +115,7 @@ "size": 2457600, "type": "0FC63DAF-8483-4772-8E79-3D69D8477DE4", "uuid": "f940029d-bfbb-4887-9d44-321e85c63866", - "name": "Writable", - "attrs": "GUID:59" + "name": "Writable" }`) } @@ -150,8 +160,7 @@ } var mockOnDiskStructureWritable = gadget.OnDiskStructure{ - Node: "/dev/node3", - CreatedDuringInstall: true, + Node: "/dev/node3", LaidOutStructure: gadget.LaidOutStructure{ VolumeStructure: &gadget.VolumeStructure{ Name: "Writable", @@ -164,6 +173,8 @@ StartOffset: 1260388352, Index: 3, }, + // expanded to fill the disk + Size: 2*quantity.SizeGiB + 845*quantity.SizeMiB + 1031680, } func (s *partitionTestSuite) TestCreatePartitions(c *C) { @@ -173,9 +184,6 @@ cmdLsblk := testutil.MockCommand(c, "lsblk", makeLsblkScript(scriptPartitionsBiosSeed)) defer cmdLsblk.Restore() - cmdPartx := testutil.MockCommand(c, "partx", "") - defer cmdPartx.Restore() - calls := 0 restore := install.MockEnsureNodesExist(func(ds []gadget.OnDiskStructure, timeout time.Duration) error { calls++ @@ -185,10 +193,9 @@ }) defer restore() - gadgetRoot := filepath.Join(c.MkDir(), "gadget") - err := makeMockGadget(gadgetRoot, gadgetContent) + err := makeMockGadget(s.gadgetRoot, gadgetContent) c.Assert(err, IsNil) - pv, err := gadget.PositionedVolumeFromGadget(gadgetRoot) + pv, err := gadget.PositionedVolumeFromGadget(s.gadgetRoot) c.Assert(err, IsNil) dl, err := gadget.OnDiskVolumeFromDevice("/dev/node") @@ -200,12 +207,12 @@ // Check partition table read and write c.Assert(cmdSfdisk.Calls(), DeepEquals, [][]string{ - {"sfdisk", "--json", "-d", "/dev/node"}, + {"sfdisk", "--json", "/dev/node"}, {"sfdisk", "--append", "--no-reread", "/dev/node"}, }) // Check partition table update - c.Assert(cmdPartx.Calls(), DeepEquals, [][]string{ + c.Assert(s.cmdPartx.Calls(), DeepEquals, [][]string{ {"partx", "-u", "/dev/node"}, }) } @@ -218,17 +225,23 @@ cmdLsblk := testutil.MockCommand(c, "lsblk", makeLsblkScript(scriptPartitionsBios)) defer cmdLsblk.Restore() - cmdPartx := testutil.MockCommand(c, "partx", "") - defer cmdPartx.Restore() + err := makeMockGadget(s.gadgetRoot, gadgetContent) + c.Assert(err, IsNil) + pv, err := gadget.PositionedVolumeFromGadget(s.gadgetRoot) + c.Assert(err, IsNil) dl, err := gadget.OnDiskVolumeFromDevice("/dev/node") c.Assert(err, IsNil) - err = install.RemoveCreatedPartitions(dl) + err = install.RemoveCreatedPartitions(pv, dl) c.Assert(err, IsNil) c.Assert(cmdSfdisk.Calls(), DeepEquals, [][]string{ - {"sfdisk", "--json", "-d", "/dev/node"}, + {"sfdisk", "--json", "/dev/node"}, + }) + + c.Assert(cmdLsblk.Calls(), DeepEquals, [][]string{ + {"lsblk", "--fs", "--json", "/dev/node1"}, }) } @@ -240,7 +253,10 @@ touch %[1]s/2 exit 0 else - PART=',{"node": "/dev/node2", "start": 4096, "size": 2457600, "type": "0FC63DAF-8483-4772-8E79-3D69D8477DE4", "uuid": "44C3D5C3-CAE1-4306-83E8-DF437ACDB32F", "name": "Recovery", "attrs": "GUID:59"}' + PART=', + {"node": "/dev/node2", "start": 4096, "size": 2457600, "type": "0FC63DAF-8483-4772-8E79-3D69D8477DE4", "uuid": "44C3D5C3-CAE1-4306-83E8-DF437ACDB32F", "name": "Recovery"}, + {"node": "/dev/node3", "start": 2461696, "size": 2457600, "type": "0FC63DAF-8483-4772-8E79-3D69D8477DE4", "uuid": "44C3D5C3-CAE1-4306-83E8-DF437ACDB32F", "name": "Recovery"} + ' touch %[1]s/1 fi echo '{ @@ -261,27 +277,30 @@ cmdSfdisk := testutil.MockCommand(c, "sfdisk", fmt.Sprintf(mockSfdiskScriptRemovablePartition, s.dir)) defer cmdSfdisk.Restore() - cmdLsblk := testutil.MockCommand(c, "lsblk", makeLsblkScript(scriptPartitionsBiosSeed)) + cmdLsblk := testutil.MockCommand(c, "lsblk", makeLsblkScript(scriptPartitionsBiosSeedData)) defer cmdLsblk.Restore() - cmdPartx := testutil.MockCommand(c, "partx", "") - defer cmdPartx.Restore() - dl, err := gadget.OnDiskVolumeFromDevice("/dev/node") c.Assert(err, IsNil) c.Assert(cmdLsblk.Calls(), DeepEquals, [][]string{ {"lsblk", "--fs", "--json", "/dev/node1"}, {"lsblk", "--fs", "--json", "/dev/node2"}, + {"lsblk", "--fs", "--json", "/dev/node3"}, }) - err = install.RemoveCreatedPartitions(dl) + err = makeMockGadget(s.gadgetRoot, gadgetContent) + c.Assert(err, IsNil) + pv, err := gadget.PositionedVolumeFromGadget(s.gadgetRoot) + c.Assert(err, IsNil) + + err = install.RemoveCreatedPartitions(pv, dl) c.Assert(err, IsNil) c.Assert(cmdSfdisk.Calls(), DeepEquals, [][]string{ - {"sfdisk", "--json", "-d", "/dev/node"}, - {"sfdisk", "--no-reread", "--delete", "/dev/node", "2"}, - {"sfdisk", "--json", "-d", "/dev/node"}, + {"sfdisk", "--json", "/dev/node"}, + {"sfdisk", "--no-reread", "--delete", "/dev/node", "3"}, + {"sfdisk", "--json", "/dev/node"}, }) } @@ -292,13 +311,15 @@ cmdLsblk := testutil.MockCommand(c, "lsblk", makeLsblkScript(scriptPartitionsBiosSeedData)) defer cmdLsblk.Restore() - cmdPartx := testutil.MockCommand(c, "partx", "") - defer cmdPartx.Restore() - dl, err := gadget.OnDiskVolumeFromDevice("node") c.Assert(err, IsNil) - err = install.RemoveCreatedPartitions(dl) + err = makeMockGadget(s.gadgetRoot, gadgetContent) + c.Assert(err, IsNil) + pv, err := gadget.PositionedVolumeFromGadget(s.gadgetRoot) + c.Assert(err, IsNil) + + err = install.RemoveCreatedPartitions(pv, dl) c.Assert(err, ErrorMatches, "cannot remove partitions: /dev/node3") } @@ -347,3 +368,240 @@ c.Assert(time.Since(t) >= timeout, Equals, true) c.Assert(cmdUdevadm.Calls(), HasLen, 0) } + +const gptGadgetContentWithSave = `volumes: + pc: + bootloader: grub + structure: + - name: mbr + type: mbr + size: 440 + content: + - image: pc-boot.img + - name: BIOS Boot + type: DA,21686148-6449-6E6F-744E-656564454649 + size: 1M + offset: 1M + offset-write: mbr+92 + content: + - image: pc-core.img + - name: Recovery + role: system-seed + filesystem: vfat + # UEFI will boot the ESP partition by default first + type: EF,C12A7328-F81F-11D2-BA4B-00A0C93EC93B + size: 1200M + content: + - source: grubx64.efi + target: EFI/boot/grubx64.efi + - name: Save + role: system-save + filesystem: ext4 + type: 83,0FC63DAF-8483-4772-8E79-3D69D8477DE4 + size: 128M + - name: Writable + role: system-data + filesystem: ext4 + type: 83,0FC63DAF-8483-4772-8E79-3D69D8477DE4 + size: 1200M +` + +func (s *partitionTestSuite) TestCreatedDuringInstallGPT(c *C) { + cmdLsblk := testutil.MockCommand(c, "lsblk", ` +case $3 in + /dev/node1) + echo '{ "blockdevices": [ {"fstype":"ext4", "label":null} ] }' + ;; + /dev/node2) + echo '{ "blockdevices": [ {"fstype":"ext4", "label":"ubuntu-seed"} ] }' + ;; + /dev/node3) + echo '{ "blockdevices": [ {"fstype":"ext4", "label":"ubuntu-save"} ] }' + ;; + /dev/node4) + echo '{ "blockdevices": [ {"fstype":"ext4", "label":"ubuntu-data"} ] }' + ;; + *) + echo "unexpected args: $*" + exit 1 + ;; +esac +`) + defer cmdLsblk.Restore() + cmdSfdisk := testutil.MockCommand(c, "sfdisk", ` +echo '{ + "partitiontable": { + "label": "gpt", + "id": "9151F25B-CDF0-48F1-9EDE-68CBD616E2CA", + "device": "/dev/node", + "unit": "sectors", + "firstlba": 34, + "lastlba": 8388574, + "partitions": [ + { + "node": "/dev/node1", + "start": 2048, + "size": 2048, + "type": "21686148-6449-6E6F-744E-656564454649", + "uuid": "30a26851-4b08-4b8d-8aea-f686e723ed8c", + "name": "BIOS boot partition" + }, + { + "node": "/dev/node2", + "start": 4096, + "size": 2457600, + "type": "C12A7328-F81F-11D2-BA4B-00A0C93EC93B", + "uuid": "7ea3a75a-3f6d-4647-8134-89ae61fe88d5", + "name": "Linux filesystem" + }, + { + "node": "/dev/node3", + "start": 2461696, + "size": 262144, + "type": "0fc63daf-8483-4772-8e79-3d69d8477de4", + "uuid": "641764aa-a680-4d36-a7ad-f7bd01fd8d12", + "name": "Linux filesystem" + }, + { + "node": "/dev/node4", + "start": 2723840, + "size": 2457600, + "type": "0fc63daf-8483-4772-8e79-3d69d8477de4", + "uuid": "8ab3e8fd-d53d-4d72-9c5e-56146915fd07", + "name": "Another Linux filesystem" + } + ] + } +}' +`) + defer cmdSfdisk.Restore() + + err := makeMockGadget(s.gadgetRoot, gptGadgetContentWithSave) + c.Assert(err, IsNil) + pv, err := gadget.PositionedVolumeFromGadget(s.gadgetRoot) + c.Assert(err, IsNil) + + dl, err := gadget.OnDiskVolumeFromDevice("node") + c.Assert(err, IsNil) + + list := install.CreatedDuringInstall(pv, dl) + // only save and writable should show up + c.Check(list, DeepEquals, []string{"/dev/node3", "/dev/node4"}) +} + +// this is an mbr gadget like the pi, but doesn't have the amd64 mbr structure +// so it's probably not representative, but still useful for unit tests here +const mbrGadgetContentWithSave = `volumes: + pc: + schema: mbr + bootloader: grub + structure: + - name: Recovery + role: system-seed + filesystem: vfat + type: EF,C12A7328-F81F-11D2-BA4B-00A0C93EC93B + offset: 2M + size: 1200M + content: + - source: grubx64.efi + target: EFI/boot/grubx64.efi + - name: Boot + role: system-boot + filesystem: ext4 + type: 83,0FC63DAF-8483-4772-8E79-3D69D8477DE4 + size: 1200M + - name: Save + role: system-save + filesystem: ext4 + type: 83,0FC63DAF-8483-4772-8E79-3D69D8477DE4 + size: 128M + - name: Writable + role: system-data + filesystem: ext4 + type: 83,0FC63DAF-8483-4772-8E79-3D69D8477DE4 + size: 1200M +` + +func (s *partitionTestSuite) TestCreatedDuringInstallMBR(c *C) { + cmdLsblk := testutil.MockCommand(c, "lsblk", ` +what= +shift 2 +case "$1" in + /dev/node1) + what='{"name": "node1", "fstype":"ext4", "label":"ubuntu-seed"}' + ;; + /dev/node2) + what='{"name": "node2", "fstype":"vfat", "label":"ubuntu-boot"}' + ;; + /dev/node3) + what='{"name": "node3", "fstype":"ext4", "label":"ubuntu-save"}' + ;; + /dev/node4) + what='{"name": "node4", "fstype":"ext4", "label":"ubuntu-data"}' + ;; + *) + echo "unexpected call" + exit 1 +esac + +cat < fartherstOffsetWrite { @@ -244,12 +246,12 @@ func (b byContentStartOffset) Swap(i, j int) { b[i], b[j] = b[j], b[i] } func (b byContentStartOffset) Less(i, j int) bool { return b[i].StartOffset < b[j].StartOffset } -func getImageSize(path string) (Size, error) { +func getImageSize(path string) (quantity.Size, error) { stat, err := os.Stat(path) if err != nil { return 0, err } - return Size(stat.Size()), nil + return quantity.Size(stat.Size()), nil } func layOutStructureContent(gadgetRootDir string, ps *LaidOutStructure, known map[string]*LaidOutStructure) ([]LaidOutContent, error) { @@ -262,7 +264,7 @@ } content := make([]LaidOutContent, len(ps.Content)) - previousEnd := Size(0) + previousEnd := quantity.Size(0) for idx, c := range ps.Content { imageSize, err := getImageSize(filepath.Join(gadgetRootDir, c.Image)) @@ -270,7 +272,7 @@ return nil, fmt.Errorf("cannot lay out structure %v: content %q: %v", ps, c.Image, err) } - var start Size + var start quantity.Size if c.Offset != nil { start = *c.Offset } else { @@ -318,12 +320,12 @@ return content, nil } -func resolveOffsetWrite(offsetWrite *RelativeOffset, knownStructs map[string]*LaidOutStructure) (*Size, error) { +func resolveOffsetWrite(offsetWrite *RelativeOffset, knownStructs map[string]*LaidOutStructure) (*quantity.Size, error) { if offsetWrite == nil { return nil, nil } - var relativeToOffset Size + var relativeToOffset quantity.Size if offsetWrite.RelativeTo != "" { otherStruct, ok := knownStructs[offsetWrite.RelativeTo] if !ok { @@ -338,16 +340,16 @@ // ShiftStructureTo translates the starting offset of a laid out structure and // its content to the provided offset. -func ShiftStructureTo(ps LaidOutStructure, offset Size) LaidOutStructure { +func ShiftStructureTo(ps LaidOutStructure, offset quantity.Size) LaidOutStructure { change := int64(offset - ps.StartOffset) newPs := ps - newPs.StartOffset = Size(int64(ps.StartOffset) + change) + newPs.StartOffset = quantity.Size(int64(ps.StartOffset) + change) newPs.LaidOutContent = make([]LaidOutContent, len(ps.LaidOutContent)) for idx, pc := range ps.LaidOutContent { newPc := pc - newPc.StartOffset = Size(int64(pc.StartOffset) + change) + newPc.StartOffset = quantity.Size(int64(pc.StartOffset) + change) newPs.LaidOutContent[idx] = newPc } return newPs diff -Nru snapd-2.47.1+20.10.1build1/gadget/layout_test.go snapd-2.48+21.04/gadget/layout_test.go --- snapd-2.47.1+20.10.1build1/gadget/layout_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/gadget/layout_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -30,6 +30,7 @@ "gopkg.in/yaml.v2" "github.com/snapcore/snapd/gadget" + "github.com/snapcore/snapd/gadget/quantity" ) type layoutTestSuite struct { @@ -43,14 +44,14 @@ } var defaultConstraints = gadget.LayoutConstraints{ - NonMBRStartOffset: 1 * gadget.SizeMiB, + NonMBRStartOffset: 1 * quantity.SizeMiB, SectorSize: 512, } func (p *layoutTestSuite) TestVolumeSize(c *C) { vol := gadget.Volume{ Structure: []gadget.VolumeStructure{ - {Size: 2 * gadget.SizeMiB}, + {Size: 2 * quantity.SizeMiB}, }, } v, err := gadget.LayoutVolume(p.dir, &vol, defaultConstraints) @@ -59,14 +60,14 @@ c.Assert(v, DeepEquals, &gadget.LaidOutVolume{ Volume: &gadget.Volume{ Structure: []gadget.VolumeStructure{ - {Size: 2 * gadget.SizeMiB}, + {Size: 2 * quantity.SizeMiB}, }, }, - Size: 3 * gadget.SizeMiB, + Size: 3 * quantity.SizeMiB, SectorSize: 512, RootDir: p.dir, LaidOutStructure: []gadget.LaidOutStructure{ - {VolumeStructure: &gadget.VolumeStructure{Size: 2 * gadget.SizeMiB}, StartOffset: 1 * gadget.SizeMiB}, + {VolumeStructure: &gadget.VolumeStructure{Size: 2 * quantity.SizeMiB}, StartOffset: 1 * quantity.SizeMiB}, }, }) } @@ -102,18 +103,18 @@ c.Assert(v, DeepEquals, &gadget.LaidOutVolume{ Volume: vol, - Size: 501 * gadget.SizeMiB, + Size: 501 * quantity.SizeMiB, SectorSize: 512, RootDir: p.dir, LaidOutStructure: []gadget.LaidOutStructure{ { VolumeStructure: &vol.Structure[0], - StartOffset: 1 * gadget.SizeMiB, + StartOffset: 1 * quantity.SizeMiB, Index: 0, }, { VolumeStructure: &vol.Structure[1], - StartOffset: 401 * gadget.SizeMiB, + StartOffset: 401 * quantity.SizeMiB, Index: 1, }, }, @@ -145,28 +146,28 @@ c.Assert(v, DeepEquals, &gadget.LaidOutVolume{ Volume: vol, - Size: 1101 * gadget.SizeMiB, + Size: 1101 * quantity.SizeMiB, SectorSize: 512, RootDir: p.dir, LaidOutStructure: []gadget.LaidOutStructure{ { VolumeStructure: &vol.Structure[0], - StartOffset: 1 * gadget.SizeMiB, + StartOffset: 1 * quantity.SizeMiB, Index: 0, }, { VolumeStructure: &vol.Structure[1], - StartOffset: 401 * gadget.SizeMiB, + StartOffset: 401 * quantity.SizeMiB, Index: 1, }, { VolumeStructure: &vol.Structure[2], - StartOffset: 901 * gadget.SizeMiB, + StartOffset: 901 * quantity.SizeMiB, Index: 2, }, { VolumeStructure: &vol.Structure[3], - StartOffset: 1001 * gadget.SizeMiB, + StartOffset: 1001 * quantity.SizeMiB, Index: 3, }, }, @@ -202,28 +203,28 @@ c.Assert(v, DeepEquals, &gadget.LaidOutVolume{ Volume: vol, - Size: 1300 * gadget.SizeMiB, + Size: 1300 * quantity.SizeMiB, SectorSize: 512, RootDir: p.dir, LaidOutStructure: []gadget.LaidOutStructure{ { VolumeStructure: &vol.Structure[3], - StartOffset: 1 * gadget.SizeMiB, + StartOffset: 1 * quantity.SizeMiB, Index: 3, }, { VolumeStructure: &vol.Structure[1], - StartOffset: 200 * gadget.SizeMiB, + StartOffset: 200 * quantity.SizeMiB, Index: 1, }, { VolumeStructure: &vol.Structure[0], - StartOffset: 800 * gadget.SizeMiB, + StartOffset: 800 * quantity.SizeMiB, Index: 0, }, { VolumeStructure: &vol.Structure[2], - StartOffset: 1200 * gadget.SizeMiB, + StartOffset: 1200 * quantity.SizeMiB, Index: 2, }, }, @@ -258,28 +259,28 @@ c.Assert(v, DeepEquals, &gadget.LaidOutVolume{ Volume: vol, - Size: 1200 * gadget.SizeMiB, + Size: 1200 * quantity.SizeMiB, SectorSize: 512, RootDir: p.dir, LaidOutStructure: []gadget.LaidOutStructure{ { VolumeStructure: &vol.Structure[3], - StartOffset: 1 * gadget.SizeMiB, + StartOffset: 1 * quantity.SizeMiB, Index: 3, }, { VolumeStructure: &vol.Structure[1], - StartOffset: 200 * gadget.SizeMiB, + StartOffset: 200 * quantity.SizeMiB, Index: 1, }, { VolumeStructure: &vol.Structure[2], - StartOffset: 700 * gadget.SizeMiB, + StartOffset: 700 * quantity.SizeMiB, Index: 2, }, { VolumeStructure: &vol.Structure[0], - StartOffset: 800 * gadget.SizeMiB, + StartOffset: 800 * quantity.SizeMiB, Index: 0, }, }, @@ -305,7 +306,7 @@ c.Assert(err, ErrorMatches, `cannot lay out structure #0: content "foo.img":.*no such file or directory`) } -func makeSizedFile(c *C, path string, size gadget.Size, content []byte) { +func makeSizedFile(c *C, path string, size quantity.Size, content []byte) { err := os.MkdirAll(filepath.Dir(path), 0755) c.Assert(err, IsNil) @@ -334,7 +335,7 @@ content: - image: foo.img ` - makeSizedFile(c, filepath.Join(p.dir, "foo.img"), gadget.SizeMiB+1, nil) + makeSizedFile(c, filepath.Join(p.dir, "foo.img"), quantity.SizeMiB+1, nil) vol := mustParseVolume(c, gadgetYaml, "first") @@ -356,13 +357,13 @@ - image: foo.img - image: bar.img ` - makeSizedFile(c, filepath.Join(p.dir, "foo.img"), gadget.SizeMiB+1, nil) - makeSizedFile(c, filepath.Join(p.dir, "bar.img"), gadget.SizeMiB+1, nil) + makeSizedFile(c, filepath.Join(p.dir, "foo.img"), quantity.SizeMiB+1, nil) + makeSizedFile(c, filepath.Join(p.dir, "bar.img"), quantity.SizeMiB+1, nil) vol := mustParseVolume(c, gadgetYaml, "first") constraints := gadget.LayoutConstraints{ - NonMBRStartOffset: 1 * gadget.SizeMiB, + NonMBRStartOffset: 1 * quantity.SizeMiB, SectorSize: 512, } v, err := gadget.LayoutVolume(p.dir, vol, constraints) @@ -384,7 +385,7 @@ # 512kB offset: 524288 ` - makeSizedFile(c, filepath.Join(p.dir, "foo.img"), gadget.SizeMiB, nil) + makeSizedFile(c, filepath.Join(p.dir, "foo.img"), quantity.SizeMiB, nil) vol := mustParseVolume(c, gadgetYaml, "first") @@ -406,13 +407,13 @@ - image: foo.img size: 1M ` - makeSizedFile(c, filepath.Join(p.dir, "foo.img"), gadget.SizeMiB+1, nil) + makeSizedFile(c, filepath.Join(p.dir, "foo.img"), quantity.SizeMiB+1, nil) vol := mustParseVolume(c, gadgetYaml, "first") v, err := gadget.LayoutVolume(p.dir, vol, defaultConstraints) c.Assert(v, IsNil) - c.Assert(err, ErrorMatches, fmt.Sprintf(`cannot lay out structure #0: content "foo.img" size %v is larger than declared %v`, gadget.SizeMiB+1, gadget.SizeMiB)) + c.Assert(err, ErrorMatches, fmt.Sprintf(`cannot lay out structure #0: content "foo.img" size %v is larger than declared %v`, quantity.SizeMiB+1, quantity.SizeMiB)) } func (p *layoutTestSuite) TestLayoutVolumeErrorsContentOverlap(c *C) { @@ -433,8 +434,8 @@ size: 1M offset: 0 ` - makeSizedFile(c, filepath.Join(p.dir, "foo.img"), gadget.SizeMiB, nil) - makeSizedFile(c, filepath.Join(p.dir, "bar.img"), gadget.SizeMiB, nil) + makeSizedFile(c, filepath.Join(p.dir, "foo.img"), quantity.SizeMiB, nil) + makeSizedFile(c, filepath.Join(p.dir, "bar.img"), quantity.SizeMiB, nil) vol := mustParseVolume(c, gadgetYaml, "first") @@ -460,8 +461,8 @@ size: 1M offset: 0 ` - makeSizedFile(c, filepath.Join(p.dir, "foo.img"), gadget.SizeMiB, nil) - makeSizedFile(c, filepath.Join(p.dir, "bar.img"), gadget.SizeMiB, nil) + makeSizedFile(c, filepath.Join(p.dir, "foo.img"), quantity.SizeMiB, nil) + makeSizedFile(c, filepath.Join(p.dir, "bar.img"), quantity.SizeMiB, nil) vol := mustParseVolume(c, gadgetYaml, "first") c.Assert(vol.Structure, HasLen, 1) @@ -471,24 +472,24 @@ c.Assert(err, IsNil) c.Assert(v, DeepEquals, &gadget.LaidOutVolume{ Volume: vol, - Size: 3 * gadget.SizeMiB, + Size: 3 * quantity.SizeMiB, SectorSize: 512, RootDir: p.dir, LaidOutStructure: []gadget.LaidOutStructure{ { VolumeStructure: &vol.Structure[0], - StartOffset: 1 * gadget.SizeMiB, + StartOffset: 1 * quantity.SizeMiB, LaidOutContent: []gadget.LaidOutContent{ { VolumeContent: &vol.Structure[0].Content[1], - StartOffset: 1 * gadget.SizeMiB, - Size: gadget.SizeMiB, + StartOffset: 1 * quantity.SizeMiB, + Size: quantity.SizeMiB, Index: 1, }, { VolumeContent: &vol.Structure[0].Content[0], - StartOffset: 2 * gadget.SizeMiB, - Size: gadget.SizeMiB, + StartOffset: 2 * quantity.SizeMiB, + Size: quantity.SizeMiB, Index: 0, }, }, @@ -512,8 +513,8 @@ - image: bar.img size: 1M ` - makeSizedFile(c, filepath.Join(p.dir, "foo.img"), gadget.SizeMiB, nil) - makeSizedFile(c, filepath.Join(p.dir, "bar.img"), gadget.SizeMiB, nil) + makeSizedFile(c, filepath.Join(p.dir, "foo.img"), quantity.SizeMiB, nil) + makeSizedFile(c, filepath.Join(p.dir, "bar.img"), quantity.SizeMiB, nil) vol := mustParseVolume(c, gadgetYaml, "first") c.Assert(vol.Structure, HasLen, 1) @@ -523,24 +524,24 @@ c.Assert(err, IsNil) c.Assert(v, DeepEquals, &gadget.LaidOutVolume{ Volume: vol, - Size: 3 * gadget.SizeMiB, + Size: 3 * quantity.SizeMiB, SectorSize: 512, RootDir: p.dir, LaidOutStructure: []gadget.LaidOutStructure{ { VolumeStructure: &vol.Structure[0], - StartOffset: 1 * gadget.SizeMiB, + StartOffset: 1 * quantity.SizeMiB, LaidOutContent: []gadget.LaidOutContent{ { VolumeContent: &vol.Structure[0].Content[0], - StartOffset: 1 * gadget.SizeMiB, - Size: gadget.SizeMiB, + StartOffset: 1 * quantity.SizeMiB, + Size: quantity.SizeMiB, Index: 0, }, { VolumeContent: &vol.Structure[0].Content[1], - StartOffset: 2 * gadget.SizeMiB, - Size: gadget.SizeMiB, + StartOffset: 2 * quantity.SizeMiB, + Size: quantity.SizeMiB, Index: 1, }, }, @@ -561,7 +562,7 @@ content: - image: foo.img ` - size1_5MiB := gadget.SizeMiB + gadget.SizeMiB/2 + size1_5MiB := quantity.SizeMiB + quantity.SizeMiB/2 makeSizedFile(c, filepath.Join(p.dir, "foo.img"), size1_5MiB, nil) vol := mustParseVolume(c, gadgetYaml, "first") @@ -572,17 +573,17 @@ c.Assert(err, IsNil) c.Assert(v, DeepEquals, &gadget.LaidOutVolume{ Volume: vol, - Size: 3 * gadget.SizeMiB, + Size: 3 * quantity.SizeMiB, SectorSize: 512, RootDir: p.dir, LaidOutStructure: []gadget.LaidOutStructure{ { VolumeStructure: &vol.Structure[0], - StartOffset: 1 * gadget.SizeMiB, + StartOffset: 1 * quantity.SizeMiB, LaidOutContent: []gadget.LaidOutContent{ { VolumeContent: &vol.Structure[0].Content[0], - StartOffset: 1 * gadget.SizeMiB, + StartOffset: 1 * quantity.SizeMiB, Size: size1_5MiB, }, }, @@ -615,13 +616,13 @@ c.Assert(err, IsNil) c.Assert(v, DeepEquals, &gadget.LaidOutVolume{ Volume: vol, - Size: 3 * gadget.SizeMiB, + Size: 3 * quantity.SizeMiB, SectorSize: 512, RootDir: p.dir, LaidOutStructure: []gadget.LaidOutStructure{ { VolumeStructure: &vol.Structure[0], - StartOffset: 1 * gadget.SizeMiB, + StartOffset: 1 * quantity.SizeMiB, }, }, }) @@ -655,7 +656,7 @@ c.Assert(err, IsNil) c.Assert(v, DeepEquals, &gadget.LaidOutVolume{ Volume: vol, - Size: 3 * gadget.SizeMiB, + Size: 3 * quantity.SizeMiB, SectorSize: 512, RootDir: p.dir, LaidOutStructure: []gadget.LaidOutStructure{ @@ -666,7 +667,7 @@ }, { VolumeStructure: &vol.Structure[1], - StartOffset: 1 * gadget.SizeMiB, + StartOffset: 1 * quantity.SizeMiB, Index: 1, }, }, @@ -675,14 +676,14 @@ // still valid constraints := gadget.LayoutConstraints{ // 512kiB - NonMBRStartOffset: 512 * gadget.SizeKiB, + NonMBRStartOffset: 512 * quantity.SizeKiB, SectorSize: 512, } v, err = gadget.LayoutVolume(p.dir, vol, constraints) c.Assert(err, IsNil) c.Assert(v, DeepEquals, &gadget.LaidOutVolume{ Volume: vol, - Size: 2*gadget.SizeMiB + 512*gadget.SizeKiB, + Size: 2*quantity.SizeMiB + 512*quantity.SizeKiB, SectorSize: 512, RootDir: p.dir, LaidOutStructure: []gadget.LaidOutStructure{ @@ -693,7 +694,7 @@ }, { VolumeStructure: &vol.Structure[1], - StartOffset: 512 * gadget.SizeKiB, + StartOffset: 512 * quantity.SizeKiB, Index: 1, }, }, @@ -710,7 +711,7 @@ c.Assert(err, IsNil) c.Assert(v, DeepEquals, &gadget.LaidOutVolume{ Volume: vol, - Size: 2*gadget.SizeMiB + 446, + Size: 2*quantity.SizeMiB + 446, SectorSize: 512, RootDir: p.dir, LaidOutStructure: []gadget.LaidOutStructure{ @@ -728,14 +729,14 @@ // sector size is properly recorded constraintsSector := gadget.LayoutConstraints{ - NonMBRStartOffset: 1 * gadget.SizeMiB, + NonMBRStartOffset: 1 * quantity.SizeMiB, SectorSize: 1024, } v, err = gadget.LayoutVolume(p.dir, vol, constraintsSector) c.Assert(err, IsNil) c.Assert(v, DeepEquals, &gadget.LaidOutVolume{ Volume: vol, - Size: 3 * gadget.SizeMiB, + Size: 3 * quantity.SizeMiB, SectorSize: 1024, RootDir: p.dir, LaidOutStructure: []gadget.LaidOutStructure{ @@ -745,7 +746,7 @@ }, { VolumeStructure: &vol.Structure[1], - StartOffset: 1 * gadget.SizeMiB, + StartOffset: 1 * quantity.SizeMiB, Index: 1, }, }, @@ -775,7 +776,7 @@ vol := mustParseVolume(c, gadgetYaml, "first") constraintsBadSectorSize := gadget.LayoutConstraints{ - NonMBRStartOffset: 1 * gadget.SizeMiB, + NonMBRStartOffset: 1 * quantity.SizeMiB, SectorSize: 384, } _, err := gadget.LayoutVolume(p.dir, vol, constraintsBadSectorSize) @@ -784,7 +785,7 @@ func (p *layoutTestSuite) TestLayoutVolumeConstraintsNeedsSectorSize(c *C) { constraintsBadSectorSize := gadget.LayoutConstraints{ - NonMBRStartOffset: 1 * gadget.SizeMiB, + NonMBRStartOffset: 1 * quantity.SizeMiB, // SectorSize left unspecified } _, err := gadget.LayoutVolume(p.dir, &gadget.Volume{}, constraintsBadSectorSize) @@ -813,7 +814,7 @@ c.Assert(err, IsNil) c.Assert(v, DeepEquals, &gadget.LaidOutVolume{ Volume: vol, - Size: 2 * gadget.SizeMiB, + Size: 2 * quantity.SizeMiB, SectorSize: 512, RootDir: p.dir, LaidOutStructure: []gadget.LaidOutStructure{ @@ -824,7 +825,7 @@ Index: 0, }, { VolumeStructure: &vol.Structure[1], - StartOffset: 1 * gadget.SizeMiB, + StartOffset: 1 * quantity.SizeMiB, Index: 1, }, }, @@ -856,8 +857,8 @@ - image: bar.img offset-write: 450 ` - makeSizedFile(c, filepath.Join(p.dir, "foo.img"), 200*gadget.SizeKiB, []byte("")) - makeSizedFile(c, filepath.Join(p.dir, "bar.img"), 150*gadget.SizeKiB, []byte("")) + makeSizedFile(c, filepath.Join(p.dir, "foo.img"), 200*quantity.SizeKiB, []byte("")) + makeSizedFile(c, filepath.Join(p.dir, "bar.img"), 150*quantity.SizeKiB, []byte("")) vol := mustParseVolume(c, gadgetYaml, "pc") c.Assert(vol.Structure, HasLen, 3) @@ -866,7 +867,7 @@ c.Assert(err, IsNil) c.Assert(v, DeepEquals, &gadget.LaidOutVolume{ Volume: vol, - Size: 3 * gadget.SizeMiB, + Size: 3 * quantity.SizeMiB, SectorSize: 512, RootDir: p.dir, LaidOutStructure: []gadget.LaidOutStructure{ @@ -878,31 +879,31 @@ }, { // foo VolumeStructure: &vol.Structure[1], - StartOffset: 1 * gadget.SizeMiB, + StartOffset: 1 * quantity.SizeMiB, Index: 1, // break for gofmt < 1.11 AbsoluteOffsetWrite: asSizePtr(92), LaidOutContent: []gadget.LaidOutContent{ { VolumeContent: &vol.Structure[1].Content[0], - Size: 200 * gadget.SizeKiB, - StartOffset: 1 * gadget.SizeMiB, + Size: 200 * quantity.SizeKiB, + StartOffset: 1 * quantity.SizeMiB, // offset-write: bar+10 - AbsoluteOffsetWrite: asSizePtr(2*gadget.SizeMiB + 10), + AbsoluteOffsetWrite: asSizePtr(2*quantity.SizeMiB + 10), }, }, }, { // bar VolumeStructure: &vol.Structure[2], - StartOffset: 2 * gadget.SizeMiB, + StartOffset: 2 * quantity.SizeMiB, Index: 2, // break for gofmt < 1.11 AbsoluteOffsetWrite: asSizePtr(600), LaidOutContent: []gadget.LaidOutContent{ { VolumeContent: &vol.Structure[2].Content[0], - Size: 150 * gadget.SizeKiB, - StartOffset: 2 * gadget.SizeMiB, + Size: 150 * quantity.SizeKiB, + StartOffset: 2 * quantity.SizeMiB, // offset-write: bar+10 AbsoluteOffsetWrite: asSizePtr(450), }, @@ -919,7 +920,7 @@ { Name: "foo", Type: "DA,21686148-6449-6E6F-744E-656564454649", - Size: 1 * gadget.SizeMiB, + Size: 1 * quantity.SizeMiB, OffsetWrite: &gadget.RelativeOffset{ RelativeTo: "bar", Offset: 10, @@ -932,7 +933,7 @@ { Name: "foo", Type: "DA,21686148-6449-6E6F-744E-656564454649", - Size: 1 * gadget.SizeMiB, + Size: 1 * quantity.SizeMiB, Content: []gadget.VolumeContent{ { Image: "foo.img", @@ -946,7 +947,7 @@ }, } - makeSizedFile(c, filepath.Join(p.dir, "foo.img"), 200*gadget.SizeKiB, []byte("")) + makeSizedFile(c, filepath.Join(p.dir, "foo.img"), 200*quantity.SizeKiB, []byte("")) v, err := gadget.LayoutVolume(p.dir, &volBadStructure, defaultConstraints) c.Check(v, IsNil) @@ -979,7 +980,7 @@ v, err := gadget.LayoutVolume(p.dir, vol, defaultConstraints) c.Assert(err, IsNil) // offset-write is at 1GB - c.Check(v.Size, Equals, 1*gadget.SizeGiB+gadget.SizeLBA48Pointer) + c.Check(v.Size, Equals, 1*quantity.SizeGiB+gadget.SizeLBA48Pointer) var gadgetYamlContent = ` volumes: @@ -1005,16 +1006,16 @@ offset-write: mbr+3221225472 ` - makeSizedFile(c, filepath.Join(p.dir, "foo.img"), 200*gadget.SizeKiB, []byte("")) - makeSizedFile(c, filepath.Join(p.dir, "bar.img"), 150*gadget.SizeKiB, []byte("")) - makeSizedFile(c, filepath.Join(p.dir, "baz.img"), 100*gadget.SizeKiB, []byte("")) + makeSizedFile(c, filepath.Join(p.dir, "foo.img"), 200*quantity.SizeKiB, []byte("")) + makeSizedFile(c, filepath.Join(p.dir, "bar.img"), 150*quantity.SizeKiB, []byte("")) + makeSizedFile(c, filepath.Join(p.dir, "baz.img"), 100*quantity.SizeKiB, []byte("")) vol = mustParseVolume(c, gadgetYamlContent, "pc") v, err = gadget.LayoutVolume(p.dir, vol, defaultConstraints) c.Assert(err, IsNil) // foo.img offset-write is at 3GB - c.Check(v.Size, Equals, 3*gadget.SizeGiB+gadget.SizeLBA48Pointer) + c.Check(v.Size, Equals, 3*quantity.SizeGiB+gadget.SizeLBA48Pointer) } func (p *layoutTestSuite) TestLayoutVolumePartialNoSuchFile(c *C) { @@ -1040,7 +1041,7 @@ LaidOutStructure: []gadget.LaidOutStructure{ { VolumeStructure: &vol.Structure[0], - StartOffset: 800 * gadget.SizeMiB, + StartOffset: 800 * quantity.SizeMiB, Index: 0, }, }, @@ -1065,8 +1066,8 @@ offset: 307200 ` - makeSizedFile(c, filepath.Join(p.dir, "foo.img"), 200*gadget.SizeKiB, []byte("")) - makeSizedFile(c, filepath.Join(p.dir, "bar.img"), 150*gadget.SizeKiB, []byte("")) + makeSizedFile(c, filepath.Join(p.dir, "foo.img"), 200*quantity.SizeKiB, []byte("")) + makeSizedFile(c, filepath.Join(p.dir, "bar.img"), 150*quantity.SizeKiB, []byte("")) vol := mustParseVolume(c, gadgetYamlContent, "pc") @@ -1080,18 +1081,18 @@ c.Assert(ps, DeepEquals, gadget.LaidOutStructure{ // foo VolumeStructure: &vol.Structure[0], - StartOffset: 1 * gadget.SizeMiB, + StartOffset: 1 * quantity.SizeMiB, Index: 0, LaidOutContent: []gadget.LaidOutContent{ { VolumeContent: &vol.Structure[0].Content[0], - Size: 200 * gadget.SizeKiB, - StartOffset: 1 * gadget.SizeMiB, + Size: 200 * quantity.SizeKiB, + StartOffset: 1 * quantity.SizeMiB, Index: 0, }, { VolumeContent: &vol.Structure[0].Content[1], - Size: 150 * gadget.SizeKiB, - StartOffset: 1*gadget.SizeMiB + 300*gadget.SizeKiB, + Size: 150 * quantity.SizeKiB, + StartOffset: 1*quantity.SizeMiB + 300*quantity.SizeKiB, Index: 1, }, }, @@ -1106,34 +1107,34 @@ LaidOutContent: []gadget.LaidOutContent{ { VolumeContent: &vol.Structure[0].Content[0], - Size: 200 * gadget.SizeKiB, + Size: 200 * quantity.SizeKiB, StartOffset: 0, Index: 0, }, { VolumeContent: &vol.Structure[0].Content[1], - Size: 150 * gadget.SizeKiB, - StartOffset: 300 * gadget.SizeKiB, + Size: 150 * quantity.SizeKiB, + StartOffset: 300 * quantity.SizeKiB, Index: 1, }, }, }) - shiftedTo2M := gadget.ShiftStructureTo(ps, 2*gadget.SizeMiB) + shiftedTo2M := gadget.ShiftStructureTo(ps, 2*quantity.SizeMiB) c.Assert(shiftedTo2M, DeepEquals, gadget.LaidOutStructure{ // foo VolumeStructure: &vol.Structure[0], - StartOffset: 2 * gadget.SizeMiB, + StartOffset: 2 * quantity.SizeMiB, Index: 0, LaidOutContent: []gadget.LaidOutContent{ { VolumeContent: &vol.Structure[0].Content[0], - Size: 200 * gadget.SizeKiB, - StartOffset: 2 * gadget.SizeMiB, + Size: 200 * quantity.SizeKiB, + StartOffset: 2 * quantity.SizeMiB, Index: 0, }, { VolumeContent: &vol.Structure[0].Content[1], - Size: 150 * gadget.SizeKiB, - StartOffset: 2*gadget.SizeMiB + 300*gadget.SizeKiB, + Size: 150 * quantity.SizeKiB, + StartOffset: 2*quantity.SizeMiB + 300*quantity.SizeKiB, Index: 1, }, }, diff -Nru snapd-2.47.1+20.10.1build1/gadget/mountedfilesystem.go snapd-2.48+21.04/gadget/mountedfilesystem.go --- snapd-2.47.1+20.10.1build1/gadget/mountedfilesystem.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/gadget/mountedfilesystem.go 2020-11-19 16:51:02.000000000 +0000 @@ -31,7 +31,6 @@ "sort" "strings" - "github.com/snapcore/snapd/bootloader" "github.com/snapcore/snapd/logger" "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/strutil" @@ -57,21 +56,20 @@ return nil } -func observe(observer ContentObserver, op ContentOperation, ps *LaidOutStructure, root, dst string, data *ContentChange) error { +func observe(observer ContentObserver, op ContentOperation, ps *LaidOutStructure, root, dst string, data *ContentChange) (ContentChangeAction, error) { if observer == nil { - return nil + return ChangeApply, nil } relativeTarget := dst if strings.HasPrefix(dst, root) { // target path isn't really relative, make it so now relative, err := filepath.Rel(root, dst) if err != nil { - return err + return ChangeAbort, err } relativeTarget = relative } - _, err := observer.Observe(op, ps, root, relativeTarget, data) - return err + return observer.Observe(op, ps, root, relativeTarget, data) } // TODO: MountedFilesystemWriter should not be exported @@ -197,9 +195,13 @@ // with content in this file After: src, } - if err := observe(m.observer, ContentWrite, m.ps, volumeRoot, dst, data); err != nil { + act, err := observe(m.observer, ContentWrite, m.ps, volumeRoot, dst, data) + if err != nil { return fmt.Errorf("cannot observe file write: %v", err) } + if act == ChangeIgnore { + return nil + } return writeFileOrSymlink(src, dst, preserveInDst) } @@ -301,10 +303,9 @@ // backup copies of files, newly created directories are removed type mountedFilesystemUpdater struct { *MountedFilesystemWriter - backupDir string - mountPoint string - managedBootAssets []string - updateObserver ContentObserver + backupDir string + mountPoint string + updateObserver ContentObserver } // newMountedFilesystemUpdater returns an updater for given filesystem @@ -327,17 +328,11 @@ if err != nil { return nil, fmt.Errorf("cannot find mount location of structure %v: %v", ps, err) } - // find out if we need to preserve any boot assets - bootAssets, err := maybePreserveManagedBootAssets(mount, ps) - if err != nil { - return nil, fmt.Errorf("cannot preserve managed boot assets: %v", err) - } fu := &mountedFilesystemUpdater{ MountedFilesystemWriter: fw, backupDir: backupDir, mountPoint: mount, - managedBootAssets: bootAssets, updateObserver: observer, } return fu, nil @@ -347,52 +342,6 @@ return filepath.Join(backupDir, fmt.Sprintf("struct-%v", ps.Index)) } -func maybePreserveManagedBootAssets(mountPoint string, ps *LaidOutStructure) ([]string, error) { - if ps.Role != SystemSeed && ps.Role != SystemBoot { - return nil, nil - } - // TODO:UC20: this code should no longer be needed when we use - // the updater interface from boot, in particular we shouldn't - // try to find a bootloader from this place, we don't have - // quite enough info not to guess - - // the assets are within the system-boot or system-seed partition, set - // the right flags so that files are looked for using their paths inside - // the partition - role := bootloader.RoleRecovery - if ps.Role == SystemBoot { - role = bootloader.RoleRunMode - } - opts := &bootloader.Options{ - Role: role, - NoSlashBoot: true, - } - bl, err := bootloader.Find(mountPoint, opts) - if err != nil { - if err == bootloader.ErrBootloader { - // no bootloader in the partition? - return nil, nil - } - return nil, err - } - - mbl, ok := bl.(bootloader.ManagedAssetsBootloader) - if !ok { - // bootloader implementation does not support managing its - // assets - return nil, nil - } - managed, err := mbl.IsCurrentlyManaged() - if err != nil { - return nil, err - } - if !managed { - // assets are not managed - return nil, nil - } - return mbl.ManagedAssets(), nil -} - // entryDestPaths resolves destination and backup paths for given // source/target combination. Backup location is within provided // backup directory or empty if directory was not provided. @@ -426,8 +375,7 @@ // Update applies an update to a mounted filesystem. The caller must have // executed a Backup() before, to prepare a data set for rollback purpose. func (f *mountedFilesystemUpdater) Update() error { - preserveInDst, err := mapPreserve(f.mountPoint, - append(f.ps.Update.Preserve, f.managedBootAssets...)) + preserveInDst, err := mapPreserve(f.mountPoint, f.ps.Update.Preserve) if err != nil { return fmt.Errorf("cannot map preserve entries for mount location %q: %v", f.mountPoint, err) } @@ -525,25 +473,34 @@ func (f *mountedFilesystemUpdater) updateOrSkipFile(dstRoot, source, target string, preserveInDst []string, backupDir string) error { srcPath := f.entrySourcePath(source) dstPath, backupPath := f.entryDestPaths(dstRoot, source, target, backupDir) + backupName := backupPath + ".backup" + sameStamp := backupPath + ".same" + preserveStamp := backupPath + ".preserve" + ignoreStamp := backupPath + ".ignore" // TODO: enable support for symlinks when needed if osutil.IsSymlink(srcPath) { return fmt.Errorf("cannot update file %s: symbolic links are not supported", source) } + if osutil.FileExists(ignoreStamp) { + // explicitly ignored by request of the observer + return ErrNoUpdate + } + if osutil.FileExists(dstPath) { - if strutil.SortedListContains(preserveInDst, dstPath) { + if strutil.SortedListContains(preserveInDst, dstPath) || osutil.FileExists(preserveStamp) { // file is to be preserved return ErrNoUpdate } - if osutil.FileExists(backupPath + ".same") { + if osutil.FileExists(sameStamp) { // file is the same as current copy return ErrNoUpdate } - if !osutil.FileExists(backupPath + ".backup") { + if !osutil.FileExists(backupName) { // not preserved & different than the update, error out // as there is no backup - return fmt.Errorf("missing backup file %q for %v", backupPath+".backup", target) + return fmt.Errorf("missing backup file %q for %v", backupName, target) } } @@ -570,7 +527,7 @@ // identical/preserved files may be stamped to improve the later step of update // process. // -// The backup directory structure mirrors the the structure of destination +// The backup directory structure mirrors the structure of destination // location. Given the following destination structure: // // foo @@ -580,7 +537,8 @@ // │ ├── baz // │ │ └── d // │ └── z -// └── c +// ├── c +// └── d // // The structure of backup looks like this: // @@ -592,7 +550,8 @@ // │ └── baz.backup <-- stamp indicating ./bar/baz existed before the update // ├── bar.backup <-- stamp indicating ./bar existed before the update // ├── b.same <-- stamp indicating ./b is identical to the update data -// └── c.preserve <-- stamp indicating ./c is to be preserved +// ├── c.ignore <-- stamp indicating change to ./c was requested to be ignored +// └── d.preserve <-- stamp indicating ./d is to be preserved // func (f *mountedFilesystemUpdater) Backup() error { backupRoot := fsStructBackupPath(f.backupDir, f.ps) @@ -601,8 +560,7 @@ return fmt.Errorf("cannot create backup directory: %v", err) } - preserveInDst, err := mapPreserve(f.mountPoint, - append(f.ps.Update.Preserve, f.managedBootAssets...)) + preserveInDst, err := mapPreserve(f.mountPoint, f.ps.Update.Preserve) if err != nil { return fmt.Errorf("cannot map preserve entries for mount location %q: %v", f.mountPoint, err) } @@ -677,16 +635,39 @@ return nil } +func (f *mountedFilesystemUpdater) ignoreChange(change *ContentChange, backupPath string) error { + preserveStamp := backupPath + ".preserve" + backupName := backupPath + ".backup" + sameStamp := backupPath + ".same" + ignoreStamp := backupPath + ".ignore" + if err := makeStamp(ignoreStamp); err != nil { + return fmt.Errorf("cannot create a checkpoint file: %v", err) + } + for _, name := range []string{backupName, sameStamp, preserveStamp} { + if err := os.Remove(name); err != nil && !os.IsNotExist(err) { + return fmt.Errorf("cannot remove existing stamp file: %v", err) + } + } + return nil +} + func (f *mountedFilesystemUpdater) observedBackupOrCheckpointFile(dstRoot, source, target string, preserveInDst []string, backupDir string) error { change, err := f.backupOrCheckpointFile(dstRoot, source, target, preserveInDst, backupDir) if err != nil { return err } if change != nil { - dstPath, _ := f.entryDestPaths(dstRoot, source, target, backupDir) - if err := observe(f.updateObserver, ContentUpdate, f.ps, f.mountPoint, dstPath, change); err != nil { + dstPath, backupPath := f.entryDestPaths(dstRoot, source, target, backupDir) + act, err := observe(f.updateObserver, ContentUpdate, f.ps, f.mountPoint, dstPath, change) + if err != nil { return fmt.Errorf("cannot observe pending file write %v\n", err) } + if act == ChangeIgnore { + // observer asked for the change to be ignored + if err := f.ignoreChange(change, backupPath); err != nil { + return fmt.Errorf("cannot ignore content change: %v", err) + } + } } return nil } @@ -704,6 +685,7 @@ backupName := backupPath + ".backup" sameStamp := backupPath + ".same" preserveStamp := backupPath + ".preserve" + ignoreStamp := backupPath + ".ignore" changeWithBackup := &ContentChange{ // content of the new data @@ -717,6 +699,12 @@ After: srcPath, } + if osutil.FileExists(ignoreStamp) { + // observer already requested the change to the target location + // to be ignored + return nil, nil + } + // TODO: enable support for symlinks when needed if osutil.IsSymlink(dstPath) { return nil, fmt.Errorf("cannot backup file %s: symbolic links are not supported", target) @@ -727,6 +715,7 @@ // the udpate, no need for backup return changeNewFile, nil } + // destination file exists beyond this point if osutil.FileExists(backupName) { // file already checked and backed up @@ -740,11 +729,6 @@ // executed update pass if strutil.SortedListContains(preserveInDst, dstPath) { - // file is to be preserved, create a relevant stamp - if !osutil.FileExists(dstPath) { - // preserve, but does not exist, will be written anyway - return changeNewFile, nil - } if osutil.FileExists(preserveStamp) { // already stamped return nil, nil @@ -835,8 +819,7 @@ func (f *mountedFilesystemUpdater) Rollback() error { backupRoot := fsStructBackupPath(f.backupDir, f.ps) - preserveInDst, err := mapPreserve(f.mountPoint, - append(f.ps.Update.Preserve, f.managedBootAssets...)) + preserveInDst, err := mapPreserve(f.mountPoint, f.ps.Update.Preserve) if err != nil { return fmt.Errorf("cannot map preserve entries for mount location %q: %v", f.mountPoint, err) } @@ -900,15 +883,22 @@ backupName := backupPath + ".backup" sameStamp := backupPath + ".same" preserveStamp := backupPath + ".preserve" + ignoreStamp := backupPath + ".ignore" if strutil.SortedListContains(preserveInDst, dstPath) && osutil.FileExists(preserveStamp) { - // file was preserved at original location, do nothing + // file was preserved at original location by being + // explicitly listed return nil } if osutil.FileExists(sameStamp) { // contents are the same as original, do nothing return nil } + if osutil.FileExists(ignoreStamp) { + // observer requested the changes to the target to be ignored + // previously + return nil + } data := &ContentChange{ After: srcPath, @@ -922,8 +912,6 @@ return err } } else { - // none of the markers exists, file is not preserved, meaning, it has - // been added by the update if err := os.Remove(dstPath); err != nil && !os.IsNotExist(err) { return fmt.Errorf("cannot remove written update: %v", err) } @@ -932,7 +920,8 @@ } // avoid passing source path during rollback, the file has been restored // to the disk already - if err := observe(f.updateObserver, ContentRollback, f.ps, f.mountPoint, dstPath, data); err != nil { + _, err := observe(f.updateObserver, ContentRollback, f.ps, f.mountPoint, dstPath, data) + if err != nil { return fmt.Errorf("cannot observe pending file rollback %v\n", err) } diff -Nru snapd-2.47.1+20.10.1build1/gadget/mountedfilesystem_test.go snapd-2.48+21.04/gadget/mountedfilesystem_test.go --- snapd-2.47.1+20.10.1build1/gadget/mountedfilesystem_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/gadget/mountedfilesystem_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -28,9 +28,9 @@ . "gopkg.in/check.v1" - "github.com/snapcore/snapd/bootloader" "github.com/snapcore/snapd/gadget" "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/strutil" "github.com/snapcore/snapd/testutil" ) @@ -117,7 +117,10 @@ cleanWhere := filepath.Clean(where) got := make(map[string]contentType) - filepath.Walk(where, func(name string, fi os.FileInfo, err error) error { + err := filepath.Walk(where, func(name string, fi os.FileInfo, err error) error { + if err != nil { + return err + } if name == where { return nil } @@ -134,8 +137,12 @@ return nil }) - - c.Assert(got, DeepEquals, expected) + c.Assert(err, IsNil) + if len(expected) > 0 { + c.Assert(got, DeepEquals, expected) + } else { + c.Assert(got, HasLen, 0) + } } func (s *mountedfilesystemTestSuite) TestWriteFile(c *C) { @@ -250,14 +257,18 @@ } type mockWriteObserver struct { - content map[string][]*mockContentChange - observeErr error - expectedStruct *gadget.LaidOutStructure - c *C + content map[string][]*mockContentChange + preserveTargets []string + observeErr error + expectedStruct *gadget.LaidOutStructure + c *C } func (m *mockWriteObserver) Observe(op gadget.ContentOperation, sourceStruct *gadget.LaidOutStructure, - targetRootDir, relativeTargetPath string, data *gadget.ContentChange) (bool, error) { + targetRootDir, relativeTargetPath string, data *gadget.ContentChange) (gadget.ContentChangeAction, error) { + if m.c == nil { + panic("c is unset") + } m.c.Assert(data, NotNil) m.c.Assert(op, Equals, gadget.ContentWrite, Commentf("unexpected operation %v", op)) if m.content == nil { @@ -276,7 +287,11 @@ m.c.Assert(sourceStruct, NotNil) m.c.Check(m.expectedStruct, DeepEquals, sourceStruct) - return true, m.observeErr + + if strutil.ListContains(m.preserveTargets, relativeTargetPath) { + return gadget.ChangeIgnore, nil + } + return gadget.ChangeApply, m.observeErr } func (s *mountedfilesystemTestSuite) TestMountedWriterHappy(c *C) { @@ -440,6 +455,10 @@ } func (s *mountedfilesystemTestSuite) TestMountedWriterErrorBadDestination(c *C) { + if os.Geteuid() == 0 { + c.Skip("the test cannot be run by the root user") + } + makeSizedFile(c, filepath.Join(s.dir, "foo"), 0, []byte("foo foo foo")) ps := &gadget.LaidOutStructure{ @@ -662,6 +681,52 @@ verifyWrittenGadgetData(c, outDir, gdWritten) } +func (s *mountedfilesystemTestSuite) TestMountedWriterPreserveWithObserver(c *C) { + // some data for the gadget + gd := []gadgetData{ + {name: "foo", target: "foo-dir/foo", content: "foo from gadget"}, + } + makeGadgetData(c, s.dir, gd) + + outDir := filepath.Join(c.MkDir(), "out-dir") + makeSizedFile(c, filepath.Join(outDir, "foo"), 0, []byte("foo from disk")) + + ps := &gadget.LaidOutStructure{ + VolumeStructure: &gadget.VolumeStructure{ + Size: 2048, + Filesystem: "ext4", + Content: []gadget.VolumeContent{ + { + Source: "foo", + // would overwrite existing foo + Target: "foo", + }, { + Source: "foo", + // does not exist + Target: "foo-new", + }, + }, + }, + } + + obs := &mockWriteObserver{ + c: c, + expectedStruct: ps, + preserveTargets: []string{ + "foo", + "foo-new", + }, + } + rw, err := gadget.NewMountedFilesystemWriter(s.dir, ps, obs) + c.Assert(err, IsNil) + c.Assert(rw, NotNil) + + err = rw.Write(outDir, nil) + c.Assert(err, IsNil) + c.Check(filepath.Join(outDir, "foo"), testutil.FileEquals, "foo from disk") + c.Check(filepath.Join(outDir, "foo-new"), testutil.FileAbsent) +} + func (s *mountedfilesystemTestSuite) TestMountedWriterNonFilePreserveError(c *C) { // some data for the gadget gd := []gadgetData{ @@ -833,19 +898,29 @@ type mockContentUpdateObserver struct { contentUpdate map[string][]*mockContentChange contentRollback map[string][]*mockContentChange + preserveTargets []string observeErr error expectedStruct *gadget.LaidOutStructure c *C } +func (m *mockContentUpdateObserver) reset() { + m.contentUpdate = nil + m.contentRollback = nil +} + func (m *mockContentUpdateObserver) Observe(op gadget.ContentOperation, sourceStruct *gadget.LaidOutStructure, - targetRootDir, relativeTargetPath string, data *gadget.ContentChange) (bool, error) { + targetRootDir, relativeTargetPath string, data *gadget.ContentChange) (gadget.ContentChangeAction, error) { + if m.c == nil { + panic("c is unset") + } if m.contentUpdate == nil { m.contentUpdate = make(map[string][]*mockContentChange) } if m.contentRollback == nil { m.contentRollback = make(map[string][]*mockContentChange) } + m.c.Assert(data, NotNil) // the after content must always be set m.c.Check(osutil.FileExists(data.After) && !osutil.IsDirectory(data.After), Equals, true, @@ -870,7 +945,14 @@ m.c.Assert(sourceStruct, NotNil) m.c.Check(m.expectedStruct, DeepEquals, sourceStruct) - return true, m.observeErr + + if m.observeErr != nil { + return gadget.ChangeAbort, m.observeErr + } + if strutil.ListContains(m.preserveTargets, relativeTargetPath) { + return gadget.ChangeIgnore, nil + } + return gadget.ChangeApply, nil } func (s *mountedfilesystemTestSuite) TestMountedUpdaterBackupSimple(c *C) { @@ -1156,6 +1238,10 @@ } func (s *mountedfilesystemTestSuite) TestMountedUpdaterBackupFailsOnBackupDirErrors(c *C) { + if os.Geteuid() == 0 { + c.Skip("the test cannot be run by the root user") + } + outDir := filepath.Join(c.MkDir(), "out-dir") ps := &gadget.LaidOutStructure{ @@ -1190,6 +1276,10 @@ } func (s *mountedfilesystemTestSuite) TestMountedUpdaterBackupFailsOnDestinationErrors(c *C) { + if os.Geteuid() == 0 { + c.Skip("the test cannot be run by the root user") + } + // some data for the gadget gd := []gadgetData{ {name: "bar", content: "data"}, @@ -1232,6 +1322,10 @@ } func (s *mountedfilesystemTestSuite) TestMountedUpdaterBackupFailsOnBadSrcComparison(c *C) { + if os.Geteuid() == 0 { + c.Skip("the test cannot be run by the root user") + } + // some data for the gadget gd := []gadgetData{ {name: "bar", content: "data"}, @@ -2257,6 +2351,10 @@ } func (s *mountedfilesystemTestSuite) TestMountedUpdaterRollbackRestoreFails(c *C) { + if os.Geteuid() == 0 { + c.Skip("the test cannot be run by the root user") + } + makeGadgetData(c, s.dir, []gadgetData{ {name: "bar", content: "data"}, }) @@ -2512,6 +2610,10 @@ {name: "foo", target: "/foo", content: "data"}, {name: "boot-assets/some-dir/data", target: "data-copy", content: "data"}, {name: "boot-assets/nested-dir/nested", target: "/nested-copy/nested", content: "data"}, + {name: "preserved/same-content", target: "preserved/same-content", content: "can't touch this"}, + } + gdSameContent := []gadgetData{ + {name: "foo", target: "/foo-same", content: "data"}, } makeGadgetData(c, s.dir, append(gdWritten, gdNotWritten...)) err := os.MkdirAll(filepath.Join(s.dir, "boot-assets/empty-dir"), 0755) @@ -2522,12 +2624,15 @@ makeExistingData(c, outDir, []gadgetData{ {target: "dtb", content: "updated"}, {target: "foo", content: "can't touch this"}, + {target: "foo-same", content: "data"}, {target: "data-copy-preserved", content: "can't touch this"}, {target: "data-copy", content: "can't touch this"}, {target: "nested-copy/nested", content: "can't touch this"}, {target: "nested-copy/more-nested/"}, {target: "not-listed", content: "can't touch this"}, {target: "unrelated/data/here", content: "unrelated"}, + {target: "preserved/same-content-for-list", content: "can't touch this"}, + {target: "preserved/same-content-for-observer", content: "can't touch this"}, }) // these exist in the root directory and are preserved preserve := []string{ @@ -2536,6 +2641,7 @@ "/data-copy-preserved", "nested-copy/nested", "not-listed", // not present in 'gadget' contents + "preserved/same-content-for-list", } // these are preserved, but don't exist in the root, so data from gadget // will be written @@ -2557,6 +2663,10 @@ Source: "foo", Target: "/", }, { + // nothing written, content is unchanged + Source: "foo", + Target: "/foo-same", + }, { // preserved, but not present, will be // written Source: "bar", @@ -2583,6 +2693,12 @@ }, { Source: "/boot-assets/empty-dir/", Target: "/lone-dir/nested/", + }, { + Source: "preserved/same-content", + Target: "preserved/same-content-for-list", + }, { + Source: "preserved/same-content", + Target: "preserved/same-content-for-observer", }, }, Update: gadget.VolumeUpdate{ @@ -2595,6 +2711,9 @@ muo := &mockContentUpdateObserver{ c: c, expectedStruct: ps, + preserveTargets: []string{ + "preserved/same-content-for-observer", + }, } rw, err := gadget.NewMountedFilesystemUpdater(s.dir, ps, s.backup, func(to *gadget.LaidOutStructure) (string, error) { c.Check(to, DeepEquals, ps) @@ -2604,14 +2723,17 @@ c.Assert(rw, NotNil) originalState := map[string]contentType{ - "foo": typeFile, - "dtb": typeFile, - "data-copy": typeFile, - "not-listed": typeFile, - "data-copy-preserved": typeFile, - "nested-copy/nested": typeFile, - "nested-copy/more-nested": typeDir, - "unrelated/data/here": typeFile, + "foo": typeFile, + "foo-same": typeFile, + "dtb": typeFile, + "data-copy": typeFile, + "not-listed": typeFile, + "data-copy-preserved": typeFile, + "nested-copy/nested": typeFile, + "nested-copy/more-nested": typeDir, + "unrelated/data/here": typeFile, + "preserved/same-content-for-list": typeFile, + "preserved/same-content-for-observer": typeFile, } verifyDirContents(c, outDir, originalState) @@ -2620,16 +2742,21 @@ c.Assert(err, IsNil) verifyDirContents(c, filepath.Join(s.backup, "struct-0"), map[string]contentType{ - "nested-copy.backup": typeFile, - "nested-copy/nested.preserve": typeFile, - "nested-copy/more-nested.backup": typeFile, - "foo.preserve": typeFile, - "data-copy-preserved.preserve": typeFile, - "data-copy.backup": typeFile, - "dtb.backup": typeFile, + "nested-copy.backup": typeFile, + "nested-copy/nested.preserve": typeFile, + "nested-copy/more-nested.backup": typeFile, + "foo.preserve": typeFile, + "foo-same.same": typeFile, + "data-copy-preserved.preserve": typeFile, + "data-copy.backup": typeFile, + "dtb.backup": typeFile, + "preserved.backup": typeFile, + "preserved/same-content-for-list.preserve": typeFile, + "preserved/same-content-for-observer.same": typeFile, }) expectedObservedContentChange := map[string][]*mockContentChange{ + // observer is notified about changed and new files outDir: { {"foo-dir/foo", &gadget.ContentChange{ After: filepath.Join(s.dir, "foo"), @@ -2698,6 +2825,7 @@ verifyDirContents(c, outDir, map[string]contentType{ "foo": typeFile, + "foo-same": typeFile, "not-listed": typeFile, // boot-assets/some-dir/data -> /data-copy @@ -2739,6 +2867,9 @@ // boot-assets/empty-dir/ -> /lone-dir/nested/ "lone-dir/nested": typeDir, + + "preserved/same-content-for-list": typeFile, + "preserved/same-content-for-observer": typeFile, }) // files that existed were preserved @@ -2747,7 +2878,7 @@ c.Check(p, testutil.FileEquals, "can't touch this") } // everything else was written - verifyWrittenGadgetData(c, outDir, gdWritten) + verifyWrittenGadgetData(c, outDir, append(gdWritten, gdSameContent...)) err = rw.Rollback() c.Assert(err, IsNil) @@ -2905,7 +3036,7 @@ c.Check(err, ErrorMatches, `cannot map preserve entries for mount location ".*/out-dir": preserved entry "foo" cannot be a directory`) } -func (s *mountedfilesystemTestSuite) testMountedUpdaterGrubBootAssets(c *C, managed bool, role string, preserved bool) { +func (s *mountedfilesystemTestSuite) TestMountedUpdaterObserverPreservesBootAssets(c *C) { // mirror pc-amd64-gadget gd := []gadgetData{ {name: "grub.conf", content: "grub.conf from gadget"}, @@ -2918,9 +3049,6 @@ existingGrubCfg := `# Snapd-Boot-Config-Edition: 1 managed grub.cfg from disk` - if !managed { - existingGrubCfg = `existing grub.cfg from disk` - } makeExistingData(c, outDir, []gadgetData{ {target: "EFI/boot/grubx64.efi", content: "grubx64.efi from disk"}, {target: "EFI/ubuntu/grub.cfg", content: existingGrubCfg}, @@ -2930,7 +3058,7 @@ ps := &gadget.LaidOutStructure{ VolumeStructure: &gadget.VolumeStructure{ Size: 2048, - Role: role, + Role: gadget.SystemBoot, Filesystem: "ext4", Content: []gadget.VolumeContent{ {Source: "grubx64.efi", Target: "EFI/boot/grubx64.efi"}, @@ -2943,15 +3071,32 @@ }, }, } + obs := &mockContentUpdateObserver{ + c: c, + expectedStruct: ps, + preserveTargets: []string{"EFI/ubuntu/grub.cfg"}, + } rw, err := gadget.NewMountedFilesystemUpdater(s.dir, ps, s.backup, func(to *gadget.LaidOutStructure) (string, error) { c.Check(to, DeepEquals, ps) return outDir, nil }, - nil) + obs) c.Assert(err, IsNil) c.Assert(rw, NotNil) + expectedFileStamps := map[string]contentType{ + "EFI.backup": typeFile, + "EFI/boot/grubx64.efi.backup": typeFile, + "EFI/boot.backup": typeFile, + "EFI/ubuntu.backup": typeFile, + + // listed explicitly in the structure + "foo.preserve": typeFile, + // requested by observer + "EFI/ubuntu/grub.cfg.ignore": typeFile, + } + for _, step := range []struct { name string call func() error @@ -2971,14 +3116,9 @@ c.Check(filepath.Join(outDir, "foo"), testutil.FileEquals, "foo from disk") case "update": c.Check(filepath.Join(outDir, "EFI/boot/grubx64.efi"), testutil.FileEquals, "grubx64.efi from gadget") - if preserved { - c.Check(filepath.Join(outDir, "EFI/ubuntu/grub.cfg"), testutil.FileEquals, - `# Snapd-Boot-Config-Edition: 1 + c.Check(filepath.Join(outDir, "EFI/ubuntu/grub.cfg"), testutil.FileEquals, + `# Snapd-Boot-Config-Edition: 1 managed grub.cfg from disk`) - } else { - c.Check(filepath.Join(outDir, "EFI/ubuntu/grub.cfg"), testutil.FileEquals, - `grub.conf from gadget`) - } c.Check(filepath.Join(outDir, "foo"), testutil.FileEquals, "foo from disk") case "rollback": c.Check(filepath.Join(outDir, "EFI/boot/grubx64.efi"), testutil.FileEquals, "grubx64.efi from disk") @@ -2987,55 +3127,143 @@ default: c.Fatalf("unexpected step: %q", step.name) } + verifyDirContents(c, filepath.Join(s.backup, "struct-0"), expectedFileStamps) } } -func (s *mountedfilesystemTestSuite) TestMountedUpdaterGrubBootAssetsManaged(c *C) { - managed := true - preserved := true - s.testMountedUpdaterGrubBootAssets(c, managed, gadget.SystemBoot, preserved) -} - -func (s *mountedfilesystemTestSuite) TestMountedUpdaterGrubSeedAssetsManaged(c *C) { - managed := true - preserved := true - s.testMountedUpdaterGrubBootAssets(c, managed, gadget.SystemSeed, preserved) -} - -func (s *mountedfilesystemTestSuite) TestMountedUpdaterGrubBootAssetsNotManaged(c *C) { - managed := false - preserved := false - s.testMountedUpdaterGrubBootAssets(c, managed, gadget.SystemSeed, preserved) -} - -func (s *mountedfilesystemTestSuite) TestMountedUpdaterGrubBootAssetsManagedOtherRole(c *C) { - managed := true - preserved := false - s.testMountedUpdaterGrubBootAssets(c, managed, gadget.SystemData, preserved) -} - -func (s *mountedfilesystemTestSuite) TestMountedUpdaterBootAssetsErr(c *C) { - outDir := filepath.Join(c.MkDir(), "out-dir") - - bootloader.ForceError(errors.New("foo")) - defer bootloader.ForceError(nil) +var ( // based on pc gadget - ps := &gadget.LaidOutStructure{ + psForObserver = &gadget.LaidOutStructure{ VolumeStructure: &gadget.VolumeStructure{ Size: 2048, Role: gadget.SystemBoot, Filesystem: "ext4", Content: []gadget.VolumeContent{ - {Source: "grubx64.efi", Target: "EFI/boot/grubx64.efi"}, - {Source: "grub.conf", Target: "EFI/ubuntu/grub.cfg"}, + {Source: "foo", Target: "foo"}, + }, + Update: gadget.VolumeUpdate{ + Edition: 1, }, }, } - rw, err := gadget.NewMountedFilesystemUpdater(s.dir, ps, s.backup, +) + +func (s *mountedfilesystemTestSuite) TestMountedUpdaterObserverPreserveNewFile(c *C) { + gd := []gadgetData{ + {name: "foo", content: "foo from gadget"}, + } + makeGadgetData(c, s.dir, gd) + + outDir := filepath.Join(c.MkDir(), "out-dir") + + obs := &mockContentUpdateObserver{ + c: c, + expectedStruct: psForObserver, + preserveTargets: []string{"foo"}, + } + rw, err := gadget.NewMountedFilesystemUpdater(s.dir, psForObserver, s.backup, func(to *gadget.LaidOutStructure) (string, error) { + c.Check(to, DeepEquals, psForObserver) return outDir, nil }, - nil) - c.Assert(err, ErrorMatches, "cannot preserve managed boot assets: foo") - c.Assert(rw, IsNil) + obs) + c.Assert(err, IsNil) + c.Assert(rw, NotNil) + + expectedNewFileChanges := map[string][]*mockContentChange{ + outDir: { + {"foo", &gadget.ContentChange{After: filepath.Join(s.dir, "foo")}}, + }, + } + expectedStamps := map[string]contentType{ + "foo.ignore": typeFile, + } + // file does not exist + err = rw.Backup() + c.Assert(err, IsNil) + // no stamps + verifyDirContents(c, filepath.Join(s.backup, "struct-0"), expectedStamps) + // observer got notified about change + c.Assert(obs.contentUpdate, DeepEquals, expectedNewFileChanges) + + obs.reset() + + // try the same pass again + err = rw.Backup() + c.Assert(err, IsNil) + verifyDirContents(c, filepath.Join(s.backup, "struct-0"), expectedStamps) + // observer already requested the change to be ignored once + c.Assert(obs.contentUpdate, HasLen, 0) + + // file does not exist and is not written + err = rw.Update() + c.Assert(err, Equals, gadget.ErrNoUpdate) + c.Assert(filepath.Join(outDir, "foo"), testutil.FileAbsent) + + // nothing happens on rollback + err = rw.Rollback() + c.Assert(err, IsNil) + c.Assert(filepath.Join(outDir, "foo"), testutil.FileAbsent) +} + +func (s *mountedfilesystemTestSuite) TestMountedUpdaterObserverPreserveExistingFile(c *C) { + gd := []gadgetData{ + {name: "foo", content: "foo from gadget"}, + } + makeGadgetData(c, s.dir, gd) + + outDir := filepath.Join(c.MkDir(), "out-dir") + + obs := &mockContentUpdateObserver{ + c: c, + expectedStruct: psForObserver, + preserveTargets: []string{"foo"}, + } + rw, err := gadget.NewMountedFilesystemUpdater(s.dir, psForObserver, s.backup, + func(to *gadget.LaidOutStructure) (string, error) { + c.Check(to, DeepEquals, psForObserver) + return outDir, nil + }, + obs) + c.Assert(err, IsNil) + c.Assert(rw, NotNil) + + // file exists now + makeExistingData(c, outDir, []gadgetData{ + {target: "foo", content: "foo from disk"}, + }) + expectedExistingFileChanges := map[string][]*mockContentChange{ + outDir: { + {"foo", &gadget.ContentChange{ + After: filepath.Join(s.dir, "foo"), + Before: filepath.Join(s.backup, "struct-0/foo.backup"), + }}, + }, + } + expectedExistingFileStamps := map[string]contentType{ + "foo.ignore": typeFile, + } + err = rw.Backup() + c.Assert(err, IsNil) + verifyDirContents(c, filepath.Join(s.backup, "struct-0"), expectedExistingFileStamps) + // get notified about change + c.Assert(obs.contentUpdate, DeepEquals, expectedExistingFileChanges) + + obs.reset() + // backup called again (eg. after reset) + err = rw.Backup() + c.Assert(err, IsNil) + verifyDirContents(c, filepath.Join(s.backup, "struct-0"), expectedExistingFileStamps) + // observer already requested the change to be ignored once + c.Assert(obs.contentUpdate, HasLen, 0) + + // and nothing gets updated + err = rw.Update() + c.Assert(err, Equals, gadget.ErrNoUpdate) + c.Check(filepath.Join(outDir, "foo"), testutil.FileEquals, "foo from disk") + + // the file existed and was preserved, nothing gets removed on rollback + err = rw.Rollback() + c.Assert(err, IsNil) + c.Check(filepath.Join(outDir, "foo"), testutil.FileEquals, "foo from disk") } diff -Nru snapd-2.47.1+20.10.1build1/gadget/ondisk.go snapd-2.48+21.04/gadget/ondisk.go --- snapd-2.47.1+20.10.1build1/gadget/ondisk.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/gadget/ondisk.go 2020-11-19 16:51:02.000000000 +0000 @@ -27,6 +27,7 @@ "strconv" "strings" + "github.com/snapcore/snapd/gadget/quantity" "github.com/snapcore/snapd/logger" "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/strutil" @@ -36,10 +37,9 @@ ubuntuBootLabel = "ubuntu-boot" ubuntuSeedLabel = "ubuntu-seed" ubuntuDataLabel = "ubuntu-data" + ubuntuSaveLabel = "ubuntu-save" - sectorSize Size = 512 - - createdPartitionAttr = "59" + sectorSize quantity.Size = 512 ) var createdPartitionGUID = []string{ @@ -83,38 +83,6 @@ Name string `json:"name"` } -func isCreatedDuringInstall(p *sfdiskPartition, fs *lsblkBlockDevice, sfdiskLabel string) bool { - switch sfdiskLabel { - case "gpt": - // the created partitions use specific GPT GUID types and set a - // specific bit in partition attributes - if !creationSupported(p.Type) { - return false - } - for _, a := range strings.Fields(p.Attrs) { - if !strings.HasPrefix(a, "GUID:") { - continue - } - attrs := strings.Split(a[5:], ",") - if strutil.ListContains(attrs, createdPartitionAttr) { - return true - } - } - case "dos": - // we have no similar type/bit attribute setting for MBR, on top - // of that MBR does not support partition names, fall back to - // reasonable assumption that only partitions carrying - // ubuntu-boot and ubuntu-data labels are created during - // install, everything else was part of factory image - - // TODO:UC20 consider using gadget layout information to build a - // mapping of partition start offset to label/name - createdDuringInstall := []string{ubuntuBootLabel, ubuntuDataLabel} - return strutil.ListContains(createdDuringInstall, fs.Label) - } - return false -} - // TODO: consider looking into merging LaidOutVolume/Structure OnDiskVolume/Structure // OnDiskStructure represents a gadget structure laid on a block device. @@ -123,9 +91,11 @@ // Node identifies the device node of the block device. Node string - // CreatedDuringInstall is true when the structure has properties indicating - // it was created based on the gadget description during installation. - CreatedDuringInstall bool + + // Size of the on disk structure, which is at least equal to the + // LaidOutStructure.Size but may be bigger if the partition was + // expanded. + Size quantity.Size } // OnDiskVolume holds information about the disk device including its partitioning @@ -136,16 +106,16 @@ Device string Schema string // size in bytes - Size Size + Size quantity.Size // sector size in bytes - SectorSize Size + SectorSize quantity.Size partitionTable *sfdiskPartitionTable } // OnDiskVolumeFromDevice obtains the partitioning and filesystem information from // the block device. func OnDiskVolumeFromDevice(device string) (*OnDiskVolume, error) { - output, err := exec.Command("sfdisk", "--json", "-d", device).Output() + output, err := exec.Command("sfdisk", "--json", device).Output() if err != nil { return nil, osutil.OutputErr(output, err) } @@ -181,7 +151,7 @@ } } -func blockDeviceSizeInSectors(devpath string) (Size, error) { +func blockDeviceSizeInSectors(devpath string) (quantity.Size, error) { // the size is reported in 512-byte sectors // XXX: consider using /sys/block//size directly out, err := exec.Command("blockdev", "--getsz", devpath).CombinedOutput() @@ -193,7 +163,7 @@ if err != nil { return 0, fmt.Errorf("cannot parse device size %q: %v", nospace, err) } - return Size(sz), nil + return quantity.Size(sz), nil } // onDiskVolumeFromPartitionTable takes an sfdisk dump partition table and returns @@ -226,7 +196,7 @@ structure[i] = VolumeStructure{ Name: p.Name, - Size: Size(p.Size) * sectorSize, + Size: quantity.Size(p.Size) * sectorSize, Label: bd.Label, Type: vsType, Filesystem: bd.FSType, @@ -235,18 +205,17 @@ ds[i] = OnDiskStructure{ LaidOutStructure: LaidOutStructure{ VolumeStructure: &structure[i], - StartOffset: Size(p.Start) * sectorSize, + StartOffset: quantity.Size(p.Start) * sectorSize, Index: i + 1, }, - Node: p.Node, - CreatedDuringInstall: isCreatedDuringInstall(&p, &bd, ptable.Label), + Node: p.Node, } } - var numSectors Size + var numSectors quantity.Size if ptable.LastLBA != 0 { // sfdisk reports the last usable LBA for GPT disks only - numSectors = Size(ptable.LastLBA + 1) + numSectors = quantity.Size(ptable.LastLBA + 1) } else { // sfdisk does not report any information about the size of a // MBR partitioned disk, find out the size of the device by @@ -340,8 +309,8 @@ // build from there could be safer if the disk partitions are not consecutive // (can this actually happen in our images?) node := deviceName(ptable.Device, pIndex) - fmt.Fprintf(buf, "%s : start=%12d, size=%12d, type=%s, name=%q, attrs=\"GUID:%s\"\n", node, - p.StartOffset/sectorSize, size/sectorSize, ptype, s.Name, createdPartitionAttr) + fmt.Fprintf(buf, "%s : start=%12d, size=%12d, type=%s, name=%q\n", node, + p.StartOffset/sectorSize, size/sectorSize, ptype, s.Name) // Set expected labels based on role switch s.Role { @@ -351,12 +320,14 @@ s.Label = ubuntuSeedLabel case SystemData: s.Label = ubuntuDataLabel + case SystemSave: + s.Label = ubuntuSaveLabel } toBeCreated = append(toBeCreated, OnDiskStructure{ - LaidOutStructure: p, - Node: node, - CreatedDuringInstall: true, + LaidOutStructure: p, + Node: node, + Size: size, }) } @@ -380,18 +351,6 @@ return nil } -// CreatedDuringInstall returns a list of partitions created during the -// install process. -func CreatedDuringInstall(layout *OnDiskVolume) (created []string) { - created = make([]string, 0, len(layout.Structure)) - for _, s := range layout.Structure { - if s.CreatedDuringInstall { - created = append(created, s.Node) - } - } - return created -} - func partitionType(label, ptype string) string { t := strings.Split(ptype, ",") if len(t) < 1 { diff -Nru snapd-2.47.1+20.10.1build1/gadget/ondisk_test.go snapd-2.48+21.04/gadget/ondisk_test.go --- snapd-2.47.1+20.10.1build1/gadget/ondisk_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/gadget/ondisk_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -27,6 +27,7 @@ . "gopkg.in/check.v1" "github.com/snapcore/snapd/gadget" + "github.com/snapcore/snapd/gadget/quantity" "github.com/snapcore/snapd/testutil" ) @@ -75,8 +76,7 @@ "size": 2457600, "type": "C12A7328-F81F-11D2-BA4B-00A0C93EC93B", "uuid": "44C3D5C3-CAE1-4306-83E8-DF437ACDB32F", - "name": "Recovery", - "attrs": "GUID:59" + "name": "Recovery" } ] } @@ -136,6 +136,11 @@ content: - source: grubx64.efi target: EFI/boot/grubx64.efi + - name: Save + role: system-save + filesystem: ext4 + type: 83,0FC63DAF-8483-4772-8E79-3D69D8477DE4 + size: 128M - name: Writable role: system-data filesystem: ext4 @@ -143,21 +148,39 @@ size: 1200M ` +var mockOnDiskStructureSave = gadget.OnDiskStructure{ + Node: "/dev/node3", + LaidOutStructure: gadget.LaidOutStructure{ + VolumeStructure: &gadget.VolumeStructure{ + Name: "Save", + Size: 128 * quantity.SizeMiB, + Type: "83,0FC63DAF-8483-4772-8E79-3D69D8477DE4", + Role: "system-save", + Label: "ubuntu-save", + Filesystem: "ext4", + }, + StartOffset: 1260388352, + Index: 3, + }, + Size: 128 * quantity.SizeMiB, +} + var mockOnDiskStructureWritable = gadget.OnDiskStructure{ - Node: "/dev/node3", - CreatedDuringInstall: true, + Node: "/dev/node4", LaidOutStructure: gadget.LaidOutStructure{ VolumeStructure: &gadget.VolumeStructure{ Name: "Writable", - Size: 1258291200, + Size: 1200 * quantity.SizeMiB, Type: "83,0FC63DAF-8483-4772-8E79-3D69D8477DE4", Role: "system-data", Label: "ubuntu-data", Filesystem: "ext4", }, - StartOffset: 1260388352, - Index: 3, + StartOffset: 1394606080, + Index: 4, }, + // expanded to fill the disk + Size: 2*quantity.SizeGiB + 717*quantity.SizeMiB + 1031680, } func (s *ondiskTestSuite) TestDeviceInfoGPT(c *C) { @@ -170,7 +193,7 @@ dl, err := gadget.OnDiskVolumeFromDevice("/dev/node") c.Assert(err, IsNil) c.Assert(cmdSfdisk.Calls(), DeepEquals, [][]string{ - {"sfdisk", "--json", "-d", "/dev/node"}, + {"sfdisk", "--json", "/dev/node"}, }) c.Assert(cmdLsblk.Calls(), DeepEquals, [][]string{ {"lsblk", "--fs", "--json", "/dev/node1"}, @@ -180,8 +203,8 @@ c.Assert(dl.Schema, Equals, "gpt") c.Assert(dl.ID, Equals, "9151F25B-CDF0-48F1-9EDE-68CBD616E2CA") c.Assert(dl.Device, Equals, "/dev/node") - c.Assert(dl.SectorSize, Equals, gadget.Size(512)) - c.Assert(dl.Size, Equals, gadget.Size(8388575*512)) + c.Assert(dl.SectorSize, Equals, quantity.Size(512)) + c.Assert(dl.Size, Equals, quantity.Size(8388575*512)) c.Assert(len(dl.Structure), Equals, 2) c.Assert(dl.Structure, DeepEquals, []gadget.OnDiskStructure{ @@ -227,7 +250,8 @@ "partitions": [ {"node": "/dev/node1", "start": 4096, "size": 2457600, "type": "c"}, {"node": "/dev/node2", "start": 2461696, "size": 1048576, "type": "d"}, - {"node": "/dev/node3", "start": 3510272, "size": 1048576, "type": "d"} + {"node": "/dev/node3", "start": 3510272, "size": 1048576, "type": "d"}, + {"node": "/dev/node4", "start": 4558848, "size": 1048576, "type": "d"} ] } }'` @@ -239,6 +263,9 @@ "blockdevices": [ {"name": "node2", "fstype": "vfat", "label": "ubuntu-boot", "uuid": "A644-B808", "mountpoint": null} ] }' [ "$3" == "/dev/node3" ] && echo '{ + "blockdevices": [ {"name": "node3", "fstype": "ext4", "label": "ubuntu-save", "mountpoint": null} ] +}' +[ "$3" == "/dev/node4" ] && echo '{ "blockdevices": [ {"name": "node3", "fstype": "ext4", "label": "ubuntu-data", "mountpoint": null} ] }' exit 0` @@ -255,12 +282,13 @@ dl, err := gadget.OnDiskVolumeFromDevice("/dev/node") c.Assert(err, IsNil) c.Assert(cmdSfdisk.Calls(), DeepEquals, [][]string{ - {"sfdisk", "--json", "-d", "/dev/node"}, + {"sfdisk", "--json", "/dev/node"}, }) c.Assert(cmdLsblk.Calls(), DeepEquals, [][]string{ {"lsblk", "--fs", "--json", "/dev/node1"}, {"lsblk", "--fs", "--json", "/dev/node2"}, {"lsblk", "--fs", "--json", "/dev/node3"}, + {"lsblk", "--fs", "--json", "/dev/node4"}, }) c.Assert(cmdBlockdev.Calls(), DeepEquals, [][]string{ {"blockdev", "--getsz", "/dev/node"}, @@ -269,9 +297,9 @@ c.Assert(dl.ID, Equals, "") c.Assert(dl.Schema, Equals, "dos") c.Assert(dl.Device, Equals, "/dev/node") - c.Assert(dl.SectorSize, Equals, gadget.Size(512)) - c.Assert(dl.Size, Equals, gadget.Size(12345670*512)) - c.Assert(len(dl.Structure), Equals, 3) + c.Assert(dl.SectorSize, Equals, quantity.Size(512)) + c.Assert(dl.Size, Equals, quantity.Size(12345670*512)) + c.Assert(len(dl.Structure), Equals, 4) c.Assert(dl.Structure, DeepEquals, []gadget.OnDiskStructure{ { @@ -285,8 +313,7 @@ StartOffset: 0x200000, Index: 1, }, - Node: "/dev/node1", - CreatedDuringInstall: false, + Node: "/dev/node1", }, { LaidOutStructure: gadget.LaidOutStructure{ @@ -299,22 +326,33 @@ StartOffset: 0x4b200000, Index: 2, }, - Node: "/dev/node2", - CreatedDuringInstall: true, + Node: "/dev/node2", }, { LaidOutStructure: gadget.LaidOutStructure{ VolumeStructure: &gadget.VolumeStructure{ Size: 0x20000000, - Label: "ubuntu-data", + Label: "ubuntu-save", Type: "0D", Filesystem: "ext4", }, StartOffset: 0x6b200000, Index: 3, }, - Node: "/dev/node3", - CreatedDuringInstall: true, + Node: "/dev/node3", + }, + { + LaidOutStructure: gadget.LaidOutStructure{ + VolumeStructure: &gadget.VolumeStructure{ + Size: 0x20000000, + Label: "ubuntu-data", + Type: "0D", + Filesystem: "ext4", + }, + StartOffset: 0x8b200000, + Index: 4, + }, + Node: "/dev/node4", }, }) } @@ -423,9 +461,11 @@ // the expected expanded writable partition size is: // start offset = (2M + 1200M), expanded size in sectors = (8388575*512 - start offset)/512 sfdiskInput, create := gadget.BuildPartitionList(dl, pv) - c.Assert(sfdiskInput.String(), Equals, `/dev/node3 : start= 2461696, size= 5926879, type=0FC63DAF-8483-4772-8E79-3D69D8477DE4, name="Writable", attrs="GUID:59" + c.Assert(sfdiskInput.String(), Equals, + `/dev/node3 : start= 2461696, size= 262144, type=0FC63DAF-8483-4772-8E79-3D69D8477DE4, name="Save" +/dev/node4 : start= 2723840, size= 5664735, type=0FC63DAF-8483-4772-8E79-3D69D8477DE4, name="Writable" `) - c.Assert(create, DeepEquals, []gadget.OnDiskStructure{mockOnDiskStructureWritable}) + c.Assert(create, DeepEquals, []gadget.OnDiskStructure{mockOnDiskStructureSave, mockOnDiskStructureWritable}) } func (s *ondiskTestSuite) TestUpdatePartitionList(c *C) { @@ -487,142 +527,6 @@ c.Assert(dl.Structure[1].Node, Equals, "/dev/node2") } -func (s *ondiskTestSuite) TestCreatedDuringInstallGPT(c *C) { - cmdLsblk := testutil.MockCommand(c, "lsblk", `echo '{ "blockdevices": [ {"fstype":"ext4", "label":null} ] }'`) - defer cmdLsblk.Restore() - - ptable := gadget.SFDiskPartitionTable{ - Label: "gpt", - ID: "9151F25B-CDF0-48F1-9EDE-68CBD616E2CA", - Device: "/dev/node", - Unit: "sectors", - FirstLBA: 34, - LastLBA: 8388574, - Partitions: []gadget.SFDiskPartition{ - { - Node: "/dev/node1", - Start: 1024, - Size: 1024, - Type: "0fc63daf-8483-4772-8e79-3d69d8477de4", - UUID: "641764aa-a680-4d36-a7ad-f7bd01fd8d12", - Name: "Linux filesystem", - }, - { - Node: "/dev/node2", - Start: 2048, - Size: 2048, - Type: "0657FD6D-A4AB-43C4-84E5-0933C84B4F4F", - UUID: "7ea3a75a-3f6d-4647-8134-89ae61fe88d5", - Name: "Linux swap", - }, - { - Node: "/dev/node3", - Start: 8192, - Size: 8192, - Type: "21686148-6449-6E6F-744E-656564454649", - UUID: "30a26851-4b08-4b8d-8aea-f686e723ed8c", - Name: "BIOS boot partition", - }, - { - Node: "/dev/node4", - Start: 16384, - Size: 16384, - Type: "0fc63daf-8483-4772-8e79-3d69d8477de4", - UUID: "8ab3e8fd-d53d-4d72-9c5e-56146915fd07", - Name: "Another Linux filesystem", - }, - }, - } - dl, err := gadget.OnDiskVolumeFromPartitionTable(ptable) - c.Assert(err, IsNil) - list := gadget.CreatedDuringInstall(dl) - c.Assert(list, HasLen, 0) - - // Set attribute bit for all partitions except the last one - for i := 0; i < len(ptable.Partitions)-1; i++ { - ptable.Partitions[i].Attrs = "RequiredPartition LegacyBIOSBootable GUID:58,59" - } - - dl, err = gadget.OnDiskVolumeFromPartitionTable(ptable) - c.Assert(err, IsNil) - list = gadget.CreatedDuringInstall(dl) - c.Assert(list, DeepEquals, []string{"/dev/node1", "/dev/node2"}) -} - -func (s *ondiskTestSuite) TestCreatedDuringInstallMBR(c *C) { - cmdLsblk := testutil.MockCommand(c, "lsblk", ` -what= -shift 2 -case "$1" in - /dev/node1) - what='{"name": "node1", "fstype":"ext4", "label":"ubuntu-seed"}' - ;; - /dev/node2) - what='{"name": "node2", "fstype":"vfat", "label":"ubuntu-boot"}' - ;; - /dev/node3) - what='{"name": "node3", "fstype":null, "label":null}' - ;; - /dev/node4) - what='{"name": "node4", "fstype":"ext4", "label":"ubuntu-data"}' - ;; - *) - echo "unexpected call" - exit 1 -esac - -cat <. + * + */ + +package quantity + +import ( + "errors" + "fmt" + "math" + + "github.com/snapcore/snapd/strutil" +) + +// Size describes the size in bytes. +type Size uint64 + +const ( + // SizeKiB is the byte size of one kibibyte (2^10 = 1024 bytes) + SizeKiB = Size(1 << 10) + // SizeMiB is the size of one mebibyte (2^20) + SizeMiB = Size(1 << 20) + // SizeGiB is the size of one gibibyte (2^30) + SizeGiB = Size(1 << 30) +) + +func (s *Size) String() string { + if s == nil { + return "unspecified" + } + return fmt.Sprintf("%d", *s) +} + +// IECString formats the size using multiples from IEC units (i.e. kibibytes, +// mebibytes), that is as multiples of 1024. Printed values are truncated to 2 +// decimal points. +func (s *Size) IECString() string { + maxFloat := float64(1023.5) + r := float64(*s) + unit := "B" + for _, rangeUnit := range []string{"KiB", "MiB", "GiB", "TiB", "PiB"} { + if r < maxFloat { + break + } + r /= 1024 + unit = rangeUnit + } + precision := 0 + if math.Floor(r) != r { + precision = 2 + } + return fmt.Sprintf("%.*f %s", precision, r, unit) +} + +func (s *Size) UnmarshalYAML(unmarshal func(interface{}) error) error { + var gs string + if err := unmarshal(&gs); err != nil { + return errors.New(`cannot unmarshal gadget size`) + } + + var err error + *s, err = ParseSize(gs) + if err != nil { + return fmt.Errorf("cannot parse size %q: %v", gs, err) + } + return err +} + +// ParseSize parses a string expressing size in a gadget specific format. The +// accepted format is one of: | M | G. +func ParseSize(gs string) (Size, error) { + number, unit, err := strutil.SplitUnit(gs) + if err != nil { + return 0, err + } + if number < 0 { + return 0, errors.New("size cannot be negative") + } + var size Size + switch unit { + case "M": + // MiB + size = Size(number) * SizeMiB + case "G": + // GiB + size = Size(number) * SizeGiB + case "": + // straight bytes + size = Size(number) + default: + return 0, fmt.Errorf("invalid suffix %q", unit) + } + return size, nil +} diff -Nru snapd-2.47.1+20.10.1build1/gadget/quantity/size_test.go snapd-2.48+21.04/gadget/quantity/size_test.go --- snapd-2.47.1+20.10.1build1/gadget/quantity/size_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48+21.04/gadget/quantity/size_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -0,0 +1,87 @@ +// -*- 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 quantity_test + +import ( + "fmt" + "testing" + + . "gopkg.in/check.v1" + "gopkg.in/yaml.v2" + + "github.com/snapcore/snapd/gadget/quantity" +) + +func TestRun(t *testing.T) { TestingT(t) } + +type sizeTestSuite struct{} + +var _ = Suite(&sizeTestSuite{}) + +func (s *sizeTestSuite) TestIECString(c *C) { + for _, tc := range []struct { + size quantity.Size + exp string + }{ + {512, "512 B"}, + {1000, "1000 B"}, + {1030, "1.01 KiB"}, + {quantity.SizeKiB + 512, "1.50 KiB"}, + {123 * quantity.SizeKiB, "123 KiB"}, + {512 * quantity.SizeKiB, "512 KiB"}, + {578 * quantity.SizeMiB, "578 MiB"}, + {1*quantity.SizeGiB + 123*quantity.SizeMiB, "1.12 GiB"}, + {1024 * quantity.SizeGiB, "1 TiB"}, + {2 * 1024 * 1024 * 1024 * quantity.SizeGiB, "2048 PiB"}, + } { + c.Check(tc.size.IECString(), Equals, tc.exp) + } +} + +func (s *sizeTestSuite) TestUnmarshalYAMLSize(c *C) { + type foo struct { + Size quantity.Size `yaml:"size"` + } + + for i, tc := range []struct { + s string + sz quantity.Size + err string + }{ + {"1234", 1234, ""}, + {"1234M", 1234 * quantity.SizeMiB, ""}, + {"1234G", 1234 * quantity.SizeGiB, ""}, + {"0", 0, ""}, + {"a0M", 0, `cannot parse size "a0M": no numerical prefix.*`}, + {"-123", 0, `cannot parse size "-123": size cannot be negative`}, + {"123a", 0, `cannot parse size "123a": invalid suffix "a"`}, + } { + c.Logf("tc: %v", i) + + var f foo + err := yaml.Unmarshal([]byte(fmt.Sprintf("size: %s", tc.s)), &f) + if tc.err != "" { + c.Check(err, ErrorMatches, tc.err) + } else { + c.Check(err, IsNil) + c.Check(f.Size, Equals, tc.sz) + } + } +} diff -Nru snapd-2.47.1+20.10.1build1/gadget/raw.go snapd-2.48+21.04/gadget/raw.go --- snapd-2.47.1+20.10.1build1/gadget/raw.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/gadget/raw.go 2020-11-19 16:51:02.000000000 +0000 @@ -28,6 +28,7 @@ "os" "path/filepath" + "github.com/snapcore/snapd/gadget/quantity" "github.com/snapcore/snapd/osutil" ) @@ -104,7 +105,7 @@ deviceLookup deviceLookupFunc } -type deviceLookupFunc func(ps *LaidOutStructure) (device string, offs Size, err error) +type deviceLookupFunc func(ps *LaidOutStructure) (device string, offs quantity.Size, err error) // newRawStructureUpdater returns an updater for the given raw (bare) structure. // Update data will be loaded from the provided gadget content directory. diff -Nru snapd-2.47.1+20.10.1build1/gadget/raw_test.go snapd-2.48+21.04/gadget/raw_test.go --- snapd-2.47.1+20.10.1build1/gadget/raw_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/gadget/raw_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -28,6 +28,7 @@ . "gopkg.in/check.v1" "github.com/snapcore/snapd/gadget" + "github.com/snapcore/snapd/gadget/quantity" "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/testutil" ) @@ -44,7 +45,7 @@ r.backup = c.MkDir() } -func openSizedFile(c *C, path string, size gadget.Size) *os.File { +func openSizedFile(c *C, path string, size quantity.Size) *os.File { f, err := os.Create(path) c.Assert(err, IsNil) @@ -61,7 +62,7 @@ off int64 } -func mutateFile(c *C, path string, size gadget.Size, writes []mutateWrite) { +func mutateFile(c *C, path string, size quantity.Size, writes []mutateWrite) { out := openSizedFile(c, path, size) for _, op := range writes { _, err := out.WriteAt(op.what, op.off) @@ -278,7 +279,7 @@ }, } - ru, err := gadget.NewRawStructureUpdater(r.dir, ps, r.backup, func(to *gadget.LaidOutStructure) (string, gadget.Size, error) { + ru, err := gadget.NewRawStructureUpdater(r.dir, ps, r.backup, func(to *gadget.LaidOutStructure) (string, quantity.Size, error) { c.Fatalf("unexpected call") return "", 0, nil }) @@ -300,25 +301,25 @@ VolumeStructure: &gadget.VolumeStructure{ Size: 2048, }, - StartOffset: 1 * gadget.SizeMiB, + StartOffset: 1 * quantity.SizeMiB, LaidOutContent: []gadget.LaidOutContent{ { VolumeContent: &gadget.VolumeContent{ Image: "foo.img", }, - StartOffset: 1 * gadget.SizeMiB, + StartOffset: 1 * quantity.SizeMiB, Size: 128, }, { VolumeContent: &gadget.VolumeContent{ Image: "bar.img", }, - StartOffset: 1*gadget.SizeMiB + 1024, + StartOffset: 1*quantity.SizeMiB + 1024, Size: 128, Index: 1, }, }, } - ru, err := gadget.NewRawStructureUpdater(r.dir, ps, r.backup, func(to *gadget.LaidOutStructure) (string, gadget.Size, error) { + ru, err := gadget.NewRawStructureUpdater(r.dir, ps, r.backup, func(to *gadget.LaidOutStructure) (string, quantity.Size, error) { c.Check(to, DeepEquals, ps) // Structure has a partition, thus it starts at 0 offset. return partitionPath, 0, nil @@ -337,7 +338,7 @@ c.Assert(err, IsNil) // update should be a noop now, use the same locations, point to a file // of 0 size, so that seek fails and write would increase the size - ru, err = gadget.NewRawStructureUpdater(r.dir, ps, r.backup, func(to *gadget.LaidOutStructure) (string, gadget.Size, error) { + ru, err = gadget.NewRawStructureUpdater(r.dir, ps, r.backup, func(to *gadget.LaidOutStructure) (string, quantity.Size, error) { return emptyDiskPath, 0, nil }) c.Assert(err, IsNil) @@ -380,32 +381,32 @@ VolumeStructure: &gadget.VolumeStructure{ Size: 4096, }, - StartOffset: 1 * gadget.SizeMiB, + StartOffset: 1 * quantity.SizeMiB, LaidOutContent: []gadget.LaidOutContent{ { VolumeContent: &gadget.VolumeContent{ Image: "foo.img", }, - StartOffset: 1 * gadget.SizeMiB, + StartOffset: 1 * quantity.SizeMiB, Size: 128, }, { VolumeContent: &gadget.VolumeContent{ Image: "bar.img", }, - StartOffset: 1*gadget.SizeMiB + 1024, + StartOffset: 1*quantity.SizeMiB + 1024, Size: 256, Index: 1, }, { VolumeContent: &gadget.VolumeContent{ Image: "unchanged.img", }, - StartOffset: 1*gadget.SizeMiB + 2048, + StartOffset: 1*quantity.SizeMiB + 2048, Size: 128, Index: 2, }, }, } - ru, err := gadget.NewRawStructureUpdater(r.dir, ps, r.backup, func(to *gadget.LaidOutStructure) (string, gadget.Size, error) { + ru, err := gadget.NewRawStructureUpdater(r.dir, ps, r.backup, func(to *gadget.LaidOutStructure) (string, quantity.Size, error) { c.Check(to, DeepEquals, ps) // Structure has a partition, thus it starts at 0 offset. return diskPath, 0, nil @@ -453,9 +454,9 @@ func (r *rawTestSuite) TestRawUpdaterBackupUpdateRestoreNoPartition(c *C) { diskPath := filepath.Join(r.dir, "disk.img") - mutateFile(c, diskPath, gadget.SizeMiB+2048, []mutateWrite{ - {[]byte("baz baz baz"), int64(gadget.SizeMiB)}, - {[]byte("oof oof oof"), int64(gadget.SizeMiB + 1024)}, + mutateFile(c, diskPath, quantity.SizeMiB+2048, []mutateWrite{ + {[]byte("baz baz baz"), int64(quantity.SizeMiB)}, + {[]byte("oof oof oof"), int64(quantity.SizeMiB + 1024)}, }) pristinePath := filepath.Join(r.dir, "pristine.img") @@ -463,9 +464,9 @@ c.Assert(err, IsNil) expectedPath := filepath.Join(r.dir, "expected.img") - mutateFile(c, expectedPath, gadget.SizeMiB+2048, []mutateWrite{ - {[]byte("zzz zzz zzz zzz"), int64(gadget.SizeMiB)}, - {[]byte("xxx xxx xxx xxx"), int64(gadget.SizeMiB + 1024)}, + mutateFile(c, expectedPath, quantity.SizeMiB+2048, []mutateWrite{ + {[]byte("zzz zzz zzz zzz"), int64(quantity.SizeMiB)}, + {[]byte("xxx xxx xxx xxx"), int64(quantity.SizeMiB + 1024)}, }) makeSizedFile(c, filepath.Join(r.dir, "foo.img"), 128, []byte("zzz zzz zzz zzz")) @@ -476,25 +477,25 @@ Type: "bare", Size: 2048, }, - StartOffset: 1 * gadget.SizeMiB, + StartOffset: 1 * quantity.SizeMiB, LaidOutContent: []gadget.LaidOutContent{ { VolumeContent: &gadget.VolumeContent{ Image: "foo.img", }, - StartOffset: 1 * gadget.SizeMiB, + StartOffset: 1 * quantity.SizeMiB, Size: 128, }, { VolumeContent: &gadget.VolumeContent{ Image: "bar.img", }, - StartOffset: 1*gadget.SizeMiB + 1024, + StartOffset: 1*quantity.SizeMiB + 1024, Size: 256, Index: 1, }, }, } - ru, err := gadget.NewRawStructureUpdater(r.dir, ps, r.backup, func(to *gadget.LaidOutStructure) (string, gadget.Size, error) { + ru, err := gadget.NewRawStructureUpdater(r.dir, ps, r.backup, func(to *gadget.LaidOutStructure) (string, quantity.Size, error) { c.Check(to, DeepEquals, ps) // No partition table, returned path corresponds to a disk, start offset is non-0. return diskPath, ps.StartOffset, nil @@ -548,7 +549,7 @@ }, } - ru, err := gadget.NewRawStructureUpdater(r.dir, ps, r.backup, func(to *gadget.LaidOutStructure) (string, gadget.Size, error) { + ru, err := gadget.NewRawStructureUpdater(r.dir, ps, r.backup, func(to *gadget.LaidOutStructure) (string, quantity.Size, error) { c.Check(to, DeepEquals, ps) return diskPath, 0, nil }) @@ -597,7 +598,7 @@ }, } - ru, err := gadget.NewRawStructureUpdater(r.dir, ps, r.backup, func(to *gadget.LaidOutStructure) (string, gadget.Size, error) { + ru, err := gadget.NewRawStructureUpdater(r.dir, ps, r.backup, func(to *gadget.LaidOutStructure) (string, quantity.Size, error) { c.Check(to, DeepEquals, ps) return diskPath, 0, nil }) @@ -643,7 +644,7 @@ c.Assert(err, ErrorMatches, "internal error: device lookup helper must be provided") c.Assert(ru, IsNil) - ru, err = gadget.NewRawStructureUpdater(r.dir, ps, r.backup, func(to *gadget.LaidOutStructure) (string, gadget.Size, error) { + ru, err = gadget.NewRawStructureUpdater(r.dir, ps, r.backup, func(to *gadget.LaidOutStructure) (string, quantity.Size, error) { c.Check(to, DeepEquals, ps) return "", 0, errors.New("failed") }) @@ -661,6 +662,10 @@ } func (r *rawTestSuite) TestRawUpdaterRollbackErrors(c *C) { + if os.Geteuid() == 0 { + c.Skip("the test cannot be run by the root user") + } + diskPath := filepath.Join(r.dir, "disk.img") // 0 sized disk, copying will fail with early EOF makeSizedFile(c, diskPath, 0, nil) @@ -681,7 +686,7 @@ }, } - ru, err := gadget.NewRawStructureUpdater(r.dir, ps, r.backup, func(to *gadget.LaidOutStructure) (string, gadget.Size, error) { + ru, err := gadget.NewRawStructureUpdater(r.dir, ps, r.backup, func(to *gadget.LaidOutStructure) (string, quantity.Size, error) { c.Check(to, DeepEquals, ps) return diskPath, 0, nil }) @@ -707,6 +712,10 @@ } func (r *rawTestSuite) TestRawUpdaterUpdateErrors(c *C) { + if os.Geteuid() == 0 { + c.Skip("the test cannot be run by the root user") + } + diskPath := filepath.Join(r.dir, "disk.img") // 0 sized disk, copying will fail with early EOF makeSizedFile(c, diskPath, 2048, nil) @@ -727,7 +736,7 @@ }, } - ru, err := gadget.NewRawStructureUpdater(r.dir, ps, r.backup, func(to *gadget.LaidOutStructure) (string, gadget.Size, error) { + ru, err := gadget.NewRawStructureUpdater(r.dir, ps, r.backup, func(to *gadget.LaidOutStructure) (string, quantity.Size, error) { c.Check(to, DeepEquals, ps) return diskPath, 0, nil }) @@ -784,7 +793,7 @@ }, } - f := func(to *gadget.LaidOutStructure) (string, gadget.Size, error) { + f := func(to *gadget.LaidOutStructure) (string, quantity.Size, error) { return "", 0, errors.New("unexpected call") } rw, err := gadget.NewRawStructureUpdater("", ps, r.backup, f) diff -Nru snapd-2.47.1+20.10.1build1/gadget/update.go snapd-2.48+21.04/gadget/update.go --- snapd-2.47.1+20.10.1build1/gadget/update.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/gadget/update.go 2020-11-19 16:51:02.000000000 +0000 @@ -23,6 +23,7 @@ "errors" "fmt" + "github.com/snapcore/snapd/gadget/quantity" "github.com/snapcore/snapd/logger" ) @@ -33,7 +34,7 @@ var ( // default positioning constraints that match ubuntu-image defaultConstraints = LayoutConstraints{ - NonMBRStartOffset: 1 * SizeMiB, + NonMBRStartOffset: 1 * quantity.SizeMiB, SectorSize: 512, } ) @@ -61,19 +62,21 @@ } type ContentOperation int +type ContentChangeAction int const ( ContentWrite ContentOperation = iota ContentUpdate ContentRollback + + ChangeAbort ContentChangeAction = iota + ChangeApply + ChangeIgnore ) // ContentObserver allows for observing operations on the content of the gadget // structures. type ContentObserver interface { - // TODO:UC20: add Observe() result value indicating that a file should - // be preserved - // Observe is called to observe an pending or completed action, related // to content being written, updated or being rolled back. In each of // the scenarios, the target path is relative under the root. @@ -82,8 +85,14 @@ // that will be written. When called during rollback, observe call // happens after the original file has been restored (or removed if the // file was added during the update), the source path is empty. + // + // Returning ChangeApply indicates that the observer agrees for a given + // change to be applied. When called with a ContentUpdate or + // ContentWrite operation, returning ChangeIgnore indicates that the + // change shall be ignored. ChangeAbort is expected to be returned along + // with a non-nil error. Observe(op ContentOperation, sourceStruct *LaidOutStructure, - targetRootDir, relativeTargetPath string, dataChange *ContentChange) (bool, error) + targetRootDir, relativeTargetPath string, dataChange *ContentChange) (ContentChangeAction, error) } // ContentUpdateObserver allows for observing update (and potentially a @@ -188,7 +197,7 @@ return &oldV, &newV, nil } -func isSameOffset(one *Size, two *Size) bool { +func isSameOffset(one *quantity.Size, two *quantity.Size) bool { if one == nil && two == nil { return true } diff -Nru snapd-2.47.1+20.10.1build1/gadget/update_test.go snapd-2.48+21.04/gadget/update_test.go --- snapd-2.47.1+20.10.1build1/gadget/update_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/gadget/update_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -31,6 +31,7 @@ "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/gadget" + "github.com/snapcore/snapd/gadget/quantity" "github.com/snapcore/snapd/logger" "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/testutil" @@ -121,19 +122,19 @@ { // size change from: gadget.LaidOutStructure{ - VolumeStructure: &gadget.VolumeStructure{Size: 1 * gadget.SizeMiB}, + VolumeStructure: &gadget.VolumeStructure{Size: 1 * quantity.SizeMiB}, }, to: gadget.LaidOutStructure{ - VolumeStructure: &gadget.VolumeStructure{Size: 1*gadget.SizeMiB + 1*gadget.SizeKiB}, + VolumeStructure: &gadget.VolumeStructure{Size: 1*quantity.SizeMiB + 1*quantity.SizeKiB}, }, err: "cannot change structure size from [0-9]+ to [0-9]+", }, { // size change from: gadget.LaidOutStructure{ - VolumeStructure: &gadget.VolumeStructure{Size: 1 * gadget.SizeMiB}, + VolumeStructure: &gadget.VolumeStructure{Size: 1 * quantity.SizeMiB}, }, to: gadget.LaidOutStructure{ - VolumeStructure: &gadget.VolumeStructure{Size: 1 * gadget.SizeMiB}, + VolumeStructure: &gadget.VolumeStructure{Size: 1 * quantity.SizeMiB}, }, err: "", }, @@ -247,22 +248,22 @@ { // explicitly declared start offset change from: gadget.LaidOutStructure{ - VolumeStructure: &gadget.VolumeStructure{Size: 1 * gadget.SizeMiB, Offset: asSizePtr(1024)}, + VolumeStructure: &gadget.VolumeStructure{Size: 1 * quantity.SizeMiB, Offset: asSizePtr(1024)}, StartOffset: 1024, }, to: gadget.LaidOutStructure{ - VolumeStructure: &gadget.VolumeStructure{Size: 1 * gadget.SizeMiB, Offset: asSizePtr(2048)}, + VolumeStructure: &gadget.VolumeStructure{Size: 1 * quantity.SizeMiB, Offset: asSizePtr(2048)}, StartOffset: 2048, }, err: "cannot change structure offset from [0-9]+ to [0-9]+", }, { // explicitly declared start offset in new structure from: gadget.LaidOutStructure{ - VolumeStructure: &gadget.VolumeStructure{Size: 1 * gadget.SizeMiB, Offset: nil}, + VolumeStructure: &gadget.VolumeStructure{Size: 1 * quantity.SizeMiB, Offset: nil}, StartOffset: 1024, }, to: gadget.LaidOutStructure{ - VolumeStructure: &gadget.VolumeStructure{Size: 1 * gadget.SizeMiB, Offset: asSizePtr(2048)}, + VolumeStructure: &gadget.VolumeStructure{Size: 1 * quantity.SizeMiB, Offset: asSizePtr(2048)}, StartOffset: 2048, }, err: "cannot change structure offset from unspecified to [0-9]+", @@ -270,23 +271,23 @@ // explicitly declared start offset in old structure, // missing from new from: gadget.LaidOutStructure{ - VolumeStructure: &gadget.VolumeStructure{Size: 1 * gadget.SizeMiB, Offset: asSizePtr(1024)}, + VolumeStructure: &gadget.VolumeStructure{Size: 1 * quantity.SizeMiB, Offset: asSizePtr(1024)}, StartOffset: 1024, }, to: gadget.LaidOutStructure{ - VolumeStructure: &gadget.VolumeStructure{Size: 1 * gadget.SizeMiB, Offset: nil}, + VolumeStructure: &gadget.VolumeStructure{Size: 1 * quantity.SizeMiB, Offset: nil}, StartOffset: 2048, }, err: "cannot change structure offset from [0-9]+ to unspecified", }, { // start offset changed due to layout from: gadget.LaidOutStructure{ - VolumeStructure: &gadget.VolumeStructure{Size: 1 * gadget.SizeMiB}, - StartOffset: 1 * gadget.SizeMiB, + VolumeStructure: &gadget.VolumeStructure{Size: 1 * quantity.SizeMiB}, + StartOffset: 1 * quantity.SizeMiB, }, to: gadget.LaidOutStructure{ - VolumeStructure: &gadget.VolumeStructure{Size: 1 * gadget.SizeMiB}, - StartOffset: 2 * gadget.SizeMiB, + VolumeStructure: &gadget.VolumeStructure{Size: 1 * quantity.SizeMiB}, + StartOffset: 2 * quantity.SizeMiB, }, err: "cannot change structure start offset from [0-9]+ to [0-9]+", }, @@ -645,14 +646,14 @@ // prepare the stage bareStruct := gadget.VolumeStructure{ Name: "first", - Size: 5 * gadget.SizeMiB, + Size: 5 * quantity.SizeMiB, Content: []gadget.VolumeContent{ {Image: "first.img"}, }, } fsStruct := gadget.VolumeStructure{ Name: "second", - Size: 10 * gadget.SizeMiB, + Size: 10 * quantity.SizeMiB, Filesystem: "ext4", Content: []gadget.VolumeContent{ {Source: "/second-content", Target: "/"}, @@ -660,7 +661,7 @@ } lastStruct := gadget.VolumeStructure{ Name: "third", - Size: 5 * gadget.SizeMiB, + Size: 5 * quantity.SizeMiB, Filesystem: "vfat", Content: []gadget.VolumeContent{ {Source: "/third-content", Target: "/"}, @@ -688,15 +689,15 @@ } oldRootDir := c.MkDir() - makeSizedFile(c, filepath.Join(oldRootDir, "first.img"), gadget.SizeMiB, nil) + makeSizedFile(c, filepath.Join(oldRootDir, "first.img"), quantity.SizeMiB, nil) makeSizedFile(c, filepath.Join(oldRootDir, "/second-content/foo"), 0, nil) makeSizedFile(c, filepath.Join(oldRootDir, "/third-content/bar"), 0, nil) oldData = gadget.GadgetData{Info: oldInfo, RootDir: oldRootDir} newRootDir := c.MkDir() - makeSizedFile(c, filepath.Join(newRootDir, "first.img"), 900*gadget.SizeKiB, nil) - makeSizedFile(c, filepath.Join(newRootDir, "/second-content/foo"), gadget.SizeKiB, nil) - makeSizedFile(c, filepath.Join(newRootDir, "/third-content/bar"), gadget.SizeKiB, nil) + makeSizedFile(c, filepath.Join(newRootDir, "first.img"), 900*quantity.SizeKiB, nil) + makeSizedFile(c, filepath.Join(newRootDir, "/second-content/foo"), quantity.SizeKiB, nil) + makeSizedFile(c, filepath.Join(newRootDir, "/third-content/bar"), quantity.SizeKiB, nil) newData = gadget.GadgetData{Info: newInfo, RootDir: newRootDir} rollbackDir = c.MkDir() @@ -711,8 +712,8 @@ } func (m *mockUpdateProcessObserver) Observe(op gadget.ContentOperation, sourceStruct *gadget.LaidOutStructure, - targetRootDir, relativeTargetPath string, data *gadget.ContentChange) (bool, error) { - return false, errors.New("unexpected call") + targetRootDir, relativeTargetPath string, data *gadget.ContentChange) (gadget.ContentChangeAction, error) { + return gadget.ChangeAbort, errors.New("unexpected call") } func (m *mockUpdateProcessObserver) BeforeWrite() error { @@ -745,21 +746,21 @@ case 0: c.Check(ps.Name, Equals, "first") c.Check(ps.HasFilesystem(), Equals, false) - c.Check(ps.Size, Equals, 5*gadget.SizeMiB) + c.Check(ps.Size, Equals, 5*quantity.SizeMiB) c.Check(ps.IsPartition(), Equals, true) // non MBR start offset defaults to 1MiB - c.Check(ps.StartOffset, Equals, 1*gadget.SizeMiB) + c.Check(ps.StartOffset, Equals, 1*quantity.SizeMiB) c.Assert(ps.LaidOutContent, HasLen, 1) c.Check(ps.LaidOutContent[0].Image, Equals, "first.img") - c.Check(ps.LaidOutContent[0].Size, Equals, 900*gadget.SizeKiB) + c.Check(ps.LaidOutContent[0].Size, Equals, 900*quantity.SizeKiB) case 1: c.Check(ps.Name, Equals, "second") c.Check(ps.HasFilesystem(), Equals, true) c.Check(ps.Filesystem, Equals, "ext4") c.Check(ps.IsPartition(), Equals, true) - c.Check(ps.Size, Equals, 10*gadget.SizeMiB) + c.Check(ps.Size, Equals, 10*quantity.SizeMiB) // foo's start offset + foo's size - c.Check(ps.StartOffset, Equals, (1+5)*gadget.SizeMiB) + c.Check(ps.StartOffset, Equals, (1+5)*quantity.SizeMiB) c.Assert(ps.LaidOutContent, HasLen, 0) c.Assert(ps.Content, HasLen, 1) c.Check(ps.Content[0].Source, Equals, "/second-content") @@ -850,7 +851,7 @@ // prepare the stage bareStruct := gadget.VolumeStructure{ Name: "foo", - Size: 5 * gadget.SizeMiB, + Size: 5 * quantity.SizeMiB, Content: []gadget.VolumeContent{ {Image: "first.img"}, }, @@ -889,7 +890,7 @@ err := gadget.Update(oldData, newData, rollbackDir, nil, nil) c.Assert(err, ErrorMatches, `cannot lay out the new volume: cannot lay out structure #0 \("foo"\): content "first.img": .* no such file or directory`) - makeSizedFile(c, filepath.Join(newRootDir, "first.img"), gadget.SizeMiB, nil) + makeSizedFile(c, filepath.Join(newRootDir, "first.img"), quantity.SizeMiB, nil) // Update does not error out when when the bare struct data of the old volume is missing err = gadget.Update(oldData, newData, rollbackDir, nil, nil) @@ -900,7 +901,7 @@ // prepare the stage bareStruct := gadget.VolumeStructure{ Name: "foo", - Size: 5 * gadget.SizeMiB, + Size: 5 * quantity.SizeMiB, Content: []gadget.VolumeContent{ {Image: "first.img"}, }, @@ -936,8 +937,8 @@ rollbackDir := c.MkDir() - makeSizedFile(c, filepath.Join(oldRootDir, "first.img"), gadget.SizeMiB, nil) - makeSizedFile(c, filepath.Join(newRootDir, "first.img"), 900*gadget.SizeKiB, nil) + makeSizedFile(c, filepath.Join(oldRootDir, "first.img"), quantity.SizeMiB, nil) + makeSizedFile(c, filepath.Join(newRootDir, "first.img"), 900*quantity.SizeKiB, nil) err := gadget.Update(oldData, newData, rollbackDir, nil, nil) c.Assert(err, ErrorMatches, `cannot apply update to volume: cannot change the number of structures within volume from 1 to 2`) @@ -947,7 +948,7 @@ // prepare the stage bareStruct := gadget.VolumeStructure{ Name: "foo", - Size: 5 * gadget.SizeMiB, + Size: 5 * quantity.SizeMiB, Content: []gadget.VolumeContent{ {Image: "first.img"}, }, @@ -955,7 +956,7 @@ fsStruct := gadget.VolumeStructure{ Name: "foo", Filesystem: "ext4", - Size: 5 * gadget.SizeMiB, + Size: 5 * quantity.SizeMiB, Content: []gadget.VolumeContent{ {Source: "/", Target: "/"}, }, @@ -988,7 +989,7 @@ rollbackDir := c.MkDir() - makeSizedFile(c, filepath.Join(oldRootDir, "first.img"), gadget.SizeMiB, nil) + makeSizedFile(c, filepath.Join(oldRootDir, "first.img"), quantity.SizeMiB, nil) err := gadget.Update(oldData, newData, rollbackDir, nil, nil) c.Assert(err, ErrorMatches, `cannot update volume structure #0 \("foo"\): cannot change a bare structure to filesystem one`) @@ -998,7 +999,7 @@ // prepare the stage bareStruct := gadget.VolumeStructure{ Name: "foo", - Size: 5 * gadget.SizeMiB, + Size: 5 * quantity.SizeMiB, Content: []gadget.VolumeContent{ {Image: "first.img"}, }, @@ -1037,7 +1038,7 @@ // prepare the stage bareStruct := gadget.VolumeStructure{ Name: "foo", - Size: 5 * gadget.SizeMiB, + Size: 5 * quantity.SizeMiB, Content: []gadget.VolumeContent{ {Image: "first.img"}, }, @@ -1057,13 +1058,13 @@ oldRootDir := c.MkDir() oldData := gadget.GadgetData{Info: oldInfo, RootDir: oldRootDir} - makeSizedFile(c, filepath.Join(oldRootDir, "first.img"), gadget.SizeMiB, nil) + makeSizedFile(c, filepath.Join(oldRootDir, "first.img"), quantity.SizeMiB, nil) newRootDir := c.MkDir() // same volume description newData := gadget.GadgetData{Info: oldInfo, RootDir: newRootDir} // different content, but updates are opt in - makeSizedFile(c, filepath.Join(newRootDir, "first.img"), 900*gadget.SizeKiB, nil) + makeSizedFile(c, filepath.Join(newRootDir, "first.img"), 900*quantity.SizeKiB, nil) rollbackDir := c.MkDir() @@ -1087,7 +1088,7 @@ noPartitionStruct := gadget.VolumeStructure{ Name: "no-partition", Type: "bare", - Size: 5 * gadget.SizeMiB, + Size: 5 * quantity.SizeMiB, Content: []gadget.VolumeContent{ {Image: "first.img"}, }, @@ -1416,9 +1417,9 @@ psBare := &gadget.LaidOutStructure{ VolumeStructure: &gadget.VolumeStructure{ Filesystem: "none", - Size: 10 * gadget.SizeMiB, + Size: 10 * quantity.SizeMiB, }, - StartOffset: 1 * gadget.SizeMiB, + StartOffset: 1 * quantity.SizeMiB, } updater, err := gadget.UpdaterForStructure(psBare, gadgetRootDir, rollbackDir, nil) c.Assert(err, IsNil) @@ -1427,10 +1428,10 @@ psFs := &gadget.LaidOutStructure{ VolumeStructure: &gadget.VolumeStructure{ Filesystem: "ext4", - Size: 10 * gadget.SizeMiB, + Size: 10 * quantity.SizeMiB, Label: "writable", }, - StartOffset: 1 * gadget.SizeMiB, + StartOffset: 1 * quantity.SizeMiB, } updater, err = gadget.UpdaterForStructure(psFs, gadgetRootDir, rollbackDir, nil) c.Assert(err, IsNil) diff -Nru snapd-2.47.1+20.10.1build1/gadget/validate.go snapd-2.48+21.04/gadget/validate.go --- snapd-2.47.1+20.10.1build1/gadget/validate.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/gadget/validate.go 2020-11-19 16:51:02.000000000 +0000 @@ -50,10 +50,33 @@ return nil } +func validateEncryptionSupport(info *Info) error { + for name, vol := range info.Volumes { + var haveSave bool + for _, s := range vol.Structure { + if s.Role == SystemSave { + haveSave = true + } + } + if !haveSave { + return fmt.Errorf("volume %q has no structure with system-save role", name) + } + // XXX: shall we make sure that size of ubuntu-save is reasonable? + } + return nil +} + +type ValidationConstraints struct { + // EncryptedData when true indicates that the gadget will be used on a + // device where the data partition will be encrypted. + EncryptedData bool +} + // Validate checks whether the given directory contains valid gadget snap // metadata and a matching content, under the provided model constraints, which -// are handled identically to ReadInfo(). -func Validate(gadgetSnapRootDir string, model Model) error { +// are handled identically to ReadInfo(). Optionally takes additional validation +// constraints, which for instance may only be known at run time, +func Validate(gadgetSnapRootDir string, model Model, extra *ValidationConstraints) error { info, err := ReadInfo(gadgetSnapRootDir, model) if err != nil { return fmt.Errorf("invalid gadget metadata: %v", err) @@ -68,6 +91,12 @@ return fmt.Errorf("invalid volume %q: %v", name, err) } } - + if extra != nil { + if extra.EncryptedData { + if err := validateEncryptionSupport(info); err != nil { + return fmt.Errorf("gadget does not support encrypted data: %v", err) + } + } + } return nil } diff -Nru snapd-2.47.1+20.10.1build1/gadget/validate_test.go snapd-2.48+21.04/gadget/validate_test.go --- snapd-2.47.1+20.10.1build1/gadget/validate_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/gadget/validate_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -55,7 +55,7 @@ ` makeSizedFile(c, filepath.Join(s.dir, "meta/gadget.yaml"), 0, []byte(gadgetYamlContent)) - err := gadget.Validate(s.dir, nil) + err := gadget.Validate(s.dir, nil, nil) c.Assert(err, ErrorMatches, `invalid layout of volume "pc": cannot lay out structure #0 \("foo"\): content "foo.img": stat .*/foo.img: no such file or directory`) } @@ -83,7 +83,7 @@ // only content for the first volume makeSizedFile(c, filepath.Join(s.dir, "first.img"), 1, nil) - err := gadget.Validate(s.dir, nil) + err := gadget.Validate(s.dir, nil, nil) c.Assert(err, ErrorMatches, `invalid layout of volume "second": cannot lay out structure #0 \("second-foo"\): content "second.img": stat .*/second.img: no such file or directory`) } @@ -100,7 +100,7 @@ ` makeSizedFile(c, filepath.Join(s.dir, "meta/gadget.yaml"), 0, []byte(gadgetYamlContent)) - err := gadget.Validate(s.dir, nil) + err := gadget.Validate(s.dir, nil, nil) c.Assert(err, ErrorMatches, `invalid gadget metadata: bootloader must be one of .*`) } @@ -121,13 +121,13 @@ ` makeSizedFile(c, filepath.Join(s.dir, "meta/gadget.yaml"), 0, []byte(gadgetYamlContent)) - err := gadget.Validate(s.dir, nil) + err := gadget.Validate(s.dir, nil, nil) c.Assert(err, ErrorMatches, `invalid volume "bad": structure #0 \("bad-struct"\), content source:foo/: source path does not exist`) // make it a file, which conflicts with foo/ as 'source' fooPath := filepath.Join(s.dir, "foo") makeSizedFile(c, fooPath, 1, nil) - err = gadget.Validate(s.dir, nil) + err = gadget.Validate(s.dir, nil, nil) c.Assert(err, ErrorMatches, `invalid volume "bad": structure #0 \("bad-struct"\), content source:foo/: cannot specify trailing / for a source which is not a directory`) // make it a directory @@ -136,7 +136,7 @@ err = os.Mkdir(fooPath, 0755) c.Assert(err, IsNil) // validate should no longer complain - err = gadget.Validate(s.dir, nil) + err = gadget.Validate(s.dir, nil, nil) c.Assert(err, IsNil) } @@ -146,13 +146,13 @@ ` makeSizedFile(c, filepath.Join(s.dir, "meta/gadget.yaml"), 0, []byte(gadgetYamlContent)) - err := gadget.Validate(s.dir, nil) + err := gadget.Validate(s.dir, nil, nil) c.Assert(err, IsNil) - err = gadget.Validate(s.dir, &modelConstraints{classic: true}) + err = gadget.Validate(s.dir, &modelConstraints{classic: true}, nil) c.Assert(err, IsNil) - err = gadget.Validate(s.dir, &modelConstraints{classic: false}) + err = gadget.Validate(s.dir, &modelConstraints{classic: false}, nil) c.Assert(err, ErrorMatches, "invalid gadget metadata: bootloader not declared in any volume") } @@ -174,7 +174,52 @@ role: %[1]s `, role) makeSizedFile(c, filepath.Join(s.dir, "meta/gadget.yaml"), 0, []byte(gadgetYamlContent)) - err := gadget.Validate(s.dir, nil) + err := gadget.Validate(s.dir, nil, nil) c.Assert(err, ErrorMatches, fmt.Sprintf(`invalid gadget metadata: invalid volume "pc": cannot have more than one partition with %s role`, role)) } } + +var gadgetYamlContentNoSave = ` +volumes: + vol1: + bootloader: grub + structure: + - name: ubuntu-seed + role: system-seed + type: DA,21686148-6449-6E6F-744E-656564454649 + size: 1M + filesystem: ext4 + - name: ubuntu-boot + type: DA,21686148-6449-6E6F-744E-656564454649 + size: 1M + filesystem: ext4 + - name: ubuntu-data + role: system-data + type: DA,21686148-6449-6E6F-744E-656564454649 + size: 1M + filesystem: ext4 +` + +var gadgetYamlContentWithSave = gadgetYamlContentNoSave + ` + - name: ubuntu-save + role: system-save + type: DA,21686148-6449-6E6F-744E-656564454649 + size: 1M + filesystem: ext4 +` + +func (s *validateGadgetTestSuite) TestValidateEncryptionSupportErr(c *C) { + makeSizedFile(c, filepath.Join(s.dir, "meta/gadget.yaml"), 0, []byte(gadgetYamlContentNoSave)) + err := gadget.Validate(s.dir, &modelConstraints{systemSeed: true}, &gadget.ValidationConstraints{ + EncryptedData: true, + }) + c.Assert(err, ErrorMatches, `gadget does not support encrypted data: volume "vol1" has no structure with system-save role`) +} + +func (s *validateGadgetTestSuite) TestValidateEncryptionSupportHappy(c *C) { + makeSizedFile(c, filepath.Join(s.dir, "meta/gadget.yaml"), 0, []byte(gadgetYamlContentWithSave)) + err := gadget.Validate(s.dir, &modelConstraints{systemSeed: true}, &gadget.ValidationConstraints{ + EncryptedData: true, + }) + c.Assert(err, IsNil) +} diff -Nru snapd-2.47.1+20.10.1build1/.github/workflows/cla-check.yaml snapd-2.48+21.04/.github/workflows/cla-check.yaml --- snapd-2.47.1+20.10.1build1/.github/workflows/cla-check.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/.github/workflows/cla-check.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -20,6 +20,8 @@ # The cla_check script reads git commit history, so can't # use a shallow checkout. fetch-depth: 0 + # ensure we pull PR /head, not autogenerated /merge commit + ref: ${{ github.event.pull_request.head.sha }} - name: Fetching base ref ${{ github.base_ref }} run: git fetch origin ${{ github.base_ref }}:${{ github.base_ref }} - name: CLA check diff -Nru snapd-2.47.1+20.10.1build1/.github/workflows/test.yaml snapd-2.48+21.04/.github/workflows/test.yaml --- snapd-2.47.1+20.10.1build1/.github/workflows/test.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/.github/workflows/test.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -21,7 +21,7 @@ id: cached-results run: | CACHE_RESULT_STAMP="${{ github.workspace }}/.test-results/snap-build-success" - echo "::set-env name=CACHE_RESULT_STAMP::$CACHE_RESULT_STAMP" + echo "CACHE_RESULT_STAMP=$CACHE_RESULT_STAMP" >> $GITHUB_ENV if [ -e "$CACHE_RESULT_STAMP" ]; then has_cached_snap=0 while read name; do @@ -110,7 +110,7 @@ id: cached-results run: | CACHE_RESULT_STAMP="${{ github.workspace }}/.test-results/${{ matrix.gochannel }}-success" - echo "::set-env name=CACHE_RESULT_STAMP::$CACHE_RESULT_STAMP" + echo "CACHE_RESULT_STAMP=$CACHE_RESULT_STAMP" >> $GITHUB_ENV if [ -e "$CACHE_RESULT_STAMP" ]; then echo "::set-output name=already-ran::true" fi @@ -168,6 +168,16 @@ run: | cd ${{ github.workspace }}/src/github.com/snapcore/snapd || exit 1 ./run-checks --unit + - name: Test Go (nosecboot) + if: steps.cached-results.outputs.already-ran != 'true' + run: | + cd ${{ github.workspace }}/src/github.com/snapcore/snapd || exit 1 + echo "Dropping github.com/snapcore/secboot" + # use govendor remove so that a subsequent govendor sync does not + # install secboot again + ${{ github.workspace }}/bin/govendor remove github.com/snapcore/secboot + ${{ github.workspace }}/bin/govendor remove +unused + SKIP_DIRTY_CHECK=1 GO_BUILD_TAGS=nosecboot ./run-checks --unit - name: Cache successful run run: | mkdir -p $(dirname "$CACHE_RESULT_STAMP") @@ -219,7 +229,7 @@ id: cached-results run: | CACHE_RESULT_STAMP="${{ github.workspace }}/.test-results/${{ matrix.system }}-success" - echo "::set-env name=CACHE_RESULT_STAMP::$CACHE_RESULT_STAMP" + echo "CACHE_RESULT_STAMP=$CACHE_RESULT_STAMP" >> $GITHUB_ENV if [ -e "$CACHE_RESULT_STAMP" ]; then echo "::set-output name=already-ran::true" fi @@ -271,7 +281,7 @@ id: cached-results run: | CACHE_RESULT_STAMP="${{ github.workspace }}/.test-results/${{ matrix.system }}-nested-success" - echo "::set-env name=CACHE_RESULT_STAMP::$CACHE_RESULT_STAMP" + echo "CACHE_RESULT_STAMP=$CACHE_RESULT_STAMP" >> $GITHUB_ENV if [ -e "$CACHE_RESULT_STAMP" ]; then echo "::set-output name=already-ran::true" fi @@ -284,9 +294,6 @@ echo "::add-matcher::.github/spread-problem-matcher.json" export NESTED_BUILD_SNAPD_FROM_CURRENT=true export NESTED_ENABLE_KVM=true - if [ "${{ matrix.system }}" = "ubuntu-20.04-64" ]; then - export NESTED_ENABLE_KVM=false - fi spread -abend google-nested:${{ matrix.system }}:tests/nested/... - name: Cache successful run run: | diff -Nru snapd-2.47.1+20.10.1build1/httputil/retry.go snapd-2.48+21.04/httputil/retry.go --- snapd-2.47.1+20.10.1build1/httputil/retry.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/httputil/retry.go 2020-11-19 16:51:02.000000000 +0000 @@ -36,11 +36,11 @@ "github.com/snapcore/snapd/osutil" ) -type PerstistentNetworkError struct { +type PersistentNetworkError struct { Err error } -func (e *PerstistentNetworkError) Error() string { +func (e *PersistentNetworkError) Error() string { return fmt.Sprintf("persistent network error: %v", e.Err) } @@ -264,7 +264,7 @@ } if isNetworkDown(err) || isDnsUnavailable(err) { - err = &PerstistentNetworkError{Err: err} + err = &PersistentNetworkError{Err: err} } break } diff -Nru snapd-2.47.1+20.10.1build1/i18n/xgettext-go/main.go snapd-2.48+21.04/i18n/xgettext-go/main.go --- snapd-2.47.1+20.10.1build1/i18n/xgettext-go/main.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/i18n/xgettext-go/main.go 2020-11-19 16:51:02.000000000 +0000 @@ -10,6 +10,7 @@ "io/ioutil" "log" "os" + "path/filepath" "sort" "strings" "time" @@ -170,8 +171,26 @@ return nil } +func readContent(fname string) (content []byte, err error) { + // If no search directories have been specified or we have an + // absolute path, just try to read the contents directly. + if len(opts.Directories) == 0 || filepath.IsAbs(fname) { + return ioutil.ReadFile(fname) + } + + // Otherwise, search for the file in each of the configured + // directories. + for _, dir := range opts.Directories { + content, err = ioutil.ReadFile(filepath.Join(dir, fname)) + if !os.IsNotExist(err) { + break + } + } + return content, err +} + func processSingleGoSource(fset *token.FileSet, fname string) error { - fnameContent, err := ioutil.ReadFile(fname) + fnameContent, err := readContent(fname) if err != nil { return err } @@ -277,6 +296,8 @@ var opts struct { FilesFrom string `short:"f" long:"files-from" description:"get list of input files from FILE"` + Directories []string `short:"D" long:"directory" description:"add DIRECTORY to list for input files search"` + Output string `short:"o" long:"output" description:"output to specified file"` AddComments bool `short:"c" long:"add-comments" description:"place all comment blocks preceding keyword lines in output file"` diff -Nru snapd-2.47.1+20.10.1build1/image/image_test.go snapd-2.48+21.04/image/image_test.go --- snapd-2.47.1+20.10.1build1/image/image_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/image/image_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -38,6 +38,7 @@ "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/image" "github.com/snapcore/snapd/osutil" @@ -2568,10 +2569,8 @@ return s.Brands.Model("my-brand", "my-model", headers) } -func (s *imageSuite) TestSetupSeedCore20(c *C) { - bl := bootloadertest.Mock("grub", c.MkDir()).RecoveryAware() - bootloader.Force(bl) - +func (s *imageSuite) TestSetupSeedCore20Grub(c *C) { + bootloader.Force(nil) restore := image.MockTrusted(s.StoreSigning.Trusted) defer restore() @@ -2642,28 +2641,34 @@ // check boot config grubCfg := filepath.Join(prepareDir, "system-seed", "EFI/ubuntu/grub.cfg") + seedGrubenv := filepath.Join(prepareDir, "system-seed", "EFI/ubuntu/grubenv") grubRecoveryCfgAsset := assets.Internal("grub-recovery.cfg") c.Assert(grubRecoveryCfgAsset, NotNil) c.Check(grubCfg, testutil.FileEquals, string(grubRecoveryCfgAsset)) - // make sure that grub.cfg is the only file present inside the directory + // make sure that grub.cfg and grubenv are the only files present inside + // the directory gl, err := filepath.Glob(filepath.Join(prepareDir, "system-seed/EFI/ubuntu/*")) c.Assert(err, IsNil) - c.Check(gl, DeepEquals, []string{grubCfg}) - - c.Check(s.stderr.String(), Equals, "") + c.Check(gl, DeepEquals, []string{ + grubCfg, + seedGrubenv, + }) // check recovery system specific config systems, err := filepath.Glob(filepath.Join(seeddir, "systems", "*")) c.Assert(err, IsNil) c.Assert(systems, HasLen, 1) - c.Check(bl.RecoverySystemDir, Equals, fmt.Sprintf("/systems/%s", filepath.Base(systems[0]))) - c.Check(bl.RecoverySystemBootVars, DeepEquals, map[string]string{ - "snapd_recovery_kernel": "/snaps/pc-kernel_1.snap", - }) - c.Check(bl.BootVars, DeepEquals, map[string]string{ - "snapd_recovery_system": filepath.Base(systems[0]), - }) + seedGenv := grubenv.NewEnv(seedGrubenv) + c.Assert(seedGenv.Load(), IsNil) + c.Check(seedGenv.Get("snapd_recovery_system"), Equals, filepath.Base(systems[0])) + c.Check(seedGenv.Get("snapd_recovery_mode"), Equals, "install") + + c.Check(s.stderr.String(), Equals, "") + + systemGenv := grubenv.NewEnv(filepath.Join(systems[0], "grubenv")) + c.Assert(systemGenv.Load(), IsNil) + c.Check(systemGenv.Get("snapd_recovery_kernel"), Equals, "/snaps/pc-kernel_1.snap") // check the downloads c.Check(s.storeActions, HasLen, 5) @@ -2769,6 +2774,7 @@ env, err := ubootenv.Open(bootSel) c.Assert(err, IsNil) c.Assert(env.Get("snapd_recovery_system"), Equals, expectedLabel) + c.Assert(env.Get("snapd_recovery_mode"), Equals, "install") // check recovery system specific config systems, err := filepath.Glob(filepath.Join(seeddir, "systems", "*")) diff -Nru snapd-2.47.1+20.10.1build1/interfaces/apparmor/template.go snapd-2.48+21.04/interfaces/apparmor/template.go --- snapd-2.47.1+20.10.1build1/interfaces/apparmor/template.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/interfaces/apparmor/template.go 2020-11-19 16:51:02.000000000 +0000 @@ -231,7 +231,7 @@ # example, allow access to /dev/std{in,out,err} which are all symlinks to # /proc/self/fd/{0,1,2} respectively. To support the open(..., O_TMPFILE) # linkat() temporary file technique, allow all fds. Importantly, access to - # another's task's fd via this proc interface is mediated via 'ptrace (read)' + # another task's fd via this proc interface is mediated via 'ptrace (read)' # (readonly) and 'ptrace (trace)' (read/write) which is denied by default, so # this rule by itself doesn't allow opening another snap's fds via proc. owner @{PROC}/@{pid}/{,task/@{tid}}fd/[0-9]* rw, @@ -314,6 +314,9 @@ # Read-only of this snap /var/lib/snapd/snaps/@{SNAP_NAME}_*.snap r, + # Read-only of snapd restart state for snapctl specifically + /var/lib/snapd/maintenance.json r, + # Read-only for the install directory # bind mount used here (see 'parallel installs', above) @{INSTALL_DIR}/{@{SNAP_NAME},@{SNAP_INSTANCE_NAME}}/ r, diff -Nru snapd-2.47.1+20.10.1build1/interfaces/builtin/bluez_test.go snapd-2.48+21.04/interfaces/builtin/bluez_test.go --- snapd-2.47.1+20.10.1build1/interfaces/builtin/bluez_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/interfaces/builtin/bluez_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -100,6 +100,7 @@ ` const bluezCoreYaml = `name: core +type: os version: 0 slots: bluez: diff -Nru snapd-2.47.1+20.10.1build1/interfaces/builtin/fwupd.go snapd-2.48+21.04/interfaces/builtin/fwupd.go --- snapd-2.47.1+20.10.1build1/interfaces/builtin/fwupd.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/interfaces/builtin/fwupd.go 2020-11-19 16:51:02.000000000 +0000 @@ -72,6 +72,10 @@ # Allow write access for efi firmware updater /boot/efi/{,**/} r, + # allow access to fwupd* and fw/ under boot/ for core systems + /boot/efi/EFI/boot/fwupd*.efi* rw, + /boot/efi/EFI/boot/fw/** rw, + # allow access to fwupd* and fw/ under ubuntu/ for classic systems /boot/efi/EFI/ubuntu/fwupd*.efi* rw, /boot/efi/EFI/ubuntu/fw/** rw, diff -Nru snapd-2.47.1+20.10.1build1/interfaces/builtin/kvm_test.go snapd-2.48+21.04/interfaces/builtin/kvm_test.go --- snapd-2.47.1+20.10.1build1/interfaces/builtin/kvm_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/interfaces/builtin/kvm_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -20,6 +20,7 @@ package builtin_test import ( + "fmt" "io/ioutil" "path/filepath" @@ -117,7 +118,7 @@ c.Assert(spec.Snippets(), HasLen, 2) c.Assert(spec.Snippets()[0], Equals, `# kvm KERNEL=="kvm", TAG+="snap_consumer_app"`) - c.Assert(spec.Snippets(), testutil.Contains, `TAG=="snap_consumer_app", RUN+="/usr/lib/snapd/snap-device-helper $env{ACTION} snap_consumer_app $devpath $major:$minor"`) + c.Assert(spec.Snippets(), testutil.Contains, fmt.Sprintf(`TAG=="snap_consumer_app", RUN+="%s/snap-device-helper $env{ACTION} snap_consumer_app $devpath $major:$minor"`, dirs.DistroLibExecDir)) } func (s *kvmInterfaceSuite) TestStaticInfo(c *C) { diff -Nru snapd-2.47.1+20.10.1build1/interfaces/builtin/media_hub.go snapd-2.48+21.04/interfaces/builtin/media_hub.go --- snapd-2.47.1+20.10.1build1/interfaces/builtin/media_hub.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/interfaces/builtin/media_hub.go 2020-11-19 16:51:02.000000000 +0000 @@ -41,7 +41,7 @@ ` const mediaHubPermanentSlotAppArmor = ` -# Description: Allow operating as the the media-hub service. +# Description: Allow operating as the media-hub service. # DBus accesses #include diff -Nru snapd-2.47.1+20.10.1build1/interfaces/builtin/network_manager.go snapd-2.48+21.04/interfaces/builtin/network_manager.go --- snapd-2.47.1+20.10.1build1/interfaces/builtin/network_manager.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/interfaces/builtin/network_manager.go 2020-11-19 16:51:02.000000000 +0000 @@ -186,6 +186,13 @@ interface=org.freedesktop.DBus.* peer=(label=unconfined), +# Allow ObjectManager methods from and signals to unconfined clients. +dbus (receive, send) + bus=system + path=/org/freedesktop + interface=org.freedesktop.DBus.ObjectManager + peer=(label=unconfined), + # Allow access to hostname system service dbus (receive, send) bus=system diff -Nru snapd-2.47.1+20.10.1build1/interfaces/builtin/network_manager_observe_test.go snapd-2.48+21.04/interfaces/builtin/network_manager_observe_test.go --- snapd-2.47.1+20.10.1build1/interfaces/builtin/network_manager_observe_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/interfaces/builtin/network_manager_observe_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -59,6 +59,7 @@ ` const networkManagerObserveCoreYaml = `name: core +type: os version: 0 slots: network-manager-observe: diff -Nru snapd-2.47.1+20.10.1build1/interfaces/builtin/ptp.go snapd-2.48+21.04/interfaces/builtin/ptp.go --- snapd-2.47.1+20.10.1build1/interfaces/builtin/ptp.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48+21.04/interfaces/builtin/ptp.go 2020-11-19 16:51:02.000000000 +0000 @@ -0,0 +1,55 @@ +// -*- 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 builtin + +const ptpSummary = `allows access to the PTP Hardware Clock subsystem` + +const ptpBaseDeclarationSlots = ` + ptp: + allow-installation: + slot-snap-type: + - core + deny-auto-connection: true +` + +const ptpConnectedPlugAppArmor = ` +# Description: Can access PTP Hardware Clock subsystem. +# Devices +/dev/ptp[0-9]* rw, +# /sys/class/ptp specified by the kernel docs +/sys/class/ptp/ptp[0-9]*/{extts_enable,period,pps_enable} w, +/sys/class/ptp/ptp[0-9]*/* r, +` + +var ptpConnectedPlugUDev = []string{ + `SUBSYSTEM=="ptp", KERNEL=="ptp[0-9]*"`, +} + +func init() { + registerIface(&commonInterface{ + name: "ptp", + summary: ptpSummary, + implicitOnCore: true, + implicitOnClassic: true, + baseDeclarationSlots: ptpBaseDeclarationSlots, + connectedPlugAppArmor: ptpConnectedPlugAppArmor, + connectedPlugUDev: ptpConnectedPlugUDev, + }) +} diff -Nru snapd-2.47.1+20.10.1build1/interfaces/builtin/ptp_test.go snapd-2.48+21.04/interfaces/builtin/ptp_test.go --- snapd-2.47.1+20.10.1build1/interfaces/builtin/ptp_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48+21.04/interfaces/builtin/ptp_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -0,0 +1,106 @@ +// -*- 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 builtin_test + +import ( + "github.com/snapcore/snapd/interfaces/apparmor" + "github.com/snapcore/snapd/interfaces/udev" + "github.com/snapcore/snapd/testutil" + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/interfaces" + "github.com/snapcore/snapd/interfaces/builtin" + "github.com/snapcore/snapd/snap" +) + +type PTPInterfaceSuite struct { + iface interfaces.Interface + slotInfo *snap.SlotInfo + slot *interfaces.ConnectedSlot + plugInfo *snap.PlugInfo + plug *interfaces.ConnectedPlug +} + +var _ = Suite(&PTPInterfaceSuite{ + iface: builtin.MustInterface("ptp"), +}) + +const ptpConsumerYaml = `name: consumer +version: 0 +apps: + app: + plugs: [ptp] +` + +const ptpCoreYaml = `name: core +version: 0 +type: os +slots: + ptp: +` + +func (s *PTPInterfaceSuite) SetUpTest(c *C) { + s.plug, s.plugInfo = MockConnectedPlug(c, ptpConsumerYaml, nil, "ptp") + s.slot, s.slotInfo = MockConnectedSlot(c, ptpCoreYaml, nil, "ptp") +} + +func (s *PTPInterfaceSuite) TestName(c *C) { + c.Assert(s.iface.Name(), Equals, "ptp") +} + +func (s *PTPInterfaceSuite) TestSanitizeSlot(c *C) { + c.Assert(interfaces.BeforePrepareSlot(s.iface, s.slotInfo), IsNil) +} + +func (s *PTPInterfaceSuite) TestSanitizePlug(c *C) { + c.Assert(interfaces.BeforePreparePlug(s.iface, s.plugInfo), IsNil) +} + +func (s *PTPInterfaceSuite) TestAppArmorSpec(c *C) { + spec := &apparmor.Specification{} + c.Assert(spec.AddConnectedPlug(s.iface, s.plug, s.slot), IsNil) + c.Assert(spec.SecurityTags(), DeepEquals, []string{"snap.consumer.app"}) + c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, "/dev/ptp[0-9]*") +} + +func (s *PTPInterfaceSuite) TestUDevSpec(c *C) { + spec := &udev.Specification{} + c.Assert(spec.AddConnectedPlug(s.iface, s.plug, s.slot), IsNil) + c.Assert(spec.Snippets(), HasLen, 2) + c.Assert(spec.Snippets(), testutil.Contains, `# ptp +SUBSYSTEM=="ptp", KERNEL=="ptp[0-9]*", TAG+="snap_consumer_app"`) + c.Assert(spec.Snippets(), testutil.Contains, `TAG=="snap_consumer_app", RUN+="/usr/lib/snapd/snap-device-helper $env{ACTION} snap_consumer_app $devpath $major:$minor"`) +} + +func (s *PTPInterfaceSuite) TestStaticInfo(c *C) { + si := interfaces.StaticInfoOf(s.iface) + c.Assert(si.ImplicitOnCore, Equals, true) + c.Assert(si.ImplicitOnClassic, Equals, true) + c.Assert(si.Summary, Equals, `allows access to the PTP Hardware Clock subsystem`) + c.Assert(si.BaseDeclarationSlots, testutil.Contains, "ptp") +} + +func (s *PTPInterfaceSuite) TestAutoConnect(c *C) { + c.Assert(s.iface.AutoConnect(s.plugInfo, s.slotInfo), Equals, true) +} + +func (s *PTPInterfaceSuite) TestInterfaces(c *C) { + c.Check(builtin.Interfaces(), testutil.DeepContains, s.iface) +} diff -Nru snapd-2.47.1+20.10.1build1/interfaces/builtin/raw_usb.go snapd-2.48+21.04/interfaces/builtin/raw_usb.go --- snapd-2.47.1+20.10.1build1/interfaces/builtin/raw_usb.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/interfaces/builtin/raw_usb.go 2020-11-19 16:51:02.000000000 +0000 @@ -36,6 +36,7 @@ # Allow access to all ttyUSB devices too /dev/tty{USB,ACM}[0-9]* rwk, +@{PROC}/tty/drivers r, # Allow raw access to USB printers (i.e. for receipt printers in POS systems). /dev/usb/lp[0-9]* rwk, diff -Nru snapd-2.47.1+20.10.1build1/interfaces/builtin/x11.go snapd-2.48+21.04/interfaces/builtin/x11.go --- snapd-2.47.1+20.10.1build1/interfaces/builtin/x11.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/interfaces/builtin/x11.go 2020-11-19 16:51:02.000000000 +0000 @@ -20,13 +20,15 @@ package builtin import ( + "fmt" "strings" "github.com/snapcore/snapd/interfaces" "github.com/snapcore/snapd/interfaces/apparmor" + "github.com/snapcore/snapd/interfaces/mount" "github.com/snapcore/snapd/interfaces/seccomp" "github.com/snapcore/snapd/interfaces/udev" - "github.com/snapcore/snapd/release" + "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/snap" ) @@ -102,6 +104,7 @@ type=stream addr="@/tmp/.X11-unix/X[0-9]*" peer=(label=###PLUG_SECURITY_TAGS###), +# TODO: deprecate and remove this if it doesn't break X11 server snaps. unix (connect, receive, send, accept) type=stream addr="@/tmp/.ICE-unix/[0-9]*" @@ -138,6 +141,14 @@ network netlink raw, /run/udev/data/c13:[0-9]* r, /run/udev/data/+input:* r, + +# Deny access to ICE granted by abstractions/X +# See: https://bugs.launchpad.net/snapd/+bug/1901489 +deny owner @{HOME}/.ICEauthority r, +deny owner /run/user/*/ICEauthority r, +deny unix (connect, receive, send) + type=stream + peer=(addr="@/tmp/.ICE-unix/[0-9]*"), ` const x11ConnectedPlugSecComp = ` @@ -153,8 +164,67 @@ commonInterface } +func (iface *x11Interface) MountConnectedPlug(spec *mount.Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error { + if implicitSystemConnectedSlot(slot) { + // X11 slot is provided by the host system. Bring the host's + // /tmp/.X11-unix/ directory over to the snap mount namespace. + return spec.AddMountEntry(osutil.MountEntry{ + Name: "/var/lib/snapd/hostfs/tmp/.X11-unix", + Dir: "/tmp/.X11-unix", + Options: []string{"bind", "ro"}, + }) + } + + // X11 slot is provided by another snap on the system. Bring that snap's + // /tmp/.X11-unix/ directory over to the snap mount namespace. Here we + // rely on the predictable naming of the private /tmp directory of the + // slot-side snap which is currently provided by snap-confine. + + // But if the same snap is providing both the plug and the slot, this is + // not necessary. + if plug.Snap().InstanceName() == slot.Snap().InstanceName() { + return nil + } + slotSnapName := slot.Snap().InstanceName() + return spec.AddMountEntry(osutil.MountEntry{ + Name: fmt.Sprintf("/var/lib/snapd/hostfs/tmp/snap.%s/tmp/.X11-unix", slotSnapName), + Dir: "/tmp/.X11-unix", + Options: []string{"bind", "ro"}, + }) +} + +func (iface *x11Interface) AppArmorConnectedPlug(spec *apparmor.Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error { + if err := iface.commonInterface.AppArmorConnectedPlug(spec, plug, slot); err != nil { + return err + } + // Consult the comments in MountConnectedPlug for the rationale of the control flow. + if implicitSystemConnectedSlot(slot) { + spec.AddUpdateNS(` + /{,var/lib/snapd/hostfs/}tmp/.X11-unix/ rw, + mount options=(rw, bind) /var/lib/snapd/hostfs/tmp/.X11-unix/ -> /tmp/.X11-unix/, + mount options=(ro, remount, bind) -> /tmp/.X11-unix/, + mount options=(rslave) -> /tmp/.X11-unix/, + umount /tmp/.X11-unix/, + `) + return nil + } + if plug.Snap().InstanceName() == slot.Snap().InstanceName() { + return nil + } + slotSnapName := slot.Snap().InstanceName() + spec.AddUpdateNS(fmt.Sprintf(` + /tmp/.X11-unix/ rw, + /var/lib/snapd/hostfs/tmp/snap.%s/tmp/.X11-unix/ rw, + mount options=(rw, bind) /var/lib/snapd/hostfs/tmp/snap.%s/tmp/.X11-unix/ -> /tmp/.X11-unix/, + mount options=(ro, remount, bind) -> /tmp/.X11-unix/, + mount options=(rslave) -> /tmp/.X11-unix/, + umount /tmp/.X11-unix/, + `, slotSnapName, slotSnapName)) + return nil +} + func (iface *x11Interface) AppArmorConnectedSlot(spec *apparmor.Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error { - if !release.OnClassic { + if !implicitSystemConnectedSlot(slot) { old := "###PLUG_SECURITY_TAGS###" new := plugAppLabelExpr(plug) snippet := strings.Replace(x11ConnectedSlotAppArmor, old, new, -1) @@ -164,21 +234,21 @@ } func (iface *x11Interface) SecCompPermanentSlot(spec *seccomp.Specification, slot *snap.SlotInfo) error { - if !release.OnClassic { + if !implicitSystemPermanentSlot(slot) { spec.AddSnippet(x11PermanentSlotSecComp) } return nil } func (iface *x11Interface) AppArmorPermanentSlot(spec *apparmor.Specification, slot *snap.SlotInfo) error { - if !release.OnClassic { + if !implicitSystemPermanentSlot(slot) { spec.AddSnippet(x11PermanentSlotAppArmor) } return nil } func (iface *x11Interface) UDevPermanentSlot(spec *udev.Specification, slot *snap.SlotInfo) error { - if !release.OnClassic { + if !implicitSystemPermanentSlot(slot) { spec.TriggerSubsystem("input") spec.TagDevice(`KERNEL=="tty[0-9]*"`) spec.TagDevice(`KERNEL=="mice"`) diff -Nru snapd-2.47.1+20.10.1build1/interfaces/builtin/x11_test.go snapd-2.48+21.04/interfaces/builtin/x11_test.go --- snapd-2.47.1+20.10.1build1/interfaces/builtin/x11_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/interfaces/builtin/x11_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -25,8 +25,10 @@ "github.com/snapcore/snapd/interfaces" "github.com/snapcore/snapd/interfaces/apparmor" "github.com/snapcore/snapd/interfaces/builtin" + "github.com/snapcore/snapd/interfaces/mount" "github.com/snapcore/snapd/interfaces/seccomp" "github.com/snapcore/snapd/interfaces/udev" + "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/release" "github.com/snapcore/snapd/snap" "github.com/snapcore/snapd/testutil" @@ -36,6 +38,8 @@ iface interfaces.Interface coreSlotInfo *snap.SlotInfo coreSlot *interfaces.ConnectedSlot + corePlugInfo *snap.PlugInfo + corePlug *interfaces.ConnectedPlug classicSlotInfo *snap.SlotInfo classicSlot *interfaces.ConnectedSlot plugInfo *snap.PlugInfo @@ -57,8 +61,15 @@ const x11CoreYaml = `name: x11 version: 0 apps: - app1: - slots: [x11] + app: + slots: [x11-provider] + plugs: [x11-consumer] +plugs: + x11-consumer: + interface: x11 +slots: + x11-provider: + interface: x11 ` // an x11 slot on the core snap (as automatically added on classic) @@ -72,7 +83,8 @@ func (s *X11InterfaceSuite) SetUpTest(c *C) { s.plug, s.plugInfo = MockConnectedPlug(c, x11MockPlugSnapInfoYaml, nil, "x11") - s.coreSlot, s.coreSlotInfo = MockConnectedSlot(c, x11CoreYaml, nil, "x11") + s.coreSlot, s.coreSlotInfo = MockConnectedSlot(c, x11CoreYaml, nil, "x11-provider") + s.corePlug, s.corePlugInfo = MockConnectedPlug(c, x11CoreYaml, nil, "x11-consumer") s.classicSlot, s.classicSlotInfo = MockConnectedSlot(c, x11ClassicYaml, nil, "x11") } @@ -89,28 +101,80 @@ c.Assert(interfaces.BeforePreparePlug(s.iface, s.plugInfo), IsNil) } +func (s *X11InterfaceSuite) TestMountSpec(c *C) { + // case A: x11 slot is provided by the system + spec := &mount.Specification{} + c.Assert(spec.AddConnectedPlug(s.iface, s.plug, s.classicSlot), IsNil) + c.Assert(spec.MountEntries(), DeepEquals, []osutil.MountEntry{{ + Name: "/var/lib/snapd/hostfs/tmp/.X11-unix", + Dir: "/tmp/.X11-unix", + Options: []string{"bind", "ro"}, + }}) + c.Assert(spec.UserMountEntries(), HasLen, 0) + + // case B: x11 slot is provided by another snap on the system + spec = &mount.Specification{} + c.Assert(spec.AddConnectedPlug(s.iface, s.plug, s.coreSlot), IsNil) + c.Assert(spec.MountEntries(), DeepEquals, []osutil.MountEntry{{ + Name: "/var/lib/snapd/hostfs/tmp/snap.x11/tmp/.X11-unix", + Dir: "/tmp/.X11-unix", + Options: []string{"bind", "ro"}, + }}) + c.Assert(spec.UserMountEntries(), HasLen, 0) + + // case C: x11 slot is both provided and consumed by a snap on the system. + spec = &mount.Specification{} + c.Assert(spec.AddConnectedPlug(s.iface, s.corePlug, s.coreSlot), IsNil) + c.Assert(spec.MountEntries(), HasLen, 0) + c.Assert(spec.UserMountEntries(), HasLen, 0) +} + func (s *X11InterfaceSuite) TestAppArmorSpec(c *C) { - // on a core system with x11 slot coming from a regular app snap. - restore := release.MockOnClassic(false) + // case A: x11 slot is provided by the classic system + restore := release.MockOnClassic(true) defer restore() - // connected plug to core slot + // Plug side connection permissions spec := &apparmor.Specification{} + c.Assert(spec.AddConnectedPlug(s.iface, s.plug, s.classicSlot), IsNil) + c.Assert(spec.SecurityTags(), DeepEquals, []string{"snap.consumer.app"}) + c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, "fontconfig") + c.Assert(spec.UpdateNS(), HasLen, 1) + c.Assert(spec.UpdateNS()[0], testutil.Contains, `mount options=(rw, bind) /var/lib/snapd/hostfs/tmp/.X11-unix/ -> /tmp/.X11-unix/,`) + + // case B: x11 slot is provided by another snap on the system + restore = release.MockOnClassic(false) + defer restore() + + // Plug side connection permissions + spec = &apparmor.Specification{} c.Assert(spec.AddConnectedPlug(s.iface, s.plug, s.coreSlot), IsNil) c.Assert(spec.SecurityTags(), DeepEquals, []string{"snap.consumer.app"}) c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, "fontconfig") + c.Assert(spec.UpdateNS(), HasLen, 1) + c.Assert(spec.UpdateNS()[0], testutil.Contains, `mount options=(rw, bind) /var/lib/snapd/hostfs/tmp/snap.x11/tmp/.X11-unix/ -> /tmp/.X11-unix/,`) - // connected core slot to plug + // Slot side connection permissions spec = &apparmor.Specification{} c.Assert(spec.AddConnectedSlot(s.iface, s.plug, s.coreSlot), IsNil) - c.Assert(spec.SecurityTags(), DeepEquals, []string{"snap.x11.app1"}) - c.Assert(spec.SnippetForTag("snap.x11.app1"), testutil.Contains, `peer=(label="snap.consumer.app"),`) + c.Assert(spec.SecurityTags(), DeepEquals, []string{"snap.x11.app"}) + c.Assert(spec.SnippetForTag("snap.x11.app"), testutil.Contains, `peer=(label="snap.consumer.app"),`) + c.Assert(spec.UpdateNS(), HasLen, 0) - // permanent core slot + // Slot side permantent permissions spec = &apparmor.Specification{} c.Assert(spec.AddPermanentSlot(s.iface, s.coreSlotInfo), IsNil) - c.Assert(spec.SecurityTags(), DeepEquals, []string{"snap.x11.app1"}) - c.Assert(spec.SnippetForTag("snap.x11.app1"), testutil.Contains, "capability sys_tty_config,") + c.Assert(spec.SecurityTags(), DeepEquals, []string{"snap.x11.app"}) + c.Assert(spec.SnippetForTag("snap.x11.app"), testutil.Contains, "capability sys_tty_config,") + c.Assert(spec.UpdateNS(), HasLen, 0) + + // case C: x11 slot is both provided and consumed by a snap on the system. + spec = &apparmor.Specification{} + c.Assert(spec.AddConnectedPlug(s.iface, s.corePlug, s.coreSlot), IsNil) + c.Assert(spec.SecurityTags(), DeepEquals, []string{"snap.x11.app"}) + c.Assert(spec.SnippetForTag("snap.x11.app"), testutil.Contains, "fontconfig") + // Self-connection does not need bind mounts, so no additional permissions are provided to snap-update-ns. + c.Assert(spec.UpdateNS(), HasLen, 0) } func (s *X11InterfaceSuite) TestAppArmorSpecOnClassic(c *C) { @@ -163,8 +227,8 @@ c.Assert(err, IsNil) // both app and x11 have secomp rules set - c.Assert(seccompSpec.SecurityTags(), DeepEquals, []string{"snap.consumer.app", "snap.x11.app1"}) - c.Assert(seccompSpec.SnippetForTag("snap.x11.app1"), testutil.Contains, "listen\n") + c.Assert(seccompSpec.SecurityTags(), DeepEquals, []string{"snap.consumer.app", "snap.x11.app"}) + c.Assert(seccompSpec.SnippetForTag("snap.x11.app"), testutil.Contains, "listen\n") c.Assert(seccompSpec.SnippetForTag("snap.consumer.app"), testutil.Contains, "bind\n") } @@ -177,16 +241,16 @@ c.Assert(spec.AddPermanentSlot(s.iface, s.coreSlotInfo), IsNil) c.Assert(spec.Snippets(), HasLen, 6) c.Assert(spec.Snippets(), testutil.Contains, `# x11 -KERNEL=="event[0-9]*", TAG+="snap_x11_app1"`) +KERNEL=="event[0-9]*", TAG+="snap_x11_app"`) c.Assert(spec.Snippets(), testutil.Contains, `# x11 -KERNEL=="mice", TAG+="snap_x11_app1"`) +KERNEL=="mice", TAG+="snap_x11_app"`) c.Assert(spec.Snippets(), testutil.Contains, `# x11 -KERNEL=="mouse[0-9]*", TAG+="snap_x11_app1"`) +KERNEL=="mouse[0-9]*", TAG+="snap_x11_app"`) c.Assert(spec.Snippets(), testutil.Contains, `# x11 -KERNEL=="ts[0-9]*", TAG+="snap_x11_app1"`) +KERNEL=="ts[0-9]*", TAG+="snap_x11_app"`) c.Assert(spec.Snippets(), testutil.Contains, `# x11 -KERNEL=="tty[0-9]*", TAG+="snap_x11_app1"`) - c.Assert(spec.Snippets(), testutil.Contains, `TAG=="snap_x11_app1", RUN+="/usr/lib/snapd/snap-device-helper $env{ACTION} snap_x11_app1 $devpath $major:$minor"`) +KERNEL=="tty[0-9]*", TAG+="snap_x11_app"`) + c.Assert(spec.Snippets(), testutil.Contains, `TAG=="snap_x11_app", RUN+="/usr/lib/snapd/snap-device-helper $env{ACTION} snap_x11_app $devpath $major:$minor"`) c.Assert(spec.TriggeredSubsystems(), DeepEquals, []string{"input"}) // on a classic system with x11 slot coming from the core snap. @@ -194,7 +258,7 @@ defer restore() spec = &udev.Specification{} - c.Assert(spec.AddPermanentSlot(s.iface, s.coreSlotInfo), IsNil) + c.Assert(spec.AddPermanentSlot(s.iface, s.classicSlotInfo), IsNil) c.Assert(spec.Snippets(), HasLen, 0) c.Assert(spec.TriggeredSubsystems(), IsNil) } diff -Nru snapd-2.47.1+20.10.1build1/interfaces/systemd/backend.go snapd-2.48+21.04/interfaces/systemd/backend.go --- snapd-2.47.1+20.10.1build1/interfaces/systemd/backend.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/interfaces/systemd/backend.go 2020-11-19 16:51:02.000000000 +0000 @@ -78,7 +78,7 @@ if b.preseed { systemd = sysd.NewEmulationMode(dirs.GlobalRootDir) } else { - systemd = sysd.New(dirs.GlobalRootDir, sysd.SystemMode, &dummyReporter{}) + systemd = sysd.New(sysd.SystemMode, &dummyReporter{}) } // We need to be carefully here and stop all removed service units before @@ -113,7 +113,14 @@ // Remove disables, stops and removes systemd services of a given snap. func (b *Backend) Remove(snapName string) error { - systemd := sysd.New(dirs.GlobalRootDir, sysd.SystemMode, &dummyReporter{}) + var systemd sysd.Systemd + if b.preseed { + // removing while preseeding is not a viable scenario, but implemented + // for completness. + systemd = sysd.NewEmulationMode(dirs.GlobalRootDir) + } else { + systemd = sysd.New(sysd.SystemMode, &dummyReporter{}) + } // Remove all the files matching snap glob glob := interfaces.InterfaceServiceName(snapName, "*") _, removed, errEnsure := osutil.EnsureDirState(dirs.SnapServicesDir, glob, nil) diff -Nru snapd-2.47.1+20.10.1build1/interfaces/systemd/backend_test.go snapd-2.48+21.04/interfaces/systemd/backend_test.go --- snapd-2.47.1+20.10.1build1/interfaces/systemd/backend_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/interfaces/systemd/backend_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -92,7 +92,7 @@ // the service was also started (whee) c.Check(sysdLog, DeepEquals, [][]string{ {"daemon-reload"}, - {"--root", dirs.GlobalRootDir, "enable", "snap.samba.interface.foo.service"}, + {"enable", "snap.samba.interface.foo.service"}, {"stop", "snap.samba.interface.foo.service"}, {"show", "--property=ActiveState", "snap.samba.interface.foo.service"}, {"start", "snap.samba.interface.foo.service"}, @@ -113,7 +113,7 @@ c.Check(os.IsNotExist(err), Equals, true) // the service was stopped c.Check(s.systemctlArgs, DeepEquals, [][]string{ - {"systemctl", "--root", dirs.GlobalRootDir, "disable", "snap.samba.interface.foo.service"}, + {"systemctl", "disable", "snap.samba.interface.foo.service"}, {"systemctl", "stop", "snap.samba.interface.foo.service"}, {"systemctl", "show", "--property=ActiveState", "snap.samba.interface.foo.service"}, {"systemctl", "daemon-reload"}, @@ -147,7 +147,7 @@ s.UpdateSnap(c, snapInfo, interfaces.ConfinementOptions{}, ifacetest.SambaYamlV1, 0) // The bar service should have been stopped c.Check(s.systemctlArgs, DeepEquals, [][]string{ - {"systemctl", "--root", dirs.GlobalRootDir, "disable", "snap.samba.interface.bar.service"}, + {"systemctl", "disable", "snap.samba.interface.bar.service"}, {"systemctl", "stop", "snap.samba.interface.bar.service"}, {"systemctl", "show", "--property=ActiveState", "snap.samba.interface.bar.service"}, {"systemctl", "daemon-reload"}, diff -Nru snapd-2.47.1+20.10.1build1/interfaces/udev/spec.go snapd-2.48+21.04/interfaces/udev/spec.go --- snapd-2.47.1+20.10.1build1/interfaces/udev/spec.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/interfaces/udev/spec.go 2020-11-19 16:51:02.000000000 +0000 @@ -24,6 +24,7 @@ "sort" "strings" + "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/interfaces" "github.com/snapcore/snapd/snap" "github.com/snapcore/snapd/strutil" @@ -91,7 +92,8 @@ for _, securityTag := range spec.securityTags { tag := udevTag(securityTag) spec.addEntry(fmt.Sprintf("# %s\n%s, TAG+=\"%s\"", spec.iface, snippet, tag), tag) - spec.addEntry(fmt.Sprintf("TAG==\"%s\", RUN+=\"/usr/lib/snapd/snap-device-helper $env{ACTION} %s $devpath $major:$minor\"", tag, tag), tag) + spec.addEntry(fmt.Sprintf("TAG==\"%s\", RUN+=\"%s/snap-device-helper $env{ACTION} %s $devpath $major:$minor\"", + tag, dirs.DistroLibExecDir, tag), tag) } } diff -Nru snapd-2.47.1+20.10.1build1/interfaces/udev/spec_test.go snapd-2.48+21.04/interfaces/udev/spec_test.go --- snapd-2.47.1+20.10.1build1/interfaces/udev/spec_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/interfaces/udev/spec_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -20,11 +20,15 @@ package udev_test import ( + "fmt" + . "gopkg.in/check.v1" + "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/interfaces" "github.com/snapcore/snapd/interfaces/ifacetest" "github.com/snapcore/snapd/interfaces/udev" + "github.com/snapcore/snapd/release" "github.com/snapcore/snapd/snap" "github.com/snapcore/snapd/snap/snaptest" ) @@ -93,7 +97,7 @@ c.Assert(s.spec.Snippets(), DeepEquals, []string{"foo"}) } -func (s *specSuite) TestTagDevice(c *C) { +func (s *specSuite) testTagDevice(c *C, helperDir string) { // TagDevice acts in the scope of the plug/slot (as appropriate) and // affects all of the apps and hooks related to the given plug or slot // (with the exception that slots cannot have hooks). @@ -120,15 +124,33 @@ kernel="voodoo", TAG+="snap_snap1_foo"`, `# iface-2 kernel="hoodoo", TAG+="snap_snap1_foo"`, - `TAG=="snap_snap1_foo", RUN+="/usr/lib/snapd/snap-device-helper $env{ACTION} snap_snap1_foo $devpath $major:$minor"`, + fmt.Sprintf(`TAG=="snap_snap1_foo", RUN+="%s/snap-device-helper $env{ACTION} snap_snap1_foo $devpath $major:$minor"`, helperDir), `# iface-1 kernel="voodoo", TAG+="snap_snap1_hook_configure"`, `# iface-2 kernel="hoodoo", TAG+="snap_snap1_hook_configure"`, - `TAG=="snap_snap1_hook_configure", RUN+="/usr/lib/snapd/snap-device-helper $env{ACTION} snap_snap1_hook_configure $devpath $major:$minor"`, + fmt.Sprintf(`TAG=="snap_snap1_hook_configure", RUN+="%[1]s/snap-device-helper $env{ACTION} snap_snap1_hook_configure $devpath $major:$minor"`, helperDir), }) } +func (s *specSuite) TestTagDevice(c *C) { + defer func() { dirs.SetRootDir("") }() + restore := release.MockReleaseInfo(&release.OS{ID: "ubuntu"}) + defer restore() + dirs.SetRootDir("") + s.testTagDevice(c, "/usr/lib/snapd") +} + +func (s *specSuite) TestTagDeviceAltLibexecdir(c *C) { + defer func() { dirs.SetRootDir("") }() + restore := release.MockReleaseInfo(&release.OS{ID: "fedora"}) + defer restore() + dirs.SetRootDir("") + // sanity + c.Check(dirs.DistroLibExecDir, Equals, "/usr/libexec/snapd") + s.testTagDevice(c, "/usr/libexec/snapd") +} + // The spec.Specification can be used through the interfaces.Specification interface func (s *specSuite) TestSpecificationIface(c *C) { var r interfaces.Specification = s.spec diff -Nru snapd-2.47.1+20.10.1build1/kernel/kernel.go snapd-2.48+21.04/kernel/kernel.go --- snapd-2.47.1+20.10.1build1/kernel/kernel.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/kernel/kernel.go 2020-11-19 16:51:02.000000000 +0000 @@ -27,13 +27,15 @@ "regexp" "gopkg.in/yaml.v2" - - "github.com/snapcore/snapd/gadget/edition" ) type Asset struct { - Edition edition.Number `yaml:"edition,omitempty"` - Content []string `yaml:"content,omitempty"` + // TODO: we may make this an (optional) map at some point in + // the future to select what things should be updated. + // + // Update set to true indicates that assets shall be updated. + Update bool `yaml:"update,omitempty"` + Content []string `yaml:"content,omitempty"` } type Info struct { diff -Nru snapd-2.47.1+20.10.1build1/kernel/kernel_test.go snapd-2.48+21.04/kernel/kernel_test.go --- snapd-2.47.1+20.10.1build1/kernel/kernel_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/kernel/kernel_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -57,7 +57,7 @@ var mockKernelYaml = []byte(` assets: dtbs: - edition: 1 + update: true content: - dtbs/bcm2711-rpi-4-b.dtb - dtbs/bcm2836-rpi-2-b.dtb @@ -86,7 +86,7 @@ c.Check(ki, DeepEquals, &kernel.Info{ Assets: map[string]*kernel.Asset{ "dtbs": { - Edition: 1, + Update: true, Content: []string{ "dtbs/bcm2711-rpi-4-b.dtb", "dtbs/bcm2836-rpi-2-b.dtb", @@ -128,7 +128,7 @@ c.Check(ki, DeepEquals, &kernel.Info{ Assets: map[string]*kernel.Asset{ "dtbs": { - Edition: 1, + Update: true, Content: []string{ "dtbs/bcm2711-rpi-4-b.dtb", "dtbs/bcm2836-rpi-2-b.dtb", diff -Nru snapd-2.47.1+20.10.1build1/logger/export_test.go snapd-2.48+21.04/logger/export_test.go --- snapd-2.47.1+20.10.1build1/logger/export_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/logger/export_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -35,10 +35,10 @@ return log.log.Flags() } -func MockProcCmdline(new string) (restore func()) { - old := procCmdline - procCmdline = new +func ProcCmdlineMustMock(new bool) (restore func()) { + old := procCmdlineUseDefaultMockInTests + procCmdlineUseDefaultMockInTests = new return func() { - procCmdline = old + procCmdlineUseDefaultMockInTests = old } } diff -Nru snapd-2.47.1+20.10.1build1/logger/logger.go snapd-2.48+21.04/logger/logger.go --- snapd-2.47.1+20.10.1build1/logger/logger.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/logger/logger.go 2020-11-19 16:51:02.000000000 +0000 @@ -23,14 +23,11 @@ "bytes" "fmt" "io" - "io/ioutil" "log" "os" - "strings" "sync" "github.com/snapcore/snapd/osutil" - "github.com/snapcore/snapd/strutil" ) // A Logger is a fairly minimal logging tool. @@ -163,15 +160,17 @@ return err } -var procCmdline = "/proc/cmdline" +// used to force testing of the kernel command line parsing +var procCmdlineUseDefaultMockInTests = true // TODO: consider generalizing this to snapdenv and having it used by // other places that consider SNAPD_DEBUG func debugEnabledOnKernelCmdline() bool { - buf, err := ioutil.ReadFile(procCmdline) - if err != nil { + // if this is called during tests, always ignore it so we don't have to mock + // the /proc/cmdline for every test that ends up using a logger + if osutil.IsTestBinary() && procCmdlineUseDefaultMockInTests { return false } - l := strings.Split(string(buf), " ") - return strutil.ListContains(l, "snapd.debug=1") + m, _ := osutil.KernelCommandLineKeyValues("snapd.debug") + return m["snapd.debug"] == "1" } diff -Nru snapd-2.47.1+20.10.1build1/logger/logger_test.go snapd-2.48+21.04/logger/logger_test.go --- snapd-2.47.1+20.10.1build1/logger/logger_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/logger/logger_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -31,6 +31,7 @@ . "gopkg.in/check.v1" "github.com/snapcore/snapd/logger" + "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/testutil" ) @@ -126,10 +127,17 @@ } func (s *LogSuite) TestIntegrationDebugFromKernelCmdline(c *C) { + // must enable actually checking the command line, because by default the + // logger package will skip checking for the kernel command line parameter + // if it detects it is in a test because otherwise we would have to mock the + // cmdline in many many many more tests that end up using a logger + restore := logger.ProcCmdlineMustMock(false) + defer restore() + mockProcCmdline := filepath.Join(c.MkDir(), "proc-cmdline") - err := ioutil.WriteFile(mockProcCmdline, []byte("console=tty panic=-1 snapd.debug=1"), 0644) + err := ioutil.WriteFile(mockProcCmdline, []byte("console=tty panic=-1 snapd.debug=1\n"), 0644) c.Assert(err, IsNil) - restore := logger.MockProcCmdline(mockProcCmdline) + restore = osutil.MockProcCmdline(mockProcCmdline) defer restore() var buf bytes.Buffer diff -Nru snapd-2.47.1+20.10.1build1/osutil/disks/disks.go snapd-2.48+21.04/osutil/disks/disks.go --- snapd-2.47.1+20.10.1build1/osutil/disks/disks.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/osutil/disks/disks.go 2020-11-19 16:51:02.000000000 +0000 @@ -28,16 +28,15 @@ "os/exec" "path/filepath" "regexp" + "sort" "strconv" "strings" + "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/osutil" ) var ( - // for mocking in tests - devBlockDir = "/sys/dev/block" - // this regexp is for the DM_UUID udev property, or equivalently the dm/uuid // sysfs entry for a luks2 device mapper volume dynamically created by // systemd-cryptsetup when unlocking @@ -233,12 +232,14 @@ // be missing from the initrd previously, and are not // available at all during userspace on UC20 for some reason errFmt := "mountpoint source %s is not a decrypted device: could not read device mapper metadata: %v" - dmUUID, err := ioutil.ReadFile(filepath.Join(devBlockDir, d.Dev(), "dm", "uuid")) + + dmDir := filepath.Join(dirs.SysfsDir, "dev", "block", d.Dev(), "dm") + dmUUID, err := ioutil.ReadFile(filepath.Join(dmDir, "uuid")) if err != nil { return nil, fmt.Errorf(errFmt, partMountPointSource, err) } - dmName, err := ioutil.ReadFile(filepath.Join(devBlockDir, d.Dev(), "dm", "name")) + dmName, err := ioutil.ReadFile(filepath.Join(dmDir, "name")) if err != nil { return nil, fmt.Errorf(errFmt, partMountPointSource, err) } @@ -336,59 +337,88 @@ // if we haven't found the partitions for this disk yet, do that now if d.fsLabelToPartUUID == nil { d.fsLabelToPartUUID = make(map[string]string) - // step 1. find all devices with a matching major number - // step 2. start at the major + minor device for the disk, and iterate over - // all devices that have a partition attribute, starting with the - // device with major same as disk and minor equal to disk minor + 1 - // step 3. if we hit a device that does not have a partition attribute, then - // we hit another disk, and shall stop searching - - // note that this code assumes that all contiguous major / minor devices - // belong to the same physical device, even with MBR and - // logical/extended partition nodes jumping to i.e. /dev/sd*5 - - // start with the minor + 1, since the major + minor of the disk we have - // itself is not a partition - currentMinor := d.minor - for { - currentMinor++ - partMajMin := fmt.Sprintf("%d:%d", d.major, currentMinor) - props, err := udevProperties(filepath.Join("/dev/block", partMajMin)) - if err != nil && strings.Contains(err.Error(), "Unknown device") { - // the device doesn't exist, we hit the end of the disk - break - } else if err != nil { - // some other error trying to get udev properties, we should fail - return "", fmt.Errorf("cannot get udev properties for partition %s: %v", partMajMin, err) + + // step 1. find the devpath for the disk, then glob for matching + // devices using the devname in that sysfs directory + // step 2. iterate over all those devices and save all the ones that are + // partitions using the partition sysfs file + // step 3. for all partition devices found, query udev to get the fs + // label and partition uuid + + udevProps, err := udevProperties(filepath.Join("/dev/block", d.Dev())) + if err != nil { + return "", err + } + + // get the base device name + devName := udevProps["DEVNAME"] + if devName == "" { + return "", fmt.Errorf("cannot get udev properties for device %s, missing udev property \"DEVNAME\"", d.Dev()) + } + // the DEVNAME as returned by udev includes the /dev/mmcblk0 path, we + // just want mmcblk0 for example + devName = filepath.Base(devName) + + // get the device path in sysfs + devPath := udevProps["DEVPATH"] + if devPath == "" { + return "", fmt.Errorf("cannot get udev properties for device %s, missing udev property \"DEVPATH\"", d.Dev()) + } + + // glob for /sys/${devPath}/${devName}* + paths, err := filepath.Glob(filepath.Join(dirs.SysfsDir, devPath, devName+"*")) + if err != nil { + return "", fmt.Errorf("internal error getting udev properties for device %s: %v", err, d.Dev()) + } + + // Glob does not sort, so sort manually to have consistent tests + sort.Strings(paths) + + for _, path := range paths { + // check if this device is a partition - note that the mere + // existence of this file is sufficient to indicate that it is a + // partition, the file is the partition number of the device, it + // will be absent for pseudo sub-devices, such as the + // /dev/mmcblk0boot0 disk device on the dragonboard which exists + // under the /dev/mmcblk0 disk, but is not a partition and is + // instead a proper disk + _, err := ioutil.ReadFile(filepath.Join(path, "partition")) + if err != nil { + continue } - if props["DEVTYPE"] != "partition" { - // we ran into another disk, break out - break + // then the device is a partition, get the udev props for it + partDev := filepath.Base(path) + udevProps, err := udevProperties(partDev) + if err != nil { + continue } - // TODO: maybe save ID_PART_ENTRY_NAME here too, which is the name - // of the partition. this may be useful if this function gets - // used in the gadget update code - fsLabelEnc := props["ID_FS_LABEL_ENC"] + partUUID := udevProps["ID_PART_ENTRY_UUID"] + if partUUID == "" { + return "", fmt.Errorf("cannot get udev properties for device %s (a partition of %s), missing udev property \"ID_PART_ENTRY_UUID\"", partDev, d.Dev()) + } + + fsLabelEnc := udevProps["ID_FS_LABEL_ENC"] if fsLabelEnc == "" { - // this partition does not have a filesystem, and thus doesn't - // have a filesystem label - this is not fatal, i.e. the - // bios-boot partition does not have a filesystem label but it - // is the first structure and so we should just skip it + // it is valid for there to be a partition without a fs + // label - such as the bios-boot partition on amd64 pc + // gadget systems + // in this case just skip this, since we are only matching + // by filesystem labels, obviously we cannot ever match to + // a partition which does not have a filesystem continue } - partuuid := props["ID_PART_ENTRY_UUID"] - if partuuid == "" { - return "", fmt.Errorf("cannot get udev properties for partition %s, missing udev property \"ID_PART_ENTRY_UUID\"", partMajMin) - } + // TODO: maybe save ID_PART_ENTRY_NAME here too, which is the name + // of the partition. this may be useful if this function gets + // used in the gadget update code - // we always overwrite the fsLabelEnc with the last one, this has - // the result that the last partition with a given filesystem label - // will be set/found + // we always overwrite the fsLabelEnc with the last one, this + // has the result that the last partition with a given + // filesystem label will be set/found // this matches what udev does with the symlinks in /dev - d.fsLabelToPartUUID[fsLabelEnc] = partuuid + d.fsLabelToPartUUID[fsLabelEnc] = partUUID } } diff -Nru snapd-2.47.1+20.10.1build1/osutil/disks/disks_test.go snapd-2.48+21.04/osutil/disks/disks_test.go --- snapd-2.47.1+20.10.1build1/osutil/disks/disks_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/osutil/disks/disks_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -35,6 +35,50 @@ "github.com/snapcore/snapd/testutil" ) +var ( + virtioDiskDevPath = "/devices/pci0000:00/0000:00:03.0/virtio1/block/vda/" + + // typical real-world values for tests + diskUdevPropMap = map[string]string{ + "ID_PART_ENTRY_DISK": "42:0", + "DEVNAME": "/dev/vda", + "DEVPATH": virtioDiskDevPath, + } + + // the udev prop for bios-boot has no fs label, which is typical of the + // real bios-boot partition on a amd64 pc gadget system, and so we should + // safely just ignore and skip this partition in the implementation + biotBootUdevPropMap = map[string]string{ + "ID_PART_ENTRY_UUID": "bios-boot-partuuid", + } + + // all the ubuntu- partitions have fs labels + ubuntuSeedUdevPropMap = map[string]string{ + "ID_PART_ENTRY_UUID": "ubuntu-seed-partuuid", + "ID_FS_LABEL_ENC": "ubuntu-seed", + } + ubuntuBootUdevPropMap = map[string]string{ + "ID_PART_ENTRY_UUID": "ubuntu-boot-partuuid", + "ID_FS_LABEL_ENC": "ubuntu-boot", + } + ubuntuDataUdevPropMap = map[string]string{ + "ID_PART_ENTRY_UUID": "ubuntu-data-partuuid", + "ID_FS_LABEL_ENC": "ubuntu-data", + } +) + +func createVirtioDevicesInSysfs(c *C, devsToPartition map[string]bool) { + diskDir := filepath.Join(dirs.SysfsDir, virtioDiskDevPath) + for dev, isPartition := range devsToPartition { + err := os.MkdirAll(filepath.Join(diskDir, dev), 0755) + c.Assert(err, IsNil) + if isPartition { + err = ioutil.WriteFile(filepath.Join(diskDir, dev, "partition"), []byte("1"), 0644) + c.Assert(err, IsNil) + } + } +} + type diskSuite struct { testutil.BaseTest } @@ -104,9 +148,8 @@ "DEVTYPE": "partition", }, nil default: - c.Logf("unexpected udev device properties requested: %s", dev) - c.Fail() - return nil, fmt.Errorf("unexpected udev device") + c.Errorf("unexpected udev device properties requested: %s", dev) + return nil, fmt.Errorf("unexpected udev device: %s", dev) } }) defer restore() @@ -128,10 +171,8 @@ "DEVTYPE": "disk", }, nil default: - c.Logf("unexpected udev device properties requested: %s", dev) - c.Fail() - return nil, fmt.Errorf("unexpected udev device") - + c.Errorf("unexpected udev device properties requested: %s", dev) + return nil, fmt.Errorf("unexpected udev device: %s", dev) } }) defer restore() @@ -140,76 +181,83 @@ opts := &disks.Options{IsDecryptedDevice: true} _, err := disks.DiskFromMountPoint("/run/mnt/point", opts) - c.Assert(err, ErrorMatches, `mountpoint source /dev/mapper/something is not a decrypted device: could not read device mapper metadata: open /sys/dev/block/252:0/dm/uuid: no such file or directory`) + c.Assert(err, ErrorMatches, fmt.Sprintf(`mountpoint source /dev/mapper/something is not a decrypted device: could not read device mapper metadata: open %s/dev/block/252:0/dm/uuid: no such file or directory`, dirs.SysfsDir)) } -func (s *diskSuite) TestDiskFromMountPointHappyNoPartitions(c *C) { - restore := osutil.MockMountInfo(`130 30 42:1 / /run/mnt/point rw,relatime shared:54 - ext4 /dev/vda4 rw +func (s *diskSuite) TestDiskFromMountPointHappySinglePartitionIgnoresNonPartitionsInSysfs(c *C) { + restore := osutil.MockMountInfo(`130 30 47:1 / /run/mnt/point rw,relatime shared:54 - ext4 /dev/vda4 rw `) defer restore() - // mock just the partition's disk major minor in udev, but no actual - // partitions + // mock just the single partition and the disk itself in udev + n := 0 restore = disks.MockUdevPropertiesForDevice(func(dev string) (map[string]string, error) { - switch dev { - case "/dev/block/42:1", "/dev/vda4": + n++ + switch n { + case 1, 4: + c.Assert(dev, Equals, "/dev/vda4") + // this is the partition that was mounted that we initially inspect + // to get the disk + // this is also called again when we call MountPointIsFromDisk to + // verify that the /run/mnt/point is from the same disk return map[string]string{ "ID_PART_ENTRY_DISK": "42:0", }, nil + case 2: + c.Assert(dev, Equals, "/dev/block/42:0") + // this is the disk itself, from ID_PART_ENTRY_DISK above + // note that the major/minor for the disk is not adjacent/related to + // the partition itself + return map[string]string{ + "DEVNAME": "/dev/vda", + "DEVPATH": virtioDiskDevPath, + }, nil + case 3: + c.Assert(dev, Equals, "vda4") + // this is the sysfs entry for the partition of the disk previously + // found under the DEVPATH for /dev/block/42:0 + // this is essentially the same as /dev/block/42:1 in actuality, but + // we search for it differently + return map[string]string{ + "ID_FS_LABEL_ENC": "some-label", + "ID_PART_ENTRY_UUID": "some-uuid", + }, nil default: - c.Logf("unexpected udev device properties requested: %s", dev) - c.Fail() - return nil, fmt.Errorf("unexpected udev device") - + c.Errorf("unexpected udev device properties requested: %s", dev) + return nil, fmt.Errorf("unexpected udev device: %s", dev) } }) defer restore() + // create just the single valid partition in sysfs, and an invalid + // non-partition device that we should ignore + createVirtioDevicesInSysfs(c, map[string]bool{ + "vda4": true, + "vda5": false, + }) + disk, err := disks.DiskFromMountPoint("/run/mnt/point", nil) c.Assert(err, IsNil) c.Assert(disk.Dev(), Equals, "42:0") c.Assert(disk.HasPartitions(), Equals, true) - // trying to search for any labels though will fail - _, err = disk.FindMatchingPartitionUUID("ubuntu-boot") - c.Assert(err, ErrorMatches, "no partitions found for disk 42:0") -} - -func (s *diskSuite) TestDiskFromMountPointHappyOnePartition(c *C) { - restore := osutil.MockMountInfo(`130 30 42:1 / /run/mnt/point rw,relatime shared:54 - ext4 /dev/vda1 rw -`) - defer restore() - - restore = disks.MockUdevPropertiesForDevice(func(dev string) (map[string]string, error) { - switch dev { - case "/dev/block/42:1", "/dev/vda1": - return map[string]string{ - "ID_PART_ENTRY_DISK": "42:0", - "DEVTYPE": "partition", - "ID_FS_LABEL_ENC": "ubuntu-seed", - "ID_PART_ENTRY_UUID": "ubuntu-seed-partuuid", - }, nil - case "/dev/block/42:2": - return nil, fmt.Errorf("Unknown device 42:2") - default: - c.Logf("unexpected udev device properties requested: %s", dev) - c.Fail() - return nil, fmt.Errorf("unexpected udev device") - - } - }) - defer restore() - - d, err := disks.DiskFromMountPoint("/run/mnt/point", nil) + // searching for the single label we have for this partition will succeed + label, err := disk.FindMatchingPartitionUUID("some-label") c.Assert(err, IsNil) - c.Assert(d.Dev(), Equals, "42:0") - c.Assert(d.HasPartitions(), Equals, true) + c.Assert(label, Equals, "some-uuid") - label, err := d.FindMatchingPartitionUUID("ubuntu-seed") + matches, err := disk.MountPointIsFromDisk("/run/mnt/point", nil) c.Assert(err, IsNil) - c.Assert(label, Equals, "ubuntu-seed-partuuid") + c.Assert(matches, Equals, true) + + // trying to search for any other labels though will fail + _, err = disk.FindMatchingPartitionUUID("ubuntu-boot") + c.Assert(err, ErrorMatches, "filesystem label \"ubuntu-boot\" not found") + c.Assert(err, FitsTypeOf, disks.FilesystemLabelNotFoundError{}) + labelNotFoundErr := err.(disks.FilesystemLabelNotFoundError) + c.Assert(labelNotFoundErr.Label, Equals, "ubuntu-boot") } -func (s *diskSuite) TestDiskFromMountPointHappy(c *C) { +func (s *diskSuite) TestDiskFromMountPointHappyRealUdevadm(c *C) { restore := osutil.MockMountInfo(`130 30 42:1 / /run/mnt/point rw,relatime shared:54 - ext4 /dev/vda1 rw `) defer restore() @@ -274,21 +322,14 @@ "DEVTYPE": "disk", }, nil default: - c.Logf("unexpected udev device properties requested: %s", dev) - c.Fail() - return nil, fmt.Errorf("unexpected udev device") - + c.Errorf("unexpected udev device properties requested: %s", dev) + return nil, fmt.Errorf("unexpected udev device: %s", dev) } }) defer restore() - // mock the /sys/dev/block dir - devBlockDir := filepath.Join(dirs.SysfsDir, "dev", "block") - restore = disks.MockDevBlockDir(devBlockDir) - defer restore() - // mock the sysfs dm uuid and name files - dmDir := filepath.Join(devBlockDir, "242:1", "dm") + dmDir := filepath.Join(filepath.Join(dirs.SysfsDir, "dev", "block"), "242:1", "dm") err := os.MkdirAll(dmDir, 0755) c.Assert(err, IsNil) @@ -335,48 +376,85 @@ `) defer restore() + n := 0 restore = disks.MockUdevPropertiesForDevice(func(dev string) (map[string]string, error) { - switch dev { - case "/dev/vda4", "/dev/vda3": - return map[string]string{ - "ID_PART_ENTRY_DISK": "42:0", - }, nil - case "/dev/block/42:1": - return map[string]string{ - // bios-boot does not have a filesystem label, so it shouldn't - // be found, but this is not fatal - "DEVTYPE": "partition", - "ID_PART_ENTRY_UUID": "bios-boot-partuuid", - }, nil - case "/dev/block/42:2": - return map[string]string{ - "DEVTYPE": "partition", - "ID_FS_LABEL_ENC": "ubuntu-seed", - "ID_PART_ENTRY_UUID": "ubuntu-seed-partuuid", - }, nil - case "/dev/block/42:3": - return map[string]string{ - "DEVTYPE": "partition", - "ID_FS_LABEL_ENC": "ubuntu-boot", - "ID_PART_ENTRY_UUID": "ubuntu-boot-partuuid", - }, nil - case "/dev/block/42:4": - return map[string]string{ - "DEVTYPE": "partition", - "ID_FS_LABEL_ENC": "ubuntu-data", - "ID_PART_ENTRY_UUID": "ubuntu-data-partuuid", - }, nil - case "/dev/block/42:5": - return nil, fmt.Errorf("Unknown device 42:5") + n++ + switch n { + case 1: + // first request is to the mount point source + c.Assert(dev, Equals, "/dev/vda4") + return diskUdevPropMap, nil + case 2: + // next request is for the disk itself + c.Assert(dev, Equals, "/dev/block/42:0") + return diskUdevPropMap, nil + case 3: + c.Assert(dev, Equals, "vda1") + // this is the sysfs entry for the first partition of the disk + // previously found under the DEVPATH for /dev/block/42:0 + return biotBootUdevPropMap, nil + case 4: + c.Assert(dev, Equals, "vda2") + // the second partition of the disk from sysfs has a fs label + return ubuntuSeedUdevPropMap, nil + case 5: + c.Assert(dev, Equals, "vda3") + // same for the third partition + return ubuntuBootUdevPropMap, nil + case 6: + c.Assert(dev, Equals, "vda4") + // same for the fourth partition + return ubuntuDataUdevPropMap, nil + case 7: + // next request is for the MountPointIsFromDisk for ubuntu-boot in + // this test + c.Assert(dev, Equals, "/dev/vda3") + return diskUdevPropMap, nil + case 8: + // next request is for the another DiskFromMountPoint build set of methods we + // call in this test + c.Assert(dev, Equals, "/dev/vda3") + return diskUdevPropMap, nil + case 9: + // same as for case 2, the disk itself using the major/minor + c.Assert(dev, Equals, "/dev/block/42:0") + return diskUdevPropMap, nil + case 10: + c.Assert(dev, Equals, "vda1") + // this is the sysfs entry for the first partition of the disk + // previously found under the DEVPATH for /dev/block/42:0 + return biotBootUdevPropMap, nil + case 11: + c.Assert(dev, Equals, "vda2") + // the second partition of the disk from sysfs has a fs label + return ubuntuSeedUdevPropMap, nil + case 12: + c.Assert(dev, Equals, "vda3") + // same for the third partition + return ubuntuBootUdevPropMap, nil + case 13: + c.Assert(dev, Equals, "vda4") + // same for the fourth partition + return ubuntuDataUdevPropMap, nil + case 14: + // next request is for the MountPointIsFromDisk for ubuntu-data + c.Assert(dev, Equals, "/dev/vda4") + return diskUdevPropMap, nil default: - c.Logf("unexpected udev device properties requested: %s", dev) - c.Fail() - return nil, fmt.Errorf("unexpected udev device") - + c.Errorf("unexpected udev device properties requested (request %d): %s", n, dev) + return nil, fmt.Errorf("unexpected udev device (request %d): %s", n, dev) } }) defer restore() + // create all 4 partitions as device nodes in sysfs + createVirtioDevicesInSysfs(c, map[string]bool{ + "vda1": true, + "vda2": true, + "vda3": true, + "vda4": true, + }) + ubuntuDataDisk, err := disks.DiskFromMountPoint("/run/mnt/data", nil) c.Assert(err, IsNil) c.Assert(ubuntuDataDisk, Not(IsNil)) @@ -432,63 +510,92 @@ `) defer restore() + n := 0 restore = disks.MockUdevPropertiesForDevice(func(dev string) (map[string]string, error) { - switch dev { - case "/dev/mapper/ubuntu-data-3776bab4-8bcc-46b7-9da2-6a84ce7f93b4": + n++ + switch n { + case 1: + // first request is to find the disk based on the mapper mount point + c.Assert(dev, Equals, "/dev/mapper/ubuntu-data-3776bab4-8bcc-46b7-9da2-6a84ce7f93b4") + // the mapper device is a disk/volume return map[string]string{ - // the mapper device is a disk/volume "DEVTYPE": "disk", }, nil - case "/dev/vda4", - "/dev/vda3", - "/dev/disk/by-uuid/5a522809-c87e-4dfa-81a8-8dc5667d1304": - return map[string]string{ - "ID_PART_ENTRY_DISK": "42:0", - "DEVTYPE": "partition", - }, nil - case "/dev/block/42:1": - return map[string]string{ - // bios-boot does not have a filesystem label, so it shouldn't - // be found, but this is not fatal - "DEVTYPE": "partition", - "ID_PART_ENTRY_UUID": "bios-boot-partuuid", - }, nil - case "/dev/block/42:2": - return map[string]string{ - "DEVTYPE": "partition", - "ID_FS_LABEL_ENC": "ubuntu-seed", - "ID_PART_ENTRY_UUID": "ubuntu-seed-partuuid", - }, nil - case "/dev/block/42:3": + case 2: + // next we find the physical disk by the dm uuid + c.Assert(dev, Equals, "/dev/disk/by-uuid/5a522809-c87e-4dfa-81a8-8dc5667d1304") + return diskUdevPropMap, nil + case 3: + // then re-find the disk based on it's dev major / minor + c.Assert(dev, Equals, "/dev/block/42:0") + return diskUdevPropMap, nil + case 4: + // next find each partition in turn + c.Assert(dev, Equals, "vda1") + return biotBootUdevPropMap, nil + case 5: + c.Assert(dev, Equals, "vda2") + return ubuntuSeedUdevPropMap, nil + case 6: + c.Assert(dev, Equals, "vda3") + return ubuntuBootUdevPropMap, nil + case 7: + c.Assert(dev, Equals, "vda4") return map[string]string{ - "DEVTYPE": "partition", - "ID_FS_LABEL_ENC": "ubuntu-boot", - "ID_PART_ENTRY_UUID": "ubuntu-boot-partuuid", + "ID_FS_LABEL_ENC": "ubuntu-data-enc", + "ID_PART_ENTRY_UUID": "ubuntu-data-enc-partuuid", }, nil - case "/dev/block/42:4": + case 8: + // next we will find the disk for a different mount point via + // MountPointIsFromDisk for ubuntu-boot + c.Assert(dev, Equals, "/dev/vda3") + return diskUdevPropMap, nil + case 9: + // next we will build up a disk from the ubuntu-boot mount point + c.Assert(dev, Equals, "/dev/vda3") + return diskUdevPropMap, nil + case 10: + // same as step 3 + c.Assert(dev, Equals, "/dev/block/42:0") + return diskUdevPropMap, nil + case 11: + // next find each partition in turn again, same as steps 4-7 + c.Assert(dev, Equals, "vda1") + return biotBootUdevPropMap, nil + case 12: + c.Assert(dev, Equals, "vda2") + return ubuntuSeedUdevPropMap, nil + case 13: + c.Assert(dev, Equals, "vda3") + return ubuntuBootUdevPropMap, nil + case 14: + c.Assert(dev, Equals, "vda4") return map[string]string{ - "DEVTYPE": "partition", "ID_FS_LABEL_ENC": "ubuntu-data-enc", "ID_PART_ENTRY_UUID": "ubuntu-data-enc-partuuid", }, nil - case "/dev/block/42:5": - return nil, fmt.Errorf("Unknown device 42:5") + case 15: + // then we will find the disk for ubuntu-data mapper volume to + // verify it comes from the same disk as the second disk we just + // finished finding + c.Assert(dev, Equals, "/dev/mapper/ubuntu-data-3776bab4-8bcc-46b7-9da2-6a84ce7f93b4") + // the mapper device is a disk/volume + return map[string]string{ + "DEVTYPE": "disk", + }, nil + case 16: + // then we find the physical disk by the dm uuid + c.Assert(dev, Equals, "/dev/disk/by-uuid/5a522809-c87e-4dfa-81a8-8dc5667d1304") + return diskUdevPropMap, nil default: - c.Logf("unexpected udev device properties requested: %s", dev) - c.Fail() - return nil, fmt.Errorf("unexpected udev device") - + c.Errorf("unexpected udev device properties requested (request %d): %s", n, dev) + return nil, fmt.Errorf("unexpected udev device (request %d): %s", n, dev) } }) defer restore() - // mock the /sys/dev/block dir - devBlockDir := filepath.Join(dirs.SysfsDir, "dev", "block") - restore = disks.MockDevBlockDir(devBlockDir) - defer restore() - // mock the sysfs dm uuid and name files - dmDir := filepath.Join(devBlockDir, "252:0", "dm") + dmDir := filepath.Join(filepath.Join(dirs.SysfsDir, "dev", "block"), "252:0", "dm") err := os.MkdirAll(dmDir, 0755) c.Assert(err, IsNil) @@ -500,6 +607,14 @@ err = ioutil.WriteFile(filepath.Join(dmDir, "uuid"), b, 0644) c.Assert(err, IsNil) + // mock the dev nodes in sysfs for the partitions + createVirtioDevicesInSysfs(c, map[string]bool{ + "vda1": true, + "vda2": true, + "vda3": true, + "vda4": true, + }) + opts := &disks.Options{IsDecryptedDevice: true} ubuntuDataDisk, err := disks.DiskFromMountPoint("/run/mnt/data", opts) c.Assert(err, IsNil) diff -Nru snapd-2.47.1+20.10.1build1/osutil/disks/export_test.go snapd-2.48+21.04/osutil/disks/export_test.go --- snapd-2.47.1+20.10.1build1/osutil/disks/export_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/osutil/disks/export_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -41,11 +41,3 @@ udevadmProperties = old } } - -func MockDevBlockDir(new string) (restore func()) { - old := devBlockDir - devBlockDir = new - return func() { - devBlockDir = old - } -} diff -Nru snapd-2.47.1+20.10.1build1/osutil/disks/mockdisk.go snapd-2.48+21.04/osutil/disks/mockdisk.go --- snapd-2.47.1+20.10.1build1/osutil/disks/mockdisk.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/osutil/disks/mockdisk.go 2020-11-19 16:51:02.000000000 +0000 @@ -96,9 +96,23 @@ func MockMountPointDisksToPartitionMapping(mockedMountPoints map[Mountpoint]*MockDiskMapping) (restore func()) { osutil.MustBeTestBinary("mock disks only to be used in tests") - // verify that all unique MockDiskMapping's have unique DevNum's + // verify that all unique MockDiskMapping's have unique DevNum's and that + // the srcMntPt's are all consistent + // we can't have the same mountpoint exist both as a decrypted device and + // not as a decrypted device, this is an impossible mapping, but we need to + // expose functionality to mock the same mountpoint as a decrypted device + // and as an unencrypyted device for different tests, but never at the same + // time with the same mapping alreadySeen := make(map[string]*MockDiskMapping, len(mockedMountPoints)) - for _, mockDisk := range mockedMountPoints { + seenSrcMntPts := make(map[string]bool, len(mockedMountPoints)) + for srcMntPt, mockDisk := range mockedMountPoints { + if decryptedVal, ok := seenSrcMntPts[srcMntPt.Mountpoint]; ok { + if decryptedVal != srcMntPt.IsDecryptedDevice { + msg := fmt.Sprintf("mocked source mountpoint %s is duplicated with different options - previous option for IsDecryptedDevice was %t, current option is %t", srcMntPt.Mountpoint, decryptedVal, srcMntPt.IsDecryptedDevice) + panic(msg) + } + } + seenSrcMntPts[srcMntPt.Mountpoint] = srcMntPt.IsDecryptedDevice if old, ok := alreadySeen[mockDisk.DevNum]; ok { if mockDisk != old { // we already saw a disk with this DevNum as a different pointer diff -Nru snapd-2.47.1+20.10.1build1/osutil/disks/mockdisk_test.go snapd-2.48+21.04/osutil/disks/mockdisk_test.go --- snapd-2.47.1+20.10.1build1/osutil/disks/mockdisk_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/osutil/disks/mockdisk_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -88,6 +88,31 @@ defer r() } +func (s *mockDiskSuite) TestMockMountPointDisksToPartitionMappingVerifiesConsistency(c *C) { + d1 := &disks.MockDiskMapping{ + FilesystemLabelToPartUUID: map[string]string{ + "label1": "part1", + }, + DiskHasPartitions: true, + DevNum: "d1", + } + + // a mountpoint mapping where the same mountpoint has different options for + // the source mountpoint + m := map[disks.Mountpoint]*disks.MockDiskMapping{ + {Mountpoint: "mount1", IsDecryptedDevice: false}: d1, + {Mountpoint: "mount1", IsDecryptedDevice: true}: d1, + } + + // mocking shouldn't work + c.Assert( + func() { disks.MockMountPointDisksToPartitionMapping(m) }, + PanicMatches, + // use .* for true/false since iterating over map order is not defined + `mocked source mountpoint mount1 is duplicated with different options - previous option for IsDecryptedDevice was .*, current option is .*`, + ) +} + func (s *mockDiskSuite) TestMockMountPointDisksToPartitionMapping(c *C) { d1 := &disks.MockDiskMapping{ FilesystemLabelToPartUUID: map[string]string{ diff -Nru snapd-2.47.1+20.10.1build1/osutil/export_test.go snapd-2.48+21.04/osutil/export_test.go --- snapd-2.47.1+20.10.1build1/osutil/export_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/osutil/export_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -179,18 +179,6 @@ return func() { findGidNoGetentFallback = old } } -func MockFindUid(mock func(name string) (uint64, error)) (restore func()) { - old := findUid - findUid = mock - return func() { findUid = old } -} - -func MockFindGid(mock func(name string) (uint64, error)) (restore func()) { - old := findGid - findGid = mock - return func() { findGid = old } -} - const MaxSymlinkTries = maxSymlinkTries var ParseRawEnvironment = parseRawEnvironment diff -Nru snapd-2.47.1+20.10.1build1/osutil/fshelpers_test.go snapd-2.48+21.04/osutil/fshelpers_test.go --- snapd-2.47.1+20.10.1build1/osutil/fshelpers_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/osutil/fshelpers_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -40,7 +40,7 @@ gid, err := FindGidOwning(name) c.Check(err, IsNil) - self, err := RealUser() + self, err := UserMaybeSudoUser() c.Assert(err, IsNil) c.Check(strconv.FormatUint(gid, 10), Equals, self.Gid) } diff -Nru snapd-2.47.1+20.10.1build1/osutil/group.go snapd-2.48+21.04/osutil/group.go --- snapd-2.47.1+20.10.1build1/osutil/group.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/osutil/group.go 2020-11-19 16:51:02.000000000 +0000 @@ -29,15 +29,11 @@ // FindUid returns the identifier of the given UNIX user name. It will // automatically fallback to use "getent" if needed. -func FindUid(username string) (uint64, error) { - return findUid(username) -} +var FindUid = findUid // FindGid returns the identifier of the given UNIX group name. It will // automatically fallback to use "getent" if needed. -func FindGid(groupname string) (uint64, error) { - return findGid(groupname) -} +var FindGid = findGid // getent returns the identifier of the given UNIX user or group name as // determined by the specified database @@ -76,10 +72,43 @@ return strconv.ParseUint(string(parts[2]), 10, 64) } +// TODO: both findUidNoGetentFallback and findGidNoGetentFallback should return +// a more qualified default value than uint64, because currently the +// default return value for findUid is "0" as per Go conventions, which is +// unfortunately also the uid of root, so if a caller ignored the error +// from this function and used that to perform access authorization, then +// the caller would accidentally provide the same access level as root in +// the error case. This is excaberated by the fact that the error case is +// very difficult to positively identify correctly as "not found", see the +// comments inside the functions for more details. +// Note: there is a similar implementation in overlord/snapshotstate which +// should be similarly adjusted when resolving the above TODO. + var findUidNoGetentFallback = func(username string) (uint64, error) { myuser, err := user.Lookup(username) if err != nil { - return 0, err + // Treat all non-nil errors as user.Unknown{User,Group}Error's, as + // currently Go's handling of returned errno from get{pw,gr}nam_r + // in the cgo implementation of user.Lookup is lacking, and thus + // user.Unknown{User,Group}Error is returned only when errno is 0 + // and the list of users/groups is empty, but as per the man page + // for get{pw,gr}nam_r, there are many other errno's that typical + // systems could return to indicate that the user/group wasn't + // found, however unfortunately the POSIX standard does not actually + // dictate what errno should be used to indicate "user/group not + // found", and so even if Go is more robust, it may not ever be + // fully robust. See from the man page: + // + // > It [POSIX.1-2001] does not call "not found" an error, hence + // > does not specify what value errno might have in this situation. + // > But that makes it impossible to recognize errors. + // + // See upstream Go issue: https://github.com/golang/go/issues/40334 + + // if there is a real problem finding the user/group then presumably + // other things will fail upon trying to create the user, etc. which + // will give more useful and specific errors + return 0, user.UnknownUserError(username) } return strconv.ParseUint(myuser.Uid, 10, 64) @@ -88,7 +117,28 @@ var findGidNoGetentFallback = func(groupname string) (uint64, error) { group, err := user.LookupGroup(groupname) if err != nil { - return 0, err + // Treat all non-nil errors as user.Unknown{User,Group}Error's, as + // currently Go's handling of returned errno from get{pw,gr}nam_r + // in the cgo implementation of user.Lookup is lacking, and thus + // user.Unknown{User,Group}Error is returned only when errno is 0 + // and the list of users/groups is empty, but as per the man page + // for get{pw,gr}nam_r, there are many other errno's that typical + // systems could return to indicate that the user/group wasn't + // found, however unfortunately the POSIX standard does not actually + // dictate what errno should be used to indicate "user/group not + // found", and so even if Go is more robust, it may not ever be + // fully robust. See from the man page: + // + // > It [POSIX.1-2001] does not call "not found" an error, hence + // > does not specify what value errno might have in this situation. + // > But that makes it impossible to recognize errors. + // + // See upstream Go issue: https://github.com/golang/go/issues/40334 + + // if there is a real problem finding the user/group then presumably + // other things will fail upon trying to create the user, etc. which + // will give more useful and specific errors + return 0, user.UnknownGroupError(groupname) } return strconv.ParseUint(group.Gid, 10, 64) diff -Nru snapd-2.47.1+20.10.1build1/osutil/io_test.go snapd-2.48+21.04/osutil/io_test.go --- snapd-2.47.1+20.10.1build1/osutil/io_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/osutil/io_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -330,17 +330,19 @@ c.Assert(err, ErrorMatches, `symlink target /.*/nested/bar\..*~: no such file or directory`) checkLeftoverFiles(nestedBarSymlink, nil) - // create a dir without write permission - err = os.MkdirAll(nested, 0644) - c.Assert(err, IsNil) + if os.Geteuid() != 0 { + // create a dir without write permission + err = os.MkdirAll(nested, 0644) + c.Assert(err, IsNil) - // no permission to write in dir - err = osutil.AtomicSymlink("target", nestedBarSymlink) - c.Assert(err, ErrorMatches, `symlink target /.*/nested/bar\..*~: permission denied`) - checkLeftoverFiles(nestedBarSymlink, nil) + // no permission to write in dir + err = osutil.AtomicSymlink("target", nestedBarSymlink) + c.Assert(err, ErrorMatches, `symlink target /.*/nested/bar\..*~: permission denied`) + checkLeftoverFiles(nestedBarSymlink, nil) - err = os.Chmod(nested, 0755) - c.Assert(err, IsNil) + err = os.Chmod(nested, 0755) + c.Assert(err, IsNil) + } err = osutil.AtomicSymlink("target", nestedBarSymlink) c.Assert(err, IsNil) @@ -419,16 +421,18 @@ c.Assert(err, ErrorMatches, "rename /.*/bar /.*/nested/bar: no such file or directory") } - // create a dir without write permission - err = os.MkdirAll(nested, 0644) - c.Assert(err, IsNil) + if os.Geteuid() != 0 { + // create a dir without write permission + err = os.MkdirAll(nested, 0644) + c.Assert(err, IsNil) - // no permission to write in dir - err = osutil.AtomicRename(filepath.Join(d, "bar"), filepath.Join(nested, "bar")) - c.Assert(err, ErrorMatches, "rename /.*/bar /.*/nested/bar: permission denied") + // no permission to write in dir + err = osutil.AtomicRename(filepath.Join(d, "bar"), filepath.Join(nested, "bar")) + c.Assert(err, ErrorMatches, "rename /.*/bar /.*/nested/bar: permission denied") - err = os.Chmod(nested, 0755) - c.Assert(err, IsNil) + err = os.Chmod(nested, 0755) + c.Assert(err, IsNil) + } // all good now err = osutil.AtomicRename(filepath.Join(d, "bar"), filepath.Join(nested, "bar")) diff -Nru snapd-2.47.1+20.10.1build1/osutil/kcmdline.go snapd-2.48+21.04/osutil/kcmdline.go --- snapd-2.47.1+20.10.1build1/osutil/kcmdline.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48+21.04/osutil/kcmdline.go 2020-11-19 16:51:02.000000000 +0000 @@ -0,0 +1,205 @@ +// -*- 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 osutil + +import ( + "bytes" + "fmt" + "io/ioutil" + "strings" +) + +var ( + procCmdline = "/proc/cmdline" +) + +// MockProcCmdline overrides the path to /proc/cmdline. For use in tests. +func MockProcCmdline(newPath string) (restore func()) { + MustBeTestBinary("mocking can only be done from tests") + oldProcCmdline := procCmdline + procCmdline = newPath + return func() { + procCmdline = oldProcCmdline + } +} + +// KernelCommandLineSplit tries to split the string comprising full or a part +// of a kernel command line into a list of individual arguments. Returns an +// error when the input string is incorrectly formatted. +// +// See https://www.kernel.org/doc/html/latest/admin-guide/kernel-parameters.html for details. +func KernelCommandLineSplit(s string) (out []string, err error) { + const ( + argNone int = iota // initial state + argName // looking at argument name + argAssign // looking at = + argValue // looking at unquoted value + argValueQuoteStart // looking at start of quoted value + argValueQuoted // looking at quoted value + argValueQuoteEnd // looking at end of quoted value + ) + var b bytes.Buffer + var rs = []rune(s) + var last = len(rs) - 1 + var errUnexpectedQuote = fmt.Errorf("unexpected quoting") + var errUnbalancedQUote = fmt.Errorf("unbalanced quoting") + var errUnexpectedArgument = fmt.Errorf("unexpected argument") + var errUnexpectedAssignment = fmt.Errorf("unexpected assignment") + // arguments are: + // - arg + // - arg=value, where value can be any string, spaces are preserve when quoting ".." + var state = argNone + for idx, r := range rs { + maybeSplit := false + switch state { + case argNone: + switch r { + case '"': + return nil, errUnexpectedQuote + case '=': + return nil, errUnexpectedAssignment + case ' ': + maybeSplit = true + default: + state = argName + b.WriteRune(r) + } + case argName: + switch r { + case '"': + return nil, errUnexpectedQuote + case ' ': + maybeSplit = true + state = argNone + case '=': + state = argAssign + fallthrough + default: + b.WriteRune(r) + } + case argAssign: + switch r { + case '=': + return nil, errUnexpectedAssignment + case ' ': + // no value: arg= + maybeSplit = true + state = argNone + case '"': + // arg=".. + state = argValueQuoteStart + b.WriteRune(r) + default: + // arg=v.. + state = argValue + b.WriteRune(r) + } + case argValue: + switch r { + case '"': + // arg=foo" + return nil, errUnexpectedQuote + case ' ': + state = argNone + maybeSplit = true + default: + // arg=value... + b.WriteRune(r) + } + case argValueQuoteStart: + switch r { + case '"': + // closing quote: arg="" + state = argValueQuoteEnd + b.WriteRune(r) + default: + state = argValueQuoted + b.WriteRune(r) + } + case argValueQuoted: + switch r { + case '"': + // closing quote: arg="foo" + state = argValueQuoteEnd + fallthrough + default: + b.WriteRune(r) + } + case argValueQuoteEnd: + switch r { + case ' ': + maybeSplit = true + state = argNone + case '"': + // arg="foo"" + return nil, errUnexpectedQuote + case '=': + // arg="foo"= + return nil, errUnexpectedAssignment + default: + // arg="foo"bar + return nil, errUnexpectedArgument + } + } + if maybeSplit || idx == last { + // split now + if b.Len() != 0 { + out = append(out, b.String()) + b.Reset() + } + } + } + switch state { + case argValueQuoteStart, argValueQuoted: + // ended at arg=" or arg="foo + return nil, errUnbalancedQUote + } + return out, nil +} + +// KernelCommandLineKeyValues returns a map of the specified keys to the values +// set for them in the kernel command line (eg. panic=-1). If the value is +// missing from the kernel command line or it has no value (eg. quiet), the key +// is omitted from the returned map. +func KernelCommandLineKeyValues(keys ...string) (map[string]string, error) { + buf, err := ioutil.ReadFile(procCmdline) + if err != nil { + return nil, err + } + params, err := KernelCommandLineSplit(strings.TrimSpace(string(buf))) + if err != nil { + return nil, err + } + + m := make(map[string]string, len(keys)) + + for _, param := range params { + for _, key := range keys { + if strings.HasPrefix(param, fmt.Sprintf("%s=", key)) { + res := strings.SplitN(param, "=", 2) + // we have confirmed key= prefix, thus len(res) + // is always 2 + m[key] = res[1] + break + } + } + } + return m, nil +} diff -Nru snapd-2.47.1+20.10.1build1/osutil/kcmdline_test.go snapd-2.48+21.04/osutil/kcmdline_test.go --- snapd-2.47.1+20.10.1build1/osutil/kcmdline_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48+21.04/osutil/kcmdline_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -0,0 +1,193 @@ +// -*- 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 osutil_test + +import ( + "io/ioutil" + "path/filepath" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/osutil" +) + +type kcmdlineTestSuite struct{} + +var _ = Suite(&kcmdlineTestSuite{}) + +func (s *kcmdlineTestSuite) TestSplitKernelCommandLine(c *C) { + for idx, tc := range []struct { + cmd string + exp []string + errStr string + }{ + {cmd: ``, exp: nil}, + {cmd: `foo bar baz`, exp: []string{"foo", "bar", "baz"}}, + {cmd: `foo=" many spaces " bar`, exp: []string{`foo=" many spaces "`, "bar"}}, + {cmd: `foo="1$2"`, exp: []string{`foo="1$2"`}}, + {cmd: `foo=1$2`, exp: []string{`foo=1$2`}}, + {cmd: `foo= bar`, exp: []string{"foo=", "bar"}}, + {cmd: `foo=""`, exp: []string{`foo=""`}}, + {cmd: ` cpu=1,2,3 mem=0x2000;0x4000:$2 `, exp: []string{"cpu=1,2,3", "mem=0x2000;0x4000:$2"}}, + {cmd: "isolcpus=1,2,10-20,100-2000:2/25", exp: []string{"isolcpus=1,2,10-20,100-2000:2/25"}}, + // something more realistic + { + cmd: `BOOT_IMAGE=/vmlinuz-linux root=/dev/mapper/linux-root rw quiet loglevel=3 rd.udev.log_priority=3 vt.global_cursor_default=0 rd.luks.uuid=1a273f76-3118-434b-8597-a3b12a59e017 rd.luks.uuid=775e4582-33c1-423b-ac19-f734e0d5e21c rd.luks.options=discard,timeout=0 root=/dev/mapper/linux-root apparmor=1 security=apparmor`, + exp: []string{ + "BOOT_IMAGE=/vmlinuz-linux", + "root=/dev/mapper/linux-root", + "rw", "quiet", + "loglevel=3", + "rd.udev.log_priority=3", + "vt.global_cursor_default=0", + "rd.luks.uuid=1a273f76-3118-434b-8597-a3b12a59e017", + "rd.luks.uuid=775e4582-33c1-423b-ac19-f734e0d5e21c", + "rd.luks.options=discard,timeout=0", + "root=/dev/mapper/linux-root", + "apparmor=1", + "security=apparmor", + }, + }, + // this is actually ok, eg. rd.luks.options=discard,timeout=0 + {cmd: `a=b=`, exp: []string{"a=b="}}, + // bad quoting, or otherwise malformed command line + {cmd: `foo="1$2`, errStr: "unbalanced quoting"}, + {cmd: `"foo"`, errStr: "unexpected quoting"}, + {cmd: `foo"foo"`, errStr: "unexpected quoting"}, + {cmd: `foo=foo"`, errStr: "unexpected quoting"}, + {cmd: `foo="a""b"`, errStr: "unexpected quoting"}, + {cmd: `foo="a foo="b`, errStr: "unexpected argument"}, + {cmd: `foo="a"="b"`, errStr: "unexpected assignment"}, + {cmd: `=`, errStr: "unexpected assignment"}, + {cmd: `a =`, errStr: "unexpected assignment"}, + {cmd: `="foo"`, errStr: "unexpected assignment"}, + {cmd: `a==`, errStr: "unexpected assignment"}, + {cmd: `foo ==a`, errStr: "unexpected assignment"}, + } { + c.Logf("%v: cmd: %q", idx, tc.cmd) + out, err := osutil.KernelCommandLineSplit(tc.cmd) + if tc.errStr != "" { + c.Assert(err, ErrorMatches, tc.errStr) + c.Check(out, IsNil) + } else { + c.Assert(err, IsNil) + c.Check(out, DeepEquals, tc.exp) + } + } +} + +func (s *kcmdlineTestSuite) TestGetKernelCommandLineKeyValue(c *C) { + for _, t := range []struct { + cmdline string + keys []string + exp map[string]string + err string + comment string + }{ + { + cmdline: "", + comment: "empty cmdline", + keys: []string{"foo"}, + }, + { + cmdline: "foo", + comment: "cmdline non-key-value", + keys: []string{"foo"}, + }, + { + cmdline: "foo=1", + comment: "key-value pair", + keys: []string{"foo"}, + exp: map[string]string{ + "foo": "1", + }, + }, + { + cmdline: "foo=1 otherfoo=2", + comment: "multiple key-value pairs", + keys: []string{"foo", "otherfoo"}, + exp: map[string]string{ + "foo": "1", + "otherfoo": "2", + }, + }, + { + cmdline: "foo=", + comment: "empty value in key-value pair", + keys: []string{"foo"}, + exp: map[string]string{ + "foo": "", + }, + }, + { + cmdline: "foo=1 foo=2", + comment: "duplicated key-value pair uses last one", + keys: []string{"foo"}, + exp: map[string]string{ + "foo": "2", + }, + }, + { + cmdline: "foo=1 foo foo2=other", + comment: "cmdline key-value pair and non-key-value", + keys: []string{"foo"}, + exp: map[string]string{ + "foo": "1", + }, + }, + { + cmdline: "foo=a=1", + comment: "key-value pair with = in value", + keys: []string{"foo"}, + exp: map[string]string{ + "foo": "a=1", + }, + }, + { + cmdline: "=foo", + comment: "missing key", + keys: []string{"foo"}, + err: "unexpected assignment", + }, + { + cmdline: `"foo`, + comment: "invalid kernel cmdline", + keys: []string{"foo"}, + err: "unexpected quoting", + }, + } { + cmdlineFile := filepath.Join(c.MkDir(), "cmdline") + err := ioutil.WriteFile(cmdlineFile, []byte(t.cmdline), 0644) + c.Assert(err, IsNil) + r := osutil.MockProcCmdline(cmdlineFile) + defer r() + res, err := osutil.KernelCommandLineKeyValues(t.keys...) + if t.err != "" { + c.Assert(err, ErrorMatches, t.err, Commentf(t.comment)) + } else { + c.Assert(err, IsNil) + exp := t.exp + if t.exp == nil { + exp = map[string]string{} + } + c.Assert(res, DeepEquals, exp, Commentf(t.comment)) + } + } +} diff -Nru snapd-2.47.1+20.10.1build1/osutil/mockable.go snapd-2.48+21.04/osutil/mockable.go --- snapd-2.47.1+20.10.1build1/osutil/mockable.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/osutil/mockable.go 2020-11-19 16:51:02.000000000 +0000 @@ -34,6 +34,22 @@ } } +func MockFindUid(f func(string) (uint64, error)) (restore func()) { + old := FindUid + FindUid = f + return func() { + FindUid = old + } +} + +func MockFindGid(f func(string) (uint64, error)) (restore func()) { + old := FindGid + FindGid = f + return func() { + FindGid = old + } +} + var ( mockedMountInfo *string diff -Nru snapd-2.47.1+20.10.1build1/osutil/user.go snapd-2.48+21.04/osutil/user.go --- snapd-2.47.1+20.10.1build1/osutil/user.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/osutil/user.go 2020-11-19 16:51:02.000000000 +0000 @@ -282,12 +282,12 @@ return nil } -// RealUser finds the user behind a sudo invocation when root, if applicable -// and possible. +// UserMaybeSudoUser finds the user behind a sudo invocation when root, if +// applicable and possible. Otherwise the current user is returned. // // Don't check SUDO_USER when not root and simply return the current uid // to properly support sudo'ing from root to a non-root user -func RealUser() (*user.User, error) { +func UserMaybeSudoUser() (*user.User, error) { cur, err := userCurrent() if err != nil { return nil, err @@ -305,7 +305,14 @@ } real, err := user.Lookup(realName) - // can happen when sudo is used to enter a chroot (e.g. pbuilder) + + // Note: comparing err here with UnknownUserError is inherently flawed and + // may end up missing some legitimate unknown user errors, see the comment + // on findGidNoGetentFallback in group.go for more details. + // however in this case the effect is not worrisome, because if we fail to + // identify the error as unknown user, we will just fail here and won't + // inadvertently raise or lower permissions, as the current user is already + // root in this codepath if _, ok := err.(user.UnknownUserError); ok { return cur, nil } diff -Nru snapd-2.47.1+20.10.1build1/osutil/user_test.go snapd-2.48+21.04/osutil/user_test.go --- snapd-2.47.1+20.10.1build1/osutil/user_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/osutil/user_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -191,7 +191,7 @@ c.Assert(err, check.ErrorMatches, `cannot force password change when no password is provided`) } -func (s *createUserSuite) TestRealUser(c *check.C) { +func (s *createUserSuite) TestUserMaybeSudoUser(c *check.C) { oldUser := os.Getenv("SUDO_USER") defer func() { os.Setenv("SUDO_USER", oldUser) }() @@ -217,7 +217,7 @@ defer restore() os.Setenv("SUDO_USER", t.SudoUsername) - cur, err := osutil.RealUser() + cur, err := osutil.UserMaybeSudoUser() c.Assert(err, check.IsNil) c.Check(cur.Username, check.Equals, t.CurrentUsername) } diff -Nru snapd-2.47.1+20.10.1build1/overlord/assertstate/assertstate.go snapd-2.48+21.04/overlord/assertstate/assertstate.go --- snapd-2.47.1+20.10.1build1/overlord/assertstate/assertstate.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/overlord/assertstate/assertstate.go 2020-11-19 16:51:02.000000000 +0000 @@ -29,6 +29,7 @@ "github.com/snapcore/snapd/asserts" "github.com/snapcore/snapd/asserts/snapasserts" "github.com/snapcore/snapd/httputil" + "github.com/snapcore/snapd/logger" "github.com/snapcore/snapd/overlord/snapstate" "github.com/snapcore/snapd/overlord/state" "github.com/snapcore/snapd/release" @@ -60,26 +61,37 @@ if err != nil { return err } - modelAs := deviceCtx.Model() snapStates, err := snapstate.All(s) if err != nil { return nil } + + err = bulkRefreshSnapDeclarations(s, snapStates, userID, deviceCtx) + if err == nil { + // done + return nil + } + if _, ok := err.(*bulkAssertionFallbackError); !ok { + // not an error that indicates the server rejecting/failing + // the bulk request itself + return err + } + logger.Noticef("bulk refresh of snap-declarations failed, falling back to one-by-one assertion fetching: %v", err) + + modelAs := deviceCtx.Model() + fetching := func(f asserts.Fetcher) error { - for _, snapst := range snapStates { - info, err := snapst.CurrentInfo() - if err != nil { - return err - } - if info.SnapID == "" { + for instanceName, snapst := range snapStates { + sideInfo := snapst.CurrentSideInfo() + if sideInfo.SnapID == "" { continue } - if err := snapasserts.FetchSnapDeclaration(f, info.SnapID); err != nil { - if notRetried, ok := err.(*httputil.PerstistentNetworkError); ok { + if err := snapasserts.FetchSnapDeclaration(f, sideInfo.SnapID); err != nil { + if notRetried, ok := err.(*httputil.PersistentNetworkError); ok { return notRetried } - return fmt.Errorf("cannot refresh snap-declaration for %q: %v", info.InstanceName(), err) + return fmt.Errorf("cannot refresh snap-declaration for %q: %v", instanceName, err) } } diff -Nru snapd-2.47.1+20.10.1build1/overlord/assertstate/assertstate_test.go snapd-2.48+21.04/overlord/assertstate/assertstate_test.go --- snapd-2.47.1+20.10.1build1/overlord/assertstate/assertstate_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/overlord/assertstate/assertstate_test.go 2020-11-19 16:51:02.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 @@ -21,10 +21,14 @@ import ( "bytes" + "context" "crypto" + "errors" "fmt" "io/ioutil" "path/filepath" + "sort" + "strings" "testing" "time" @@ -36,6 +40,7 @@ "github.com/snapcore/snapd/asserts/assertstest" "github.com/snapcore/snapd/asserts/sysdb" "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/httputil" "github.com/snapcore/snapd/logger" "github.com/snapcore/snapd/overlord" "github.com/snapcore/snapd/overlord/assertstate" @@ -45,6 +50,7 @@ "github.com/snapcore/snapd/overlord/state" "github.com/snapcore/snapd/snap" "github.com/snapcore/snapd/snap/snaptest" + "github.com/snapcore/snapd/store" "github.com/snapcore/snapd/store/storetest" "github.com/snapcore/snapd/testutil" ) @@ -74,6 +80,11 @@ state *state.State db asserts.RODatabase maxDeclSupportedFormat int + + requestedTypes [][]string + + snapActionErr error + downloadAssertionsErr error } func (sto *fakeStore) pokeStateLock() { @@ -93,6 +104,97 @@ return ref.Resolve(sto.db.Find) } +func (sto *fakeStore) SnapAction(_ context.Context, currentSnaps []*store.CurrentSnap, actions []*store.SnapAction, assertQuery store.AssertionQuery, user *auth.UserState, opts *store.RefreshOptions) ([]store.SnapActionResult, []store.AssertionResult, error) { + sto.pokeStateLock() + + if len(currentSnaps) != 0 || len(actions) != 0 { + panic("only assertion query supported") + } + + toResolve, err := assertQuery.ToResolve() + if err != nil { + return nil, nil, err + } + + if sto.snapActionErr != nil { + return nil, nil, sto.snapActionErr + } + + restore := asserts.MockMaxSupportedFormat(asserts.SnapDeclarationType, sto.maxDeclSupportedFormat) + defer restore() + + reqTypes := make(map[string]bool) + ares := make([]store.AssertionResult, 0, len(toResolve)) + for g, ats := range toResolve { + urls := make([]string, 0, len(ats)) + for _, at := range ats { + reqTypes[at.Ref.Type.Name] = true + a, err := at.Ref.Resolve(sto.db.Find) + if err != nil { + assertQuery.AddError(err, &at.Ref) + continue + } + if a.Revision() > at.Revision { + urls = append(urls, fmt.Sprintf("/assertions/%s", at.Unique())) + } + } + ares = append(ares, store.AssertionResult{ + Grouping: asserts.Grouping(g), + StreamURLs: urls, + }) + } + // behave like the actual SnapAction if there are no results + if len(ares) == 0 { + return nil, ares, &store.SnapActionError{ + NoResults: true, + } + } + + typeNames := make([]string, 0, len(reqTypes)) + for k := range reqTypes { + typeNames = append(typeNames, k) + } + sort.Strings(typeNames) + sto.requestedTypes = append(sto.requestedTypes, typeNames) + + return nil, ares, nil +} + +func (sto *fakeStore) DownloadAssertions(urls []string, b *asserts.Batch, user *auth.UserState) error { + sto.pokeStateLock() + + if sto.downloadAssertionsErr != nil { + return sto.downloadAssertionsErr + } + + resolve := func(ref *asserts.Ref) (asserts.Assertion, error) { + restore := asserts.MockMaxSupportedFormat(asserts.SnapDeclarationType, sto.maxDeclSupportedFormat) + defer restore() + return ref.Resolve(sto.db.Find) + } + + for _, u := range urls { + comps := strings.Split(u, "/") + + if len(comps) < 4 { + return fmt.Errorf("cannot use URL: %s", u) + } + + assertType := asserts.Type(comps[2]) + key := comps[3:] + ref := &asserts.Ref{Type: assertType, PrimaryKey: key} + a, err := resolve(ref) + if err != nil { + return err + } + if err := b.Add(a); err != nil { + return err + } + } + + return nil +} + var ( dev1PrivKey, _ = assertstest.GenerateKey(752) ) @@ -788,6 +890,16 @@ c.Check(err, FitsTypeOf, &snapstate.ChangeConflictError{}) } +func (s *assertMgrSuite) TestRefreshSnapDeclarationsNop(c *C) { + s.state.Lock() + defer s.state.Unlock() + + s.setModel(sysdb.GenericClassicModel()) + + err := assertstate.RefreshSnapDeclarations(s.state, 0) + c.Assert(err, IsNil) +} + func (s *assertMgrSuite) TestRefreshSnapDeclarationsNoStore(c *C) { s.state.Lock() defer s.state.Unlock() @@ -861,7 +973,7 @@ c.Check(a.(*asserts.Account).DisplayName(), Equals, "Dev 1 edited display-name") // change snap decl to something that has a too new format - + s.fakeStore.(*fakeStore).maxDeclSupportedFormat = 999 (func() { restore := asserts.MockMaxSupportedFormat(asserts.SnapDeclarationType, 999) defer restore() @@ -895,6 +1007,68 @@ c.Check(a.(*asserts.SnapDeclaration).Revision(), Equals, 1) } +func (s *assertMgrSuite) TestRefreshSnapDeclarationsChangingKey(c *C) { + s.state.Lock() + defer s.state.Unlock() + + s.setModel(sysdb.GenericClassicModel()) + + snapDeclFoo := s.snapDecl(c, "foo", nil) + + s.stateFromDecl(c, snapDeclFoo, "", snap.R(7)) + + // previous state + err := assertstate.Add(s.state, s.storeSigning.StoreAccountKey("")) + c.Assert(err, IsNil) + err = assertstate.Add(s.state, s.dev1Acct) + c.Assert(err, IsNil) + err = assertstate.Add(s.state, snapDeclFoo) + c.Assert(err, IsNil) + + storePrivKey2, _ := assertstest.GenerateKey(752) + err = s.storeSigning.ImportKey(storePrivKey2) + c.Assert(err, IsNil) + storeKey2 := assertstest.NewAccountKey(s.storeSigning.RootSigning, s.storeSigning.TrustedAccount, map[string]interface{}{ + "name": "store2", + }, storePrivKey2.PublicKey(), "") + err = s.storeSigning.Add(storeKey2) + c.Assert(err, IsNil) + + // one changed assertion signed with different key + headers := map[string]interface{}{ + "series": "16", + "snap-id": "foo-id", + "snap-name": "foo", + "publisher-id": s.dev1Acct.AccountID(), + "timestamp": time.Now().Format(time.RFC3339), + "revision": "1", + } + storeKey2ID := storePrivKey2.PublicKey().ID() + snapDeclFoo1, err := s.storeSigning.Sign(asserts.SnapDeclarationType, headers, nil, storeKey2ID) + c.Assert(err, IsNil) + c.Check(snapDeclFoo1.SignKeyID(), Not(Equals), snapDeclFoo.SignKeyID()) + err = s.storeSigning.Add(snapDeclFoo1) + c.Assert(err, IsNil) + + _, err = storeKey2.Ref().Resolve(assertstate.DB(s.state).Find) + c.Check(asserts.IsNotFound(err), Equals, true) + + err = assertstate.RefreshSnapDeclarations(s.state, 0) + c.Assert(err, IsNil) + + a, err := assertstate.DB(s.state).Find(asserts.SnapDeclarationType, map[string]string{ + "series": "16", + "snap-id": "foo-id", + }) + c.Assert(err, IsNil) + c.Check(a.Revision(), Equals, 1) + c.Check(a.SignKeyID(), Equals, storeKey2ID) + + // key was fetched as well + _, err = storeKey2.Ref().Resolve(assertstate.DB(s.state).Find) + c.Check(err, IsNil) +} + func (s *assertMgrSuite) TestRefreshSnapDeclarationsWithStore(c *C) { s.state.Lock() defer s.state.Unlock() @@ -994,6 +1168,326 @@ c.Check(a.(*asserts.Store).Location(), Equals, "the-cloud") } +func (s *assertMgrSuite) TestRefreshSnapDeclarationsDownloadError(c *C) { + s.state.Lock() + defer s.state.Unlock() + + s.setModel(sysdb.GenericClassicModel()) + + snapDeclFoo := s.snapDecl(c, "foo", nil) + + s.stateFromDecl(c, snapDeclFoo, "", snap.R(7)) + + // previous state + err := assertstate.Add(s.state, s.storeSigning.StoreAccountKey("")) + c.Assert(err, IsNil) + err = assertstate.Add(s.state, s.dev1Acct) + c.Assert(err, IsNil) + err = assertstate.Add(s.state, snapDeclFoo) + c.Assert(err, IsNil) + + // one changed assertion + headers := map[string]interface{}{ + "series": "16", + "snap-id": "foo-id", + "snap-name": "fo-o", + "publisher-id": s.dev1Acct.AccountID(), + "timestamp": time.Now().Format(time.RFC3339), + "revision": "1", + } + snapDeclFoo1, err := s.storeSigning.Sign(asserts.SnapDeclarationType, headers, nil, "") + c.Assert(err, IsNil) + err = s.storeSigning.Add(snapDeclFoo1) + c.Assert(err, IsNil) + + s.fakeStore.(*fakeStore).downloadAssertionsErr = errors.New("download error") + + err = assertstate.RefreshSnapDeclarations(s.state, 0) + c.Assert(err, ErrorMatches, `cannot refresh snap-declarations for snaps: + - foo: download error`) +} + +func (s *assertMgrSuite) TestRefreshSnapDeclarationsPersistentNetworkError(c *C) { + s.state.Lock() + defer s.state.Unlock() + + s.setModel(sysdb.GenericClassicModel()) + + snapDeclFoo := s.snapDecl(c, "foo", nil) + + s.stateFromDecl(c, snapDeclFoo, "", snap.R(7)) + + // previous state + err := assertstate.Add(s.state, s.storeSigning.StoreAccountKey("")) + c.Assert(err, IsNil) + err = assertstate.Add(s.state, s.dev1Acct) + c.Assert(err, IsNil) + err = assertstate.Add(s.state, snapDeclFoo) + c.Assert(err, IsNil) + + // one changed assertion + headers := map[string]interface{}{ + "series": "16", + "snap-id": "foo-id", + "snap-name": "fo-o", + "publisher-id": s.dev1Acct.AccountID(), + "timestamp": time.Now().Format(time.RFC3339), + "revision": "1", + } + snapDeclFoo1, err := s.storeSigning.Sign(asserts.SnapDeclarationType, headers, nil, "") + c.Assert(err, IsNil) + err = s.storeSigning.Add(snapDeclFoo1) + c.Assert(err, IsNil) + + pne := new(httputil.PersistentNetworkError) + s.fakeStore.(*fakeStore).snapActionErr = pne + + err = assertstate.RefreshSnapDeclarations(s.state, 0) + c.Assert(err, Equals, pne) +} + +func (s *assertMgrSuite) TestRefreshSnapDeclarationsNoStoreFallback(c *C) { + // test that if we get a 4xx or 500 error from the store trying bulk + // assertion refresh we fall back to the old logic + s.fakeStore.(*fakeStore).snapActionErr = &store.UnexpectedHTTPStatusError{StatusCode: 400} + + logbuf, restore := logger.MockLogger() + defer restore() + + s.TestRefreshSnapDeclarationsNoStore(c) + + c.Check(logbuf.String(), Matches, "(?m).*bulk refresh of snap-declarations failed, falling back to one-by-one assertion fetching:.*HTTP status code 400.*") +} + +func (s *assertMgrSuite) TestRefreshSnapDeclarationsNoStoreFallbackUnexpectedSnapActionError(c *C) { + // test that if we get an unexpected SnapAction error from the + // store trying bulk assertion refresh we fall back to the old + // logic + s.fakeStore.(*fakeStore).snapActionErr = &store.SnapActionError{ + NoResults: true, + Other: []error{errors.New("unexpected error")}, + } + + logbuf, restore := logger.MockLogger() + defer restore() + + s.TestRefreshSnapDeclarationsNoStore(c) + + c.Check(logbuf.String(), Matches, "(?m).*bulk refresh of snap-declarations failed, falling back to one-by-one assertion fetching:.*unexpected error.*") +} + +func (s *assertMgrSuite) TestRefreshSnapDeclarationsWithStoreFallback(c *C) { + // test that if we get a 4xx or 500 error from the store trying bulk + // assertion refresh we fall back to the old logic + s.fakeStore.(*fakeStore).snapActionErr = &store.UnexpectedHTTPStatusError{StatusCode: 500} + + logbuf, restore := logger.MockLogger() + defer restore() + + s.TestRefreshSnapDeclarationsWithStore(c) + + c.Check(logbuf.String(), Matches, "(?m).*bulk refresh of snap-declarations failed, falling back to one-by-one assertion fetching:.*HTTP status code 500.*") +} + +// the following tests cover what happens when refreshing snap-declarations +// need to support overflowing the chosen asserts.Pool maximum groups + +func (s *assertMgrSuite) testRefreshSnapDeclarationsMany(c *C, n int) error { + // reduce maxGroups to test and stress the logic that deals + // with overflowing it + s.AddCleanup(assertstate.MockMaxGroups(16)) + + // previous state + err := assertstate.Add(s.state, s.storeSigning.StoreAccountKey("")) + c.Assert(err, IsNil) + err = assertstate.Add(s.state, s.dev1Acct) + c.Assert(err, IsNil) + + for i := 1; i <= n; i++ { + name := fmt.Sprintf("foo%d", i) + snapDeclFooX := s.snapDecl(c, name, nil) + + s.stateFromDecl(c, snapDeclFooX, "", snap.R(7+i)) + + // previous state + err = assertstate.Add(s.state, snapDeclFooX) + c.Assert(err, IsNil) + + // make an update on top + headers := map[string]interface{}{ + "series": "16", + "snap-id": name + "-id", + "snap-name": fmt.Sprintf("fo-o-%d", i), + "publisher-id": s.dev1Acct.AccountID(), + "timestamp": time.Now().Format(time.RFC3339), + "revision": "1", + } + snapDeclFooX1, err := s.storeSigning.Sign(asserts.SnapDeclarationType, headers, nil, "") + c.Assert(err, IsNil) + err = s.storeSigning.Add(snapDeclFooX1) + c.Assert(err, IsNil) + } + + err = assertstate.RefreshSnapDeclarations(s.state, 0) + if err != nil { + // fot the caller to check + return err + } + + // check we got the updates + for i := 1; i <= n; i++ { + name := fmt.Sprintf("foo%d", i) + a, err := assertstate.DB(s.state).Find(asserts.SnapDeclarationType, map[string]string{ + "series": "16", + "snap-id": name + "-id", + }) + c.Assert(err, IsNil) + c.Check(a.(*asserts.SnapDeclaration).SnapName(), Equals, fmt.Sprintf("fo-o-%d", i)) + } + + return nil +} + +func (s *assertMgrSuite) TestRefreshSnapDeclarationsMany14NoStore(c *C) { + s.state.Lock() + defer s.state.Unlock() + s.setModel(sysdb.GenericClassicModel()) + + err := s.testRefreshSnapDeclarationsMany(c, 14) + c.Assert(err, IsNil) + + c.Check(s.fakeStore.(*fakeStore).requestedTypes, DeepEquals, [][]string{ + {"account", "account-key", "snap-declaration"}, + }) +} + +func (s *assertMgrSuite) TestRefreshSnapDeclarationsMany16NoStore(c *C) { + s.state.Lock() + defer s.state.Unlock() + s.setModel(sysdb.GenericClassicModel()) + + err := s.testRefreshSnapDeclarationsMany(c, 16) + c.Assert(err, IsNil) + + c.Check(s.fakeStore.(*fakeStore).requestedTypes, DeepEquals, [][]string{ + {"account", "account-key", "snap-declaration"}, + }) +} + +func (s *assertMgrSuite) TestRefreshSnapDeclarationsMany16WithStore(c *C) { + s.state.Lock() + defer s.state.Unlock() + // have a model and the store assertion available + storeAs := s.setupModelAndStore(c) + err := s.storeSigning.Add(storeAs) + c.Assert(err, IsNil) + + err = s.testRefreshSnapDeclarationsMany(c, 16) + c.Assert(err, IsNil) + + c.Check(s.fakeStore.(*fakeStore).requestedTypes, DeepEquals, [][]string{ + // first 16 groups request + {"account", "account-key", "snap-declaration"}, + // final separate request covering store only + {"store"}, + }) + + // store assertion was also fetched + _, err = assertstate.DB(s.state).Find(asserts.StoreType, map[string]string{ + "store": "my-brand-store", + }) + c.Assert(err, IsNil) +} + +func (s *assertMgrSuite) TestRefreshSnapDeclarationsMany17NoStore(c *C) { + s.state.Lock() + defer s.state.Unlock() + s.setModel(sysdb.GenericClassicModel()) + + err := s.testRefreshSnapDeclarationsMany(c, 17) + c.Assert(err, IsNil) + + c.Check(s.fakeStore.(*fakeStore).requestedTypes, DeepEquals, [][]string{ + // first 16 groups request + {"account", "account-key", "snap-declaration"}, + // final separate request for the rest + {"snap-declaration"}, + }) +} + +func (s *assertMgrSuite) TestRefreshSnapDeclarationsMany17NoStoreMergeErrors(c *C) { + s.state.Lock() + defer s.state.Unlock() + s.setModel(sysdb.GenericClassicModel()) + + s.fakeStore.(*fakeStore).downloadAssertionsErr = errors.New("download error") + + err := s.testRefreshSnapDeclarationsMany(c, 17) + c.Check(err, ErrorMatches, `(?s)cannot refresh snap-declarations for snaps: + - foo1: download error.* - foo9: download error`) + // all foo* snaps accounted for + c.Check(strings.Count(err.Error(), "foo"), Equals, 17) + + c.Check(s.fakeStore.(*fakeStore).requestedTypes, DeepEquals, [][]string{ + // first 16 groups request + {"account", "account-key", "snap-declaration"}, + // final separate request for the rest + {"snap-declaration"}, + }) +} + +func (s *assertMgrSuite) TestRefreshSnapDeclarationsMany31WithStore(c *C) { + s.state.Lock() + defer s.state.Unlock() + // have a model and the store assertion available + storeAs := s.setupModelAndStore(c) + err := s.storeSigning.Add(storeAs) + c.Assert(err, IsNil) + + err = s.testRefreshSnapDeclarationsMany(c, 31) + c.Assert(err, IsNil) + + c.Check(s.fakeStore.(*fakeStore).requestedTypes, DeepEquals, [][]string{ + // first 16 groups request + {"account", "account-key", "snap-declaration"}, + // final separate request for the rest and store + {"snap-declaration", "store"}, + }) + + // store assertion was also fetched + _, err = assertstate.DB(s.state).Find(asserts.StoreType, map[string]string{ + "store": "my-brand-store", + }) + c.Assert(err, IsNil) +} + +func (s *assertMgrSuite) TestRefreshSnapDeclarationsMany32WithStore(c *C) { + s.state.Lock() + defer s.state.Unlock() + // have a model and the store assertion available + storeAs := s.setupModelAndStore(c) + err := s.storeSigning.Add(storeAs) + c.Assert(err, IsNil) + + err = s.testRefreshSnapDeclarationsMany(c, 32) + c.Assert(err, IsNil) + + c.Check(s.fakeStore.(*fakeStore).requestedTypes, DeepEquals, [][]string{ + // first 16 groups request + {"account", "account-key", "snap-declaration"}, + // 2nd round request + {"snap-declaration"}, + // final separate request covering store + {"store"}, + }) + + // store assertion was also fetched + _, err = assertstate.DB(s.state).Find(asserts.StoreType, map[string]string{ + "store": "my-brand-store", + }) + c.Assert(err, IsNil) +} + func (s *assertMgrSuite) TestValidateRefreshesNothing(c *C) { s.state.Lock() defer s.state.Unlock() diff -Nru snapd-2.47.1+20.10.1build1/overlord/assertstate/bulk.go snapd-2.48+21.04/overlord/assertstate/bulk.go --- snapd-2.47.1+20.10.1build1/overlord/assertstate/bulk.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48+21.04/overlord/assertstate/bulk.go 2020-11-19 16:51:02.000000000 +0000 @@ -0,0 +1,248 @@ +// -*- 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 assertstate + +import ( + "context" + "fmt" + "sort" + "strings" + + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/overlord/snapstate" + "github.com/snapcore/snapd/overlord/state" + "github.com/snapcore/snapd/release" + "github.com/snapcore/snapd/store" +) + +const storeGroup = "store assertion" + +// maxGroups is the maximum number of assertion groups we set with the +// asserts.Pool used to refresh snap assertions, it corresponds +// roughly to for how many snaps we will request assertions in +// in one /v2/snaps/refresh request. +// Given that requesting assertions for ~500 snaps together with no +// updates can take around 900ms-1s, conservatively set it to half of +// that. Most systems should be done in one request anyway. +var maxGroups = 256 + +func bulkRefreshSnapDeclarations(s *state.State, snapStates map[string]*snapstate.SnapState, userID int, deviceCtx snapstate.DeviceContext) error { + db := cachedDB(s) + + pool := asserts.NewPool(db, maxGroups) + + var mergedRPErr *resolvePoolError + tryResolvePool := func() error { + err := resolvePool(s, pool, userID, deviceCtx) + if rpe, ok := err.(*resolvePoolError); ok { + if mergedRPErr == nil { + mergedRPErr = rpe + } else { + mergedRPErr.merge(rpe) + } + return nil + } + return err + } + + c := 0 + for instanceName, snapst := range snapStates { + sideInfo := snapst.CurrentSideInfo() + if sideInfo.SnapID == "" { + continue + } + + declRef := &asserts.Ref{ + Type: asserts.SnapDeclarationType, + PrimaryKey: []string{release.Series, sideInfo.SnapID}, + } + // update snap-declaration (and prereqs) for the snap, + // they were originally added at install time + if err := pool.AddToUpdate(declRef, instanceName); err != nil { + return fmt.Errorf("cannot prepare snap-declaration refresh for snap %q: %v", instanceName, err) + } + + c++ + if c%maxGroups == 0 { + // we have exhausted max groups, resolve + // what we setup so far and then clear groups + // to reuse the pool + if err := tryResolvePool(); err != nil { + return err + } + if err := pool.ClearGroups(); err != nil { + // this shouldn't happen but if it + // does fallback + return &bulkAssertionFallbackError{err} + } + } + } + + modelAs := deviceCtx.Model() + + // fetch store assertion if available + if modelAs.Store() != "" { + storeRef := asserts.Ref{ + Type: asserts.StoreType, + PrimaryKey: []string{modelAs.Store()}, + } + if err := pool.AddToUpdate(&storeRef, storeGroup); err != nil { + if !asserts.IsNotFound(err) { + return fmt.Errorf("cannot prepare store assertion refresh: %v", err) + } + // assertion is not present in the db yet, + // we'll try to resolve it (fetch it) first + storeAt := &asserts.AtRevision{ + Ref: storeRef, + Revision: asserts.RevisionNotKnown, + } + err := pool.AddUnresolved(storeAt, storeGroup) + if err != nil { + return fmt.Errorf("cannot prepare store assertion fetching: %v", err) + } + } + } + + if err := tryResolvePool(); err != nil { + return err + } + + if mergedRPErr != nil { + if e := mergedRPErr.errors[storeGroup]; asserts.IsNotFound(e) || e == asserts.ErrUnresolved { + // ignore + delete(mergedRPErr.errors, storeGroup) + } + if len(mergedRPErr.errors) == 0 { + return nil + } + mergedRPErr.message = "cannot refresh snap-declarations for snaps" + return mergedRPErr + } + + return nil +} + +// marker error to request falling back to the old implemention for assertion +// refreshes +type bulkAssertionFallbackError struct { + err error +} + +func (e *bulkAssertionFallbackError) Error() string { + return fmt.Sprintf("unsuccessful bulk assertion refresh, fallback: %v", e.err) +} + +type resolvePoolError struct { + message string + // errors maps groups to errors + errors map[string]error +} + +func (rpe *resolvePoolError) merge(rpe1 *resolvePoolError) { + // we expect usually rpe and rpe1 errors to be disjunct, but is also + // ok for rpe1 errors to win + for k, e := range rpe1.errors { + rpe.errors[k] = e + } +} + +func (rpe *resolvePoolError) Error() string { + message := rpe.message + if message == "" { + message = "cannot fetch and resolve assertions" + } + s := make([]string, 0, 1+len(rpe.errors)) + s = append(s, fmt.Sprintf("%s:", message)) + groups := make([]string, 0, len(rpe.errors)) + for g := range rpe.errors { + groups = append(groups, g) + } + sort.Strings(groups) + for _, g := range groups { + s = append(s, fmt.Sprintf(" - %s: %v", g, rpe.errors[g])) + } + return strings.Join(s, "\n") +} + +func resolvePool(s *state.State, pool *asserts.Pool, userID int, deviceCtx snapstate.DeviceContext) error { + user, err := userFromUserID(s, userID) + if err != nil { + return err + } + sto := snapstate.Store(s, deviceCtx) + db := cachedDB(s) + unsupported := handleUnsupported(db) + + for { + // TODO: pass refresh options? + s.Unlock() + _, aresults, err := sto.SnapAction(context.TODO(), nil, nil, pool, user, nil) + s.Lock() + if err != nil { + // request fallback on + // * unexpected SnapActionErrors or + // * unexpected HTTP status of 4xx or 500 + ignore := false + switch stoErr := err.(type) { + case *store.SnapActionError: + if !stoErr.NoResults || len(stoErr.Other) != 0 { + return &bulkAssertionFallbackError{stoErr} + } + // simply no results error, we are likely done + ignore = true + case *store.UnexpectedHTTPStatusError: + if stoErr.StatusCode >= 400 && stoErr.StatusCode <= 500 { + return &bulkAssertionFallbackError{stoErr} + } + } + if !ignore { + return err + } + } + if len(aresults) == 0 { + // everything resolved if no errors + break + } + + for _, ares := range aresults { + b := asserts.NewBatch(unsupported) + s.Unlock() + err := sto.DownloadAssertions(ares.StreamURLs, b, user) + s.Lock() + if err != nil { + pool.AddGroupingError(err, ares.Grouping) + continue + } + _, err = pool.AddBatch(b, ares.Grouping) + if err != nil { + return err + } + } + } + + pool.CommitTo(db) + + errors := pool.Errors() + if len(errors) != 0 { + return &resolvePoolError{errors: errors} + } + + return nil +} diff -Nru snapd-2.47.1+20.10.1build1/overlord/assertstate/export_test.go snapd-2.48+21.04/overlord/assertstate/export_test.go --- snapd-2.47.1+20.10.1build1/overlord/assertstate/export_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/overlord/assertstate/export_test.go 2020-11-19 16:51:02.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 @@ -23,3 +23,11 @@ var ( DoFetch = doFetch ) + +func MockMaxGroups(n int) (restore func()) { + oldMaxGroups := maxGroups + maxGroups = n + return func() { + maxGroups = oldMaxGroups + } +} diff -Nru snapd-2.47.1+20.10.1build1/overlord/assertstate/helpers.go snapd-2.48+21.04/overlord/assertstate/helpers.go --- snapd-2.47.1+20.10.1build1/overlord/assertstate/helpers.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/overlord/assertstate/helpers.go 2020-11-19 16:51:02.000000000 +0000 @@ -35,14 +35,10 @@ return auth.User(st, userID) } -func doFetch(s *state.State, userID int, deviceCtx snapstate.DeviceContext, fetching func(asserts.Fetcher) error) error { - // TODO: once we have a bulk assertion retrieval endpoint this approach will change - - db := cachedDB(s) - - // this is a fallback in case of bugs, we ask the store - // to filter unsupported formats! - unsupported := func(ref *asserts.Ref, unsupportedErr error) error { +// handleUnsupported behaves as a fallback in case of bugs, we do ask +// the store to filter unsupported formats! +func handleUnsupported(db asserts.RODatabase) func(ref *asserts.Ref, unsupportedErr error) error { + return func(ref *asserts.Ref, unsupportedErr error) error { if _, err := ref.Resolve(db.Find); err != nil { // nothing there yet or any other error return unsupportedErr @@ -51,8 +47,14 @@ logger.Noticef("Cannot update assertion %v: %v", ref, unsupportedErr) return nil } +} + +func doFetch(s *state.State, userID int, deviceCtx snapstate.DeviceContext, fetching func(asserts.Fetcher) error) error { + // TODO: once we have a bulk assertion retrieval endpoint this approach will change + + db := cachedDB(s) - b := asserts.NewBatch(unsupported) + b := asserts.NewBatch(handleUnsupported(db)) user, err := userFromUserID(s, userID) if err != nil { diff -Nru snapd-2.47.1+20.10.1build1/overlord/assertstate/validation_set_tracking.go snapd-2.48+21.04/overlord/assertstate/validation_set_tracking.go --- snapd-2.47.1+20.10.1build1/overlord/assertstate/validation_set_tracking.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48+21.04/overlord/assertstate/validation_set_tracking.go 2020-11-19 16:51:02.000000000 +0000 @@ -0,0 +1,127 @@ +// -*- 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 assertstate + +import ( + "encoding/json" + "fmt" + + "github.com/snapcore/snapd/overlord/state" +) + +// ValidationSetMode reflects the mode of respective validation set, which is +// either monitoring or enforcing. +type ValidationSetMode int + +const ( + Monitor ValidationSetMode = iota + Enforce +) + +// ValidationSetTracking holds tracking parameters for associated validation set. +type ValidationSetTracking struct { + AccountID string `json:"account-id"` + Name string `json:"name"` + Mode ValidationSetMode `json:"mode"` + + // PinnedAt is an optional pinned sequence point, or 0 if not pinned. + PinnedAt int `json:"pinned-at,omitempty"` + + // Current is the current sequence point. + Current int `json:"current,omitempty"` +} + +// ValidationSetKey formats the given account id and name into a validation set key. +func ValidationSetKey(accountID, name string) string { + return fmt.Sprintf("%s/%s", accountID, name) +} + +// UpdateValidationSet updates ValidationSetTracking. +// The method assumes valid tr fields. +func UpdateValidationSet(st *state.State, tr *ValidationSetTracking) { + var vsmap map[string]*json.RawMessage + err := st.Get("validation-sets", &vsmap) + if err != nil && err != state.ErrNoState { + panic("internal error: cannot unmarshal validation set tracking state: " + err.Error()) + } + if vsmap == nil { + vsmap = make(map[string]*json.RawMessage) + } + data, err := json.Marshal(tr) + if err != nil { + panic("internal error: cannot marshal validation set tracking state: " + err.Error()) + } + raw := json.RawMessage(data) + key := ValidationSetKey(tr.AccountID, tr.Name) + vsmap[key] = &raw + st.Set("validation-sets", vsmap) +} + +// DeleteValidationSet deletes a validation set for the given accoundID and name. +// It is not an error to delete a non-existing one. +func DeleteValidationSet(st *state.State, accountID, name string) { + var vsmap map[string]*json.RawMessage + err := st.Get("validation-sets", &vsmap) + if err != nil && err != state.ErrNoState { + panic("internal error: cannot unmarshal validation set tracking state: " + err.Error()) + } + if len(vsmap) == 0 { + return + } + delete(vsmap, ValidationSetKey(accountID, name)) + st.Set("validation-sets", vsmap) + return +} + +// GetValidationSet retrieves the ValidationSetTracking for the given account and name. +func GetValidationSet(st *state.State, accountID, name string, tr *ValidationSetTracking) error { + if tr == nil { + return fmt.Errorf("internal error: tr is nil") + } + + *tr = ValidationSetTracking{} + + var vset map[string]*json.RawMessage + err := st.Get("validation-sets", &vset) + if err != nil { + return err + } + key := ValidationSetKey(accountID, name) + raw, ok := vset[key] + if !ok { + return state.ErrNoState + } + // XXX: &tr pointer isn't needed here but it is likely historical (a bug in + // old JSON marshaling probably) and carried over from snapstate.Get. + err = json.Unmarshal([]byte(*raw), &tr) + if err != nil { + return fmt.Errorf("cannot unmarshal validation set tracking state: %v", err) + } + return nil +} + +// ValidationSets retrieves all ValidationSetTracking data. +func ValidationSets(st *state.State) (map[string]*ValidationSetTracking, error) { + var vsmap map[string]*ValidationSetTracking + if err := st.Get("validation-sets", &vsmap); err != nil && err != state.ErrNoState { + return nil, err + } + return vsmap, nil +} diff -Nru snapd-2.47.1+20.10.1build1/overlord/assertstate/validation_set_tracking_test.go snapd-2.48+21.04/overlord/assertstate/validation_set_tracking_test.go --- snapd-2.47.1+20.10.1build1/overlord/assertstate/validation_set_tracking_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48+21.04/overlord/assertstate/validation_set_tracking_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -0,0 +1,159 @@ +// -*- 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 assertstate_test + +import ( + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/overlord/assertstate" + "github.com/snapcore/snapd/overlord/state" +) + +type validationSetTrackingSuite struct { + st *state.State +} + +var _ = Suite(&validationSetTrackingSuite{}) + +func (s *validationSetTrackingSuite) SetUpTest(c *C) { + s.st = state.New(nil) +} + +func (s *validationSetTrackingSuite) TestUpdate(c *C) { + s.st.Lock() + defer s.st.Unlock() + + all, err := assertstate.ValidationSets(s.st) + c.Assert(err, IsNil) + c.Assert(all, HasLen, 0) + + tr := assertstate.ValidationSetTracking{ + AccountID: "foo", + Name: "bar", + Mode: assertstate.Enforce, + PinnedAt: 1, + Current: 2, + } + assertstate.UpdateValidationSet(s.st, &tr) + + all, err = assertstate.ValidationSets(s.st) + c.Assert(err, IsNil) + c.Assert(all, HasLen, 1) + for k, v := range all { + c.Check(k, Equals, "foo/bar") + c.Check(v, DeepEquals, &assertstate.ValidationSetTracking{AccountID: "foo", Name: "bar", Mode: assertstate.Enforce, PinnedAt: 1, Current: 2}) + } + + tr = assertstate.ValidationSetTracking{ + AccountID: "foo", + Name: "bar", + Mode: assertstate.Monitor, + PinnedAt: 2, + Current: 3, + } + assertstate.UpdateValidationSet(s.st, &tr) + + all, err = assertstate.ValidationSets(s.st) + c.Assert(err, IsNil) + c.Assert(all, HasLen, 1) + for k, v := range all { + c.Check(k, Equals, "foo/bar") + c.Check(v, DeepEquals, &assertstate.ValidationSetTracking{AccountID: "foo", Name: "bar", Mode: assertstate.Monitor, PinnedAt: 2, Current: 3}) + } + + tr = assertstate.ValidationSetTracking{ + AccountID: "foo", + Name: "baz", + Mode: assertstate.Enforce, + Current: 3, + } + assertstate.UpdateValidationSet(s.st, &tr) + + all, err = assertstate.ValidationSets(s.st) + c.Assert(err, IsNil) + c.Assert(all, HasLen, 2) + + var gotFirst, gotSecond bool + for k, v := range all { + if k == "foo/bar" { + gotFirst = true + c.Check(v, DeepEquals, &assertstate.ValidationSetTracking{AccountID: "foo", Name: "bar", Mode: assertstate.Monitor, PinnedAt: 2, Current: 3}) + } else { + gotSecond = true + c.Check(k, Equals, "foo/baz") + c.Check(v, DeepEquals, &assertstate.ValidationSetTracking{AccountID: "foo", Name: "baz", Mode: assertstate.Enforce, PinnedAt: 0, Current: 3}) + } + } + c.Check(gotFirst, Equals, true) + c.Check(gotSecond, Equals, true) +} + +func (s *validationSetTrackingSuite) TestDelete(c *C) { + s.st.Lock() + defer s.st.Unlock() + + // delete non-existing one is fine + assertstate.DeleteValidationSet(s.st, "foo", "bar") + all, err := assertstate.ValidationSets(s.st) + c.Assert(err, IsNil) + c.Assert(all, HasLen, 0) + + tr := assertstate.ValidationSetTracking{ + AccountID: "foo", + Name: "bar", + Mode: assertstate.Monitor, + } + assertstate.UpdateValidationSet(s.st, &tr) + + all, err = assertstate.ValidationSets(s.st) + c.Assert(err, IsNil) + c.Assert(all, HasLen, 1) + + // deletes existing one + assertstate.DeleteValidationSet(s.st, "foo", "bar") + all, err = assertstate.ValidationSets(s.st) + c.Assert(err, IsNil) + c.Assert(all, HasLen, 0) +} + +func (s *validationSetTrackingSuite) TestGet(c *C) { + s.st.Lock() + defer s.st.Unlock() + + err := assertstate.GetValidationSet(s.st, "foo", "bar", nil) + c.Assert(err, ErrorMatches, `internal error: tr is nil`) + + tr := assertstate.ValidationSetTracking{ + AccountID: "foo", + Name: "bar", + Mode: assertstate.Enforce, + Current: 3, + } + assertstate.UpdateValidationSet(s.st, &tr) + + var res assertstate.ValidationSetTracking + err = assertstate.GetValidationSet(s.st, "foo", "bar", &res) + c.Assert(err, IsNil) + c.Check(res, DeepEquals, tr) + + // non-existing + err = assertstate.GetValidationSet(s.st, "foo", "baz", &res) + c.Assert(err, Equals, state.ErrNoState) +} diff -Nru snapd-2.47.1+20.10.1build1/overlord/configstate/configcore/backlight.go snapd-2.48+21.04/overlord/configstate/configcore/backlight.go --- snapd-2.47.1+20.10.1build1/overlord/configstate/configcore/backlight.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/overlord/configstate/configcore/backlight.go 2020-11-19 16:51:02.000000000 +0000 @@ -51,7 +51,7 @@ if opts != nil { sysd = systemd.NewEmulationMode(opts.RootDir) } else { - sysd = systemd.New(dirs.GlobalRootDir, systemd.SystemMode, &backlightSysdLogger{}) + sysd = systemd.NewUnderRoot(dirs.GlobalRootDir, systemd.SystemMode, &backlightSysdLogger{}) } output, err := coreCfg(tr, "system.disable-backlight-service") if err != nil { diff -Nru snapd-2.47.1+20.10.1build1/overlord/configstate/configcore/journal.go snapd-2.48+21.04/overlord/configstate/configcore/journal.go --- snapd-2.47.1+20.10.1build1/overlord/configstate/configcore/journal.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/overlord/configstate/configcore/journal.go 2020-11-19 16:51:02.000000000 +0000 @@ -110,7 +110,7 @@ // upstream bug: https://bugs.freedesktop.org/show_bug.cgi?id=84923, // therefore only tell journald to reload if it's new enough. if ver >= 236 { - sysd := systemd.New(dirs.GlobalRootDir, systemd.SystemMode, nil) + sysd := systemd.NewUnderRoot(dirs.GlobalRootDir, systemd.SystemMode, nil) if err := sysd.Kill("systemd-journald", "USR1", ""); err != nil { return err } diff -Nru snapd-2.47.1+20.10.1build1/overlord/configstate/configcore/services.go snapd-2.48+21.04/overlord/configstate/configcore/services.go --- snapd-2.47.1+20.10.1build1/overlord/configstate/configcore/services.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/overlord/configstate/configcore/services.go 2020-11-19 16:51:02.000000000 +0000 @@ -144,7 +144,7 @@ if opts != nil { sysd = systemd.NewEmulationMode(opts.RootDir) } else { - sysd = systemd.New(dirs.GlobalRootDir, systemd.SystemMode, &sysdLogger{}) + sysd = systemd.NewUnderRoot(dirs.GlobalRootDir, systemd.SystemMode, &sysdLogger{}) } // some services are special diff -Nru snapd-2.47.1+20.10.1build1/overlord/configstate/configcore/services_test.go snapd-2.48+21.04/overlord/configstate/configcore/services_test.go --- snapd-2.47.1+20.10.1build1/overlord/configstate/configcore/services_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/overlord/configstate/configcore/services_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -27,8 +27,8 @@ . "gopkg.in/check.v1" - "github.com/snapcore/snapd/boot" "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/overlord/configstate/configcore" "github.com/snapcore/snapd/release" "github.com/snapcore/snapd/snap" @@ -46,6 +46,10 @@ c.Assert(os.MkdirAll(filepath.Join(dirs.GlobalRootDir, "etc"), 0755), IsNil) s.systemctlArgs = nil s.BaseTest.AddCleanup(snap.MockSanitizePlugsSlots(func(snapInfo *snap.Info) {})) + + // mock an empty cmdline since we check the cmdline to check whether we are + // in install mode or not and we don't want to use the host's proc/cmdline + s.AddCleanup(osutil.MockProcCmdline(filepath.Join(c.MkDir(), "proc/cmdline"))) } func (s *servicesSuite) TestConfigureServiceInvalidValue(c *C) { @@ -237,7 +241,7 @@ mockProcCmdline := filepath.Join(c.MkDir(), "cmdline") err := ioutil.WriteFile(mockProcCmdline, []byte("snapd_recovery_mode=install snapd_recovery_system=20201212\n"), 0644) c.Assert(err, IsNil) - restore = boot.MockProcCmdline(mockProcCmdline) + restore = osutil.MockProcCmdline(mockProcCmdline) defer restore() err = configcore.Run(&mockConf{ diff -Nru snapd-2.47.1+20.10.1build1/overlord/configstate/configcore/vitality_test.go snapd-2.48+21.04/overlord/configstate/configcore/vitality_test.go --- snapd-2.47.1+20.10.1build1/overlord/configstate/configcore/vitality_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/overlord/configstate/configcore/vitality_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -98,13 +98,12 @@ }, }) c.Assert(err, IsNil) - rootdir := dirs.GlobalRootDir svcName := "snap.test-snap.foo.service" c.Check(s.systemctlArgs, DeepEquals, [][]string{ - {"--root", rootdir, "is-enabled", "snap.test-snap.foo.service"}, - {"--root", rootdir, "enable", "snap.test-snap.foo.service"}, + {"is-enabled", "snap.test-snap.foo.service"}, + {"enable", "snap.test-snap.foo.service"}, {"daemon-reload"}, - {"--root", rootdir, "is-enabled", "snap.test-snap.foo.service"}, + {"is-enabled", "snap.test-snap.foo.service"}, {"start", "snap.test-snap.foo.service"}, }) svcPath := filepath.Join(dirs.SnapServicesDir, svcName) diff -Nru snapd-2.47.1+20.10.1build1/overlord/configstate/configcore/watchdog.go snapd-2.48+21.04/overlord/configstate/configcore/watchdog.go --- snapd-2.47.1+20.10.1build1/overlord/configstate/configcore/watchdog.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/overlord/configstate/configcore/watchdog.go 2020-11-19 16:51:02.000000000 +0000 @@ -45,7 +45,7 @@ if opts != nil { dir = dirs.SnapSystemdConfDirUnder(opts.RootDir) } else { - sysd = systemd.New(dirs.GlobalRootDir, systemd.SystemMode, &sysdLogger{}) + sysd = systemd.NewUnderRoot(dirs.GlobalRootDir, systemd.SystemMode, &sysdLogger{}) } name := "10-snapd-watchdog.conf" diff -Nru snapd-2.47.1+20.10.1build1/overlord/devicestate/devicemgr.go snapd-2.48+21.04/overlord/devicestate/devicemgr.go --- snapd-2.47.1+20.10.1build1/overlord/devicestate/devicemgr.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/overlord/devicestate/devicemgr.go 2020-11-19 16:51:02.000000000 +0000 @@ -34,6 +34,7 @@ "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/i18n" "github.com/snapcore/snapd/logger" + "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/overlord/assertstate" "github.com/snapcore/snapd/overlord/auth" "github.com/snapcore/snapd/overlord/configstate/config" @@ -42,9 +43,11 @@ "github.com/snapcore/snapd/overlord/snapstate" "github.com/snapcore/snapd/overlord/state" "github.com/snapcore/snapd/overlord/storecontext" + "github.com/snapcore/snapd/progress" "github.com/snapcore/snapd/release" "github.com/snapcore/snapd/snapdenv" "github.com/snapcore/snapd/sysconfig" + "github.com/snapcore/snapd/systemd" "github.com/snapcore/snapd/timings" ) @@ -57,9 +60,16 @@ // policies. type DeviceManager struct { systemMode string + // saveAvailable keeps track whether /var/lib/snapd/save + // is available, i.e. exists and is mounted from ubuntu-save + // if the latter exists. + // TODO: evolve this to state to track things if we start mounting + // save as rw vs ro, or mount/umount it fully on demand + saveAvailable bool - state *state.State - keypairMgr asserts.KeypairManager + state *state.State + + cachedKeypairMgr asserts.KeypairManager // newStore can make new stores for remodeling newStore func(storecontext.DeviceBackend) snapstate.StoreService @@ -87,17 +97,11 @@ func Manager(s *state.State, hookManager *hookstate.HookManager, runner *state.TaskRunner, newStore func(storecontext.DeviceBackend) snapstate.StoreService) (*DeviceManager, error) { delayedCrossMgrInit() - keypairMgr, err := asserts.OpenFSKeypairManager(dirs.SnapDeviceDir) - if err != nil { - return nil, err - } - m := &DeviceManager{ - state: s, - keypairMgr: keypairMgr, - newStore: newStore, - reg: make(chan struct{}), - preseed: snapdenv.Preseeding(), + state: s, + newStore: newStore, + reg: make(chan struct{}), + preseed: snapdenv.Preseeding(), } modeEnv, err := maybeReadModeenv() @@ -138,6 +142,9 @@ runner.AddBlocked(gadgetUpdateBlocked) + // wire FDE kernel hook support into boot + boot.HasFDESetupHook = m.hasFDESetupHook + return m, nil } @@ -149,6 +156,55 @@ return modeEnv, nil } +// StartUp implements StateStarterUp.Startup. +func (m *DeviceManager) StartUp() error { + // system mode is explicitly set on UC20 + // TODO:UC20: ubuntu-save needs to be mounted for recover too + if !release.OnClassic && m.systemMode == "run" { + if err := m.maybeSetupUbuntuSave(); err != nil { + return fmt.Errorf("cannot set up ubuntu-save: %v", err) + } + } + + return nil +} + +func (m *DeviceManager) maybeSetupUbuntuSave() error { + // only called for UC20 + + saveMounted, err := osutil.IsMounted(dirs.SnapSaveDir) + if err != nil { + return err + } + if saveMounted { + logger.Noticef("save already mounted under %v", dirs.SnapSaveDir) + m.saveAvailable = true + return nil + } + + runMntSaveMounted, err := osutil.IsMounted(boot.InitramfsUbuntuSaveDir) + if err != nil { + return err + } + if !runMntSaveMounted { + // we don't have ubuntu-save, save will be used directly + logger.Noticef("no ubuntu-save mount") + m.saveAvailable = true + return nil + } + + logger.Noticef("bind-mounting ubuntu-save under %v", dirs.SnapSaveDir) + + err = systemd.New(systemd.SystemMode, progress.Null).Mount(boot.InitramfsUbuntuSaveDir, + dirs.SnapSaveDir, "-o", "bind") + if err != nil { + logger.Noticef("bind-mounting ubuntu-save failed %v", err) + return fmt.Errorf("cannot bind mount %v under %v: %v", boot.InitramfsUbuntuSaveDir, dirs.SnapSaveDir, err) + } + m.saveAvailable = true + return nil +} + type deviceMgrKey struct{} func deviceMgr(st *state.State) *DeviceManager { @@ -905,6 +961,93 @@ return nil } +var errNoSaveSupport = errors.New("no save directory before UC20") + +// withSaveDir invokes a function making sure save dir is available. +// Under UC16/18 it returns errNoSaveSupport +// For UC20 it also checks that ubuntu-save is available/mounted. +func (m *DeviceManager) withSaveDir(f func() error) error { + // we use the model to check whether this is a UC20 device + model, err := m.Model() + if err == state.ErrNoState { + return fmt.Errorf("internal error: cannot access save dir before a model is set") + } + if err != nil { + return err + } + if model.Grade() == asserts.ModelGradeUnset { + return errNoSaveSupport + } + // at this point we need save available + if !m.saveAvailable { + return fmt.Errorf("internal error: save dir is unavailable") + } + + return f() +} + +// withSaveAssertDB invokes a function making the save device assertion +// backup database available to it. +// Under UC16/18 it returns errNoSaveSupport +// For UC20 it also checks that ubuntu-save is available/mounted. +func (m *DeviceManager) withSaveAssertDB(f func(*asserts.Database) error) error { + return m.withSaveDir(func() error { + // open an ancillary backup assertion database in save/device + assertDB, err := sysdb.OpenAt(dirs.SnapDeviceSaveDir) + if err != nil { + return err + } + return f(assertDB) + }) +} + +// withKeypairMgr invokes a function making the device KeypairManager +// available to it. +// It uses the right location for the manager depending on UC16/18 vs 20, +// the latter uses ubuntu-save. +// For UC20 it also checks that ubuntu-save is available/mounted. +func (m *DeviceManager) withKeypairMgr(f func(asserts.KeypairManager) error) error { + // we use the model to check whether this is a UC20 device + // TODO: during a theoretical UC18->20 remodel the location of + // keypair manager keys would move, we will need dedicated code + // to deal with that, this code typically will return the old location + // until a restart + model, err := m.Model() + if err == state.ErrNoState { + return fmt.Errorf("internal error: cannot access device keypair manager before a model is set") + } + if err != nil { + return err + } + underSave := false + if model.Grade() != asserts.ModelGradeUnset { + // on UC20 the keys are kept under the save dir + underSave = true + } + where := dirs.SnapDeviceDir + if underSave { + // at this point we need save available + if !m.saveAvailable { + return fmt.Errorf("internal error: cannot access device keypair manager if ubuntu-save is unavailable") + } + where = dirs.SnapDeviceSaveDir + } + keypairMgr := m.cachedKeypairMgr + if keypairMgr == nil { + var err error + keypairMgr, err = asserts.OpenFSKeypairManager(where) + if err != nil { + return err + } + m.cachedKeypairMgr = keypairMgr + } + return f(keypairMgr) +} + +// TODO:UC20: we need proper encapsulated support to read +// tpm-policy-auth-key from save if the latter can get unmounted on +// demand + func (m *DeviceManager) keyPair() (asserts.PrivateKey, error) { device, err := m.device() if err != nil { @@ -915,9 +1058,16 @@ return nil, state.ErrNoState } - privKey, err := m.keypairMgr.Get(device.KeyID) + var privKey asserts.PrivateKey + err = m.withKeypairMgr(func(keypairMgr asserts.KeypairManager) (err error) { + privKey, err = keypairMgr.Get(device.KeyID) + if err != nil { + return fmt.Errorf("cannot read device key pair: %v", err) + } + return nil + }) if err != nil { - return nil, fmt.Errorf("cannot read device key pair: %v", err) + return nil, err } return privKey, nil } @@ -1206,3 +1356,19 @@ func (m *DeviceManager) StoreContextBackend() storecontext.Backend { return storeContextBackend{m} } + +func (m *DeviceManager) hasFDESetupHook() (bool, error) { + st := m.state + + deviceCtx, err := DeviceCtx(st, nil, nil) + if err != nil { + return false, fmt.Errorf("cannot get device context: %v", err) + } + + kernelInfo, err := snapstate.KernelInfo(st, deviceCtx) + if err != nil { + return false, fmt.Errorf("cannot get kernel info: %v", err) + } + _, ok := kernelInfo.Hooks["fde-setup"] + return ok, nil +} diff -Nru snapd-2.47.1+20.10.1build1/overlord/devicestate/devicestate_gadget_test.go snapd-2.48+21.04/overlord/devicestate/devicestate_gadget_test.go --- snapd-2.47.1+20.10.1build1/overlord/devicestate/devicestate_gadget_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/overlord/devicestate/devicestate_gadget_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -29,6 +29,8 @@ . "gopkg.in/check.v1" "github.com/snapcore/snapd/boot" + "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/osutil" @@ -80,6 +82,13 @@ size: 50M ` +var uc20gadgetYamlWithSave = uc20gadgetYaml + ` + - name: ubuntu-save + role: system-save + type: 21686148-6449-6E6F-744E-656564454649 + size: 50M +` + func (s *deviceMgrGadgetSuite) setupModelWithGadget(c *C, gadget string) { s.makeModelAssertionInState(c, "canonical", "pc-model", map[string]interface{}{ "architecture": "amd64", @@ -139,9 +148,14 @@ } snaptest.MockSnapWithFiles(c, snapYaml, siCurrent, [][]string{ {"meta/gadget.yaml", gadgetYamlContent}, + {"managed-asset", "managed asset rev 33"}, + {"trusted-asset", "trusted asset rev 33"}, }) snaptest.MockSnapWithFiles(c, snapYaml, si, [][]string{ {"meta/gadget.yaml", gadgetYamlContent}, + {"managed-asset", "managed asset rev 34"}, + // SHA3-384: 88478d8afe6925b348b9cd00085f3535959fde7029a64d7841b031acc39415c690796757afab1852a9e09da913a0151b + {"trusted-asset", "trusted asset rev 34"}, }) s.state.Lock() @@ -171,9 +185,19 @@ return chg, tsk } -func (s *deviceMgrGadgetSuite) testUpdateGadgetOnCoreSimple(c *C, grade string) { +func (s *deviceMgrGadgetSuite) testUpdateGadgetOnCoreSimple(c *C, grade string, encryption bool) { var updateCalled bool var passedRollbackDir string + + if grade != "" { + bootDir := c.MkDir() + tbl := bootloadertest.Mock("trusted", bootDir).WithTrustedAssets() + tbl.TrustedAssetsList = []string{"trusted-asset"} + tbl.ManagedAssetsList = []string{"managed-asset"} + bootloader.Force(tbl) + defer func() { bootloader.Force(nil) }() + } + restore := devicestate.MockGadgetUpdate(func(current, update gadget.GadgetData, path string, policy gadget.UpdatePolicyFunc, observer gadget.ContentUpdateObserver) error { updateCalled = true passedRollbackDir = path @@ -191,6 +215,36 @@ trustedUpdateObserver, ok := observer.(*boot.TrustedAssetsUpdateObserver) c.Assert(ok, Equals, true, Commentf("unexpected type: %T", observer)) c.Assert(trustedUpdateObserver, NotNil) + + // check that observer is behaving correctly with + // respect to trusted and managed assets + targetDir := c.MkDir() + sourceStruct := &gadget.LaidOutStructure{ + VolumeStructure: &gadget.VolumeStructure{ + Role: gadget.SystemSeed, + }, + } + act, err := observer.Observe(gadget.ContentUpdate, sourceStruct, targetDir, "managed-asset", + &gadget.ContentChange{After: filepath.Join(update.RootDir, "managed-asset")}) + c.Assert(err, IsNil) + c.Check(act, Equals, gadget.ChangeIgnore) + act, err = observer.Observe(gadget.ContentUpdate, sourceStruct, targetDir, "trusted-asset", + &gadget.ContentChange{After: filepath.Join(update.RootDir, "trusted-asset")}) + c.Assert(err, IsNil) + c.Check(act, Equals, gadget.ChangeApply) + // check that the behavior is correct + m, err := boot.ReadModeenv("") + c.Assert(err, IsNil) + if encryption { + // with encryption enabled, trusted asset would + // have been picked up by the the observer and + // added to modenv + c.Assert(m.CurrentTrustedRecoveryBootAssets, NotNil) + c.Check(m.CurrentTrustedRecoveryBootAssets["trusted-asset"], DeepEquals, + []string{"88478d8afe6925b348b9cd00085f3535959fde7029a64d7841b031acc39415c690796757afab1852a9e09da913a0151b"}) + } else { + c.Check(m.CurrentTrustedRecoveryBootAssets, HasLen, 0) + } } return nil }) @@ -208,11 +262,13 @@ err := modeenv.WriteTo("") c.Assert(err, IsNil) - // sealed keys stamp - stamp := filepath.Join(dirs.SnapFDEDir, "sealed-keys") - c.Assert(os.MkdirAll(filepath.Dir(stamp), 0755), IsNil) - err = ioutil.WriteFile(stamp, nil, 0644) - c.Assert(err, IsNil) + if encryption { + // sealed keys stamp + stamp := filepath.Join(dirs.SnapFDEDir, "sealed-keys") + c.Assert(os.MkdirAll(filepath.Dir(stamp), 0755), IsNil) + err = ioutil.WriteFile(stamp, nil, 0644) + c.Assert(err, IsNil) + } } devicestate.SetBootOkRan(s.mgr, true) @@ -233,16 +289,22 @@ // should have been removed right after update c.Check(osutil.IsDirectory(rollbackDir), Equals, false) c.Check(s.restartRequests, DeepEquals, []state.RestartType{state.RestartSystem}) - } func (s *deviceMgrGadgetSuite) TestUpdateGadgetOnCoreSimple(c *C) { // unset grade - s.testUpdateGadgetOnCoreSimple(c, "") + encryption := false + s.testUpdateGadgetOnCoreSimple(c, "", encryption) +} + +func (s *deviceMgrGadgetSuite) TestUpdateGadgetOnUC20CoreSimpleWithEncryption(c *C) { + encryption := true + s.testUpdateGadgetOnCoreSimple(c, "dangerous", encryption) } -func (s *deviceMgrGadgetSuite) TestUpdateGadgetOnUC20CoreSimple(c *C) { - s.testUpdateGadgetOnCoreSimple(c, "dangerous") +func (s *deviceMgrGadgetSuite) TestUpdateGadgetOnUC20CoreSimpleNoEncryption(c *C) { + encryption := false + s.testUpdateGadgetOnCoreSimple(c, "dangerous", encryption) } func (s *deviceMgrGadgetSuite) TestUpdateGadgetOnCoreNoUpdateNeeded(c *C) { diff -Nru snapd-2.47.1+20.10.1build1/overlord/devicestate/devicestate_install_mode_test.go snapd-2.48+21.04/overlord/devicestate/devicestate_install_mode_test.go --- snapd-2.47.1+20.10.1build1/overlord/devicestate/devicestate_install_mode_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/overlord/devicestate/devicestate_install_mode_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -30,6 +30,8 @@ "github.com/snapcore/snapd/asserts" "github.com/snapcore/snapd/boot" + "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/gadget/install" @@ -39,6 +41,7 @@ "github.com/snapcore/snapd/overlord/snapstate" "github.com/snapcore/snapd/overlord/state" "github.com/snapcore/snapd/release" + "github.com/snapcore/snapd/secboot" "github.com/snapcore/snapd/snap" "github.com/snapcore/snapd/snap/snaptest" "github.com/snapcore/snapd/sysconfig" @@ -118,7 +121,7 @@ Active: true, }) snaptest.MockSnapWithFiles(c, "name: pc\ntype: gadget", si, [][]string{ - {"meta/gadget.yaml", gadgetYaml + gadgetDefaultsYaml}, + {"meta/gadget.yaml", uc20gadgetYamlWithSave + gadgetDefaultsYaml}, }) si = &snap.SideInfo{ @@ -163,20 +166,30 @@ } type encTestCase struct { - tpm bool - bypass bool - encrypt bool + tpm bool + bypass bool + encrypt bool + trustedBootloader bool } +var ( + dataEncryptionKey = secboot.EncryptionKey{'d', 'a', 't', 'a', 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16} + dataRecoveryKey = secboot.RecoveryKey{'r', 'e', 'c', 'o', 'v', 'e', 'r', 'y', 10, 11, 12, 13, 14, 15, 16, 17} + + saveKey = secboot.EncryptionKey{'s', 'a', 'v', 'e', 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16} + reinstallKey = secboot.RecoveryKey{'r', 'e', 'i', 'n', 's', 't', 'a', 'l', 'l', 11, 12, 13, 14, 15, 16, 17} +) + func (s *deviceMgrInstallModeSuite) doRunChangeTestWithEncryption(c *C, grade string, tc encTestCase) error { restore := release.MockOnClassic(false) defer restore() + bootloaderRootdir := c.MkDir() var brGadgetRoot, brDevice string var brOpts install.Options var installRunCalled int - var sealingObserver gadget.ContentObserver - restore = devicestate.MockInstallRun(func(gadgetRoot, device string, options install.Options, obs install.SystemInstallObserver) error { + var installSealingObserver gadget.ContentObserver + restore = devicestate.MockInstallRun(func(gadgetRoot, device string, options install.Options, obs gadget.ContentObserver) (*install.InstalledSystemSideData, error) { // ensure we can grab the lock here, i.e. that it's not taken s.state.Lock() s.state.Unlock() @@ -184,9 +197,24 @@ brGadgetRoot = gadgetRoot brDevice = device brOpts = options - sealingObserver = obs + installSealingObserver = obs installRunCalled++ - return nil + var keysForRoles map[string]*install.EncryptionKeySet + if tc.encrypt { + keysForRoles = map[string]*install.EncryptionKeySet{ + gadget.SystemData: { + Key: dataEncryptionKey, + RecoveryKey: dataRecoveryKey, + }, + gadget.SystemSave: { + Key: saveKey, + RecoveryKey: reinstallKey, + }, + } + } + return &install.InstalledSystemSideData{ + KeysForRoles: keysForRoles, + }, nil }) defer restore() @@ -199,6 +227,18 @@ }) defer restore() + if tc.trustedBootloader { + tab := bootloadertest.Mock("trusted", bootloaderRootdir).WithTrustedAssets() + tab.TrustedAssetsList = []string{"trusted-asset"} + bootloader.Force(tab) + s.AddCleanup(func() { bootloader.Force(nil) }) + + err := os.MkdirAll(boot.InitramfsUbuntuSeedDir, 0755) + c.Assert(err, IsNil) + err = ioutil.WriteFile(filepath.Join(boot.InitramfsUbuntuSeedDir, "trusted-asset"), nil, 0644) + c.Assert(err, IsNil) + } + s.state.Lock() mockModel := s.makeMockInstalledPcGadget(c, grade, "") s.state.Unlock() @@ -224,6 +264,8 @@ c.Check(bootWith.UnpackedGadgetDir, Equals, filepath.Join(dirs.SnapMountDir, "pc/1")) if tc.encrypt { c.Check(seal, NotNil) + } else { + c.Check(seal, IsNil) } bootMakeBootableCalled++ return nil @@ -258,27 +300,29 @@ c.Assert(installSystem.Status(), Equals, state.DoneStatus) // in the right way + c.Assert(brGadgetRoot, Equals, filepath.Join(dirs.SnapMountDir, "/pc/1")) + c.Assert(brDevice, Equals, "") if tc.encrypt { - c.Assert(brGadgetRoot, Equals, filepath.Join(dirs.SnapMountDir, "/pc/1")) - c.Assert(brDevice, Equals, "") c.Assert(brOpts, DeepEquals, install.Options{ Mount: true, Encrypt: true, }) - - // inteface is not nil - c.Assert(sealingObserver, NotNil) - // we expect a very specific type - trustedInstallObserver, ok := sealingObserver.(*boot.TrustedAssetsInstallObserver) - c.Assert(ok, Equals, true, Commentf("unexpected type: %T", sealingObserver)) - c.Assert(trustedInstallObserver, NotNil) } else { - c.Assert(brGadgetRoot, Equals, filepath.Join(dirs.SnapMountDir, "/pc/1")) - c.Assert(brDevice, Equals, "") c.Assert(brOpts, DeepEquals, install.Options{ Mount: true, }) } + if tc.encrypt { + // inteface is not nil + c.Assert(installSealingObserver, NotNil) + // we expect a very specific type + trustedInstallObserver, ok := installSealingObserver.(*boot.TrustedAssetsInstallObserver) + c.Assert(ok, Equals, true, Commentf("unexpected type: %T", installSealingObserver)) + c.Assert(trustedInstallObserver, NotNil) + } else { + c.Assert(installSealingObserver, IsNil) + } + c.Assert(installRunCalled, Equals, 1) c.Assert(bootMakeBootableCalled, Equals, 1) c.Assert(s.restartRequests, DeepEquals, []state.RestartType{state.RestartSystemNow}) @@ -290,8 +334,8 @@ restore := release.MockOnClassic(false) defer restore() - restore = devicestate.MockInstallRun(func(gadgetRoot, device string, options install.Options, _ install.SystemInstallObserver) error { - return fmt.Errorf("The horror, The horror") + restore = devicestate.MockInstallRun(func(gadgetRoot, device string, options install.Options, _ gadget.ContentObserver) (*install.InstalledSystemSideData, error) { + return nil, fmt.Errorf("The horror, The horror") }) defer restore() @@ -311,7 +355,7 @@ installSystem := s.findInstallSystem() c.Check(installSystem.Err(), ErrorMatches, `(?ms)cannot perform the following tasks: -- Setup system for run mode \(cannot create partitions: The horror, The horror\)`) +- Setup system for run mode \(cannot install system: The horror, The horror\)`) // no restart request on failure c.Check(s.restartRequests, HasLen, 0) } @@ -358,8 +402,11 @@ } func (s *deviceMgrInstallModeSuite) TestInstallDangerousWithTPM(c *C) { - err := s.doRunChangeTestWithEncryption(c, "dangerous", encTestCase{tpm: true, bypass: false, encrypt: true}) + err := s.doRunChangeTestWithEncryption(c, "dangerous", encTestCase{ + tpm: true, bypass: false, encrypt: true, trustedBootloader: true, + }) c.Assert(err, IsNil) + c.Check(filepath.Join(boot.InstallHostFDEDataDir, "recovery.key"), testutil.FileEquals, dataRecoveryKey[:]) } func (s *deviceMgrInstallModeSuite) TestInstallDangerousBypassEncryption(c *C) { @@ -378,8 +425,11 @@ } func (s *deviceMgrInstallModeSuite) TestInstallSignedWithTPM(c *C) { - err := s.doRunChangeTestWithEncryption(c, "signed", encTestCase{tpm: true, bypass: false, encrypt: true}) + err := s.doRunChangeTestWithEncryption(c, "signed", encTestCase{ + tpm: true, bypass: false, encrypt: true, trustedBootloader: true, + }) c.Assert(err, IsNil) + c.Check(filepath.Join(boot.InstallHostFDEDataDir, "recovery.key"), testutil.FileEquals, dataRecoveryKey[:]) } func (s *deviceMgrInstallModeSuite) TestInstallSignedBypassEncryption(c *C) { @@ -393,8 +443,40 @@ } func (s *deviceMgrInstallModeSuite) TestInstallSecuredWithTPM(c *C) { - err := s.doRunChangeTestWithEncryption(c, "secured", encTestCase{tpm: true, bypass: false, encrypt: true}) + err := s.doRunChangeTestWithEncryption(c, "secured", encTestCase{ + tpm: true, bypass: false, encrypt: true, trustedBootloader: true, + }) c.Assert(err, IsNil) + c.Check(filepath.Join(boot.InstallHostFDEDataDir, "recovery.key"), testutil.FileEquals, dataRecoveryKey[:]) +} + +func (s *deviceMgrInstallModeSuite) TestInstallDangerousEncryptionWithTPMNoTrustedAssets(c *C) { + err := s.doRunChangeTestWithEncryption(c, "dangerous", encTestCase{ + tpm: true, bypass: false, encrypt: true, trustedBootloader: false, + }) + c.Assert(err, IsNil) + c.Check(filepath.Join(boot.InstallHostFDEDataDir, "recovery.key"), testutil.FileEquals, dataRecoveryKey[:]) +} + +func (s *deviceMgrInstallModeSuite) TestInstallDangerousNoEncryptionWithTrustedAssets(c *C) { + err := s.doRunChangeTestWithEncryption(c, "dangerous", encTestCase{ + tpm: false, bypass: false, encrypt: false, trustedBootloader: true, + }) + c.Assert(err, IsNil) +} + +func (s *deviceMgrInstallModeSuite) TestInstallSecuredWithTPMAndSave(c *C) { + err := s.doRunChangeTestWithEncryption(c, "secured", encTestCase{ + tpm: true, bypass: false, encrypt: true, trustedBootloader: true, + }) + c.Assert(err, IsNil) + c.Check(filepath.Join(boot.InstallHostFDEDataDir, "recovery.key"), testutil.FileEquals, dataRecoveryKey[:]) + c.Check(filepath.Join(boot.InstallHostFDEDataDir, "ubuntu-save.key"), testutil.FileEquals, saveKey[:]) + c.Check(filepath.Join(boot.InstallHostFDEDataDir, "reinstall.key"), testutil.FileEquals, reinstallKey[:]) + marker, err := ioutil.ReadFile(filepath.Join(boot.InstallHostFDEDataDir, "marker")) + c.Assert(err, IsNil) + c.Check(marker, HasLen, 32) + c.Check(filepath.Join(boot.InstallHostFDESaveDir, "marker"), testutil.FileEquals, marker) } func (s *deviceMgrInstallModeSuite) TestInstallSecuredBypassEncryption(c *C) { @@ -402,12 +484,64 @@ c.Assert(err, ErrorMatches, "(?s).*cannot encrypt secured device: TPM not available.*") } +func (s *deviceMgrInstallModeSuite) testInstallEncryptionSanityChecks(c *C, errMatch string) { + restore := release.MockOnClassic(false) + defer restore() + + restore = devicestate.MockSecbootCheckKeySealingSupported(func() error { return nil }) + defer restore() + + err := ioutil.WriteFile(filepath.Join(dirs.GlobalRootDir, "/var/lib/snapd/modeenv"), + []byte("mode=install\n"), 0644) + c.Assert(err, IsNil) + + s.state.Lock() + s.makeMockInstalledPcGadget(c, "dangerous", "") + devicestate.SetSystemMode(s.mgr, "install") + s.state.Unlock() + + s.settle(c) + + s.state.Lock() + defer s.state.Unlock() + + installSystem := s.findInstallSystem() + c.Check(installSystem.Err(), ErrorMatches, errMatch) + // no restart request on failure + c.Check(s.restartRequests, HasLen, 0) +} + +func (s *deviceMgrInstallModeSuite) TestInstallEncryptionSanityChecksNoKeys(c *C) { + restore := devicestate.MockInstallRun(func(gadgetRoot, device string, options install.Options, _ gadget.ContentObserver) (*install.InstalledSystemSideData, error) { + c.Check(options.Encrypt, Equals, true) + // no keys set + return &install.InstalledSystemSideData{}, nil + }) + defer restore() + s.testInstallEncryptionSanityChecks(c, `(?ms)cannot perform the following tasks: +- Setup system for run mode \(internal error: system encryption keys are unset\)`) +} + +func (s *deviceMgrInstallModeSuite) TestInstallEncryptionSanityChecksNoSystemDataKey(c *C) { + restore := devicestate.MockInstallRun(func(gadgetRoot, device string, options install.Options, _ gadget.ContentObserver) (*install.InstalledSystemSideData, error) { + c.Check(options.Encrypt, Equals, true) + // no keys set + return &install.InstalledSystemSideData{ + // empty map + KeysForRoles: map[string]*install.EncryptionKeySet{}, + }, nil + }) + defer restore() + s.testInstallEncryptionSanityChecks(c, `(?ms)cannot perform the following tasks: +- Setup system for run mode \(internal error: system encryption keys are unset\)`) +} + func (s *deviceMgrInstallModeSuite) mockInstallModeChange(c *C, modelGrade, gadgetDefaultsYaml string) *asserts.Model { restore := release.MockOnClassic(false) defer restore() - restore = devicestate.MockInstallRun(func(gadgetRoot, device string, options install.Options, _ install.SystemInstallObserver) error { - return nil + restore = devicestate.MockInstallRun(func(gadgetRoot, device string, options install.Options, _ gadget.ContentObserver) (*install.InstalledSystemSideData, error) { + return nil, nil }) defer restore() @@ -536,7 +670,9 @@ err = ioutil.WriteFile(filepath.Join(gadgetDir, "cloud.conf"), nil, 0644) c.Assert(err, IsNil) - err = s.doRunChangeTestWithEncryption(c, "secured", encTestCase{tpm: true, bypass: false, encrypt: true}) + err = s.doRunChangeTestWithEncryption(c, "secured", encTestCase{ + tpm: true, bypass: false, encrypt: true, trustedBootloader: true, + }) c.Assert(err, IsNil) c.Assert(s.ConfigureTargetSystemOptsPassed, DeepEquals, []*sysconfig.Options{ @@ -558,7 +694,9 @@ c.Assert(err, IsNil) } - err = s.doRunChangeTestWithEncryption(c, "secured", encTestCase{tpm: true, bypass: false, encrypt: true}) + err = s.doRunChangeTestWithEncryption(c, "secured", encTestCase{ + tpm: true, bypass: false, encrypt: true, trustedBootloader: true, + }) c.Assert(err, IsNil) // and did NOT tell sysconfig about the cloud-init files, instead it was @@ -590,5 +728,72 @@ c.Check(installSystem.Err(), IsNil) c.Check(installSystem.Status(), Equals, state.DoneStatus) - c.Check(filepath.Join(boot.InitramfsUbuntuBootDir, "model"), testutil.FileEquals, buf.String()) + c.Check(filepath.Join(boot.InitramfsUbuntuBootDir, "device/model"), testutil.FileEquals, buf.String()) +} + +func (s *deviceMgrInstallModeSuite) testInstallGadgetNoSave(c *C) { + err := ioutil.WriteFile(filepath.Join(dirs.GlobalRootDir, "/var/lib/snapd/modeenv"), + []byte("mode=install\n"), 0644) + c.Assert(err, IsNil) + + s.state.Lock() + s.makeMockInstalledPcGadget(c, "dangerous", "") + info, err := snapstate.CurrentInfo(s.state, "pc") + c.Assert(err, IsNil) + // replace gadget yaml with one that has no ubuntu-save + c.Assert(uc20gadgetYaml, Not(testutil.Contains), "ubuntu-save") + err = ioutil.WriteFile(filepath.Join(info.MountDir(), "meta/gadget.yaml"), []byte(uc20gadgetYaml), 0644) + c.Assert(err, IsNil) + devicestate.SetSystemMode(s.mgr, "install") + s.state.Unlock() + + s.settle(c) +} + +func (s *deviceMgrInstallModeSuite) TestInstallWithEncryptionValidatesGadgetErr(c *C) { + restore := release.MockOnClassic(false) + defer restore() + + restore = devicestate.MockInstallRun(func(gadgetRoot, device string, options install.Options, _ gadget.ContentObserver) (*install.InstalledSystemSideData, error) { + return nil, fmt.Errorf("unexpected call") + }) + defer restore() + + // pretend we have a TPM + restore = devicestate.MockSecbootCheckKeySealingSupported(func() error { return nil }) + defer restore() + + s.testInstallGadgetNoSave(c) + + s.state.Lock() + defer s.state.Unlock() + + installSystem := s.findInstallSystem() + c.Check(installSystem.Err(), ErrorMatches, `(?ms)cannot perform the following tasks: +- Setup system for run mode \(cannot use gadget: gadget does not support encrypted data: volume "pc" has no structure with system-save role\)`) + // no restart request on failure + c.Check(s.restartRequests, HasLen, 0) +} + +func (s *deviceMgrInstallModeSuite) TestInstallWithoutEncryptionValidatesGadgetWithoutSaveHappy(c *C) { + restore := release.MockOnClassic(false) + defer restore() + + restore = devicestate.MockInstallRun(func(gadgetRoot, device string, options install.Options, _ gadget.ContentObserver) (*install.InstalledSystemSideData, error) { + return nil, nil + }) + defer restore() + + // pretend we have a TPM + restore = devicestate.MockSecbootCheckKeySealingSupported(func() error { return fmt.Errorf("TPM2 not available") }) + defer restore() + + s.testInstallGadgetNoSave(c) + + s.state.Lock() + defer s.state.Unlock() + + installSystem := s.findInstallSystem() + c.Check(installSystem.Err(), IsNil) + c.Check(s.restartRequests, HasLen, 1) } diff -Nru snapd-2.47.1+20.10.1build1/overlord/devicestate/devicestate_remodel_test.go snapd-2.48+21.04/overlord/devicestate/devicestate_remodel_test.go --- snapd-2.47.1+20.10.1build1/overlord/devicestate/devicestate_remodel_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/overlord/devicestate/devicestate_remodel_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -33,6 +33,7 @@ "github.com/snapcore/snapd/asserts" "github.com/snapcore/snapd/asserts/assertstest" "github.com/snapcore/snapd/gadget" + "github.com/snapcore/snapd/gadget/quantity" "github.com/snapcore/snapd/overlord/assertstate" "github.com/snapcore/snapd/overlord/assertstate/assertstatetest" "github.com/snapcore/snapd/overlord/auth" @@ -1351,7 +1352,7 @@ Structure: []gadget.VolumeStructure{{ Name: "foo", Type: "00000000-0000-0000-0000-0000deadcafe", - Size: 10 * gadget.SizeMiB, + Size: 10 * quantity.SizeMiB, Filesystem: "ext4", Content: []gadget.VolumeContent{ {Source: "foo-content", Target: "/"}, @@ -1359,7 +1360,7 @@ }, { Name: "bare-one", Type: "bare", - Size: gadget.SizeMiB, + Size: quantity.SizeMiB, Content: []gadget.VolumeContent{ {Image: "bare.img"}, }, @@ -1377,7 +1378,7 @@ Structure: []gadget.VolumeStructure{{ Name: "foo", Type: "00000000-0000-0000-0000-0000deadcafe", - Size: 10 * gadget.SizeMiB, + Size: 10 * quantity.SizeMiB, Filesystem: "ext4", Content: []gadget.VolumeContent{ {Source: "new-foo-content", Target: "/"}, @@ -1385,7 +1386,7 @@ }, { Name: "bare-one", Type: "bare", - Size: gadget.SizeMiB, + Size: quantity.SizeMiB, Content: []gadget.VolumeContent{ {Image: "new-bare-content.img"}, }, diff -Nru snapd-2.47.1+20.10.1build1/overlord/devicestate/devicestate_serial_test.go snapd-2.48+21.04/overlord/devicestate/devicestate_serial_test.go --- snapd-2.47.1+20.10.1build1/overlord/devicestate/devicestate_serial_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/overlord/devicestate/devicestate_serial_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -26,6 +26,7 @@ "net/http/httptest" "net/url" "os" + "path/filepath" "syscall" "time" @@ -34,8 +35,11 @@ "github.com/snapcore/snapd/asserts" "github.com/snapcore/snapd/asserts/assertstest" + "github.com/snapcore/snapd/asserts/sysdb" + "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/httputil" "github.com/snapcore/snapd/logger" + "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/overlord/assertstate" "github.com/snapcore/snapd/overlord/assertstate/assertstatetest" "github.com/snapcore/snapd/overlord/auth" @@ -47,8 +51,10 @@ "github.com/snapcore/snapd/overlord/storecontext" "github.com/snapcore/snapd/release" "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/snap/snaptest" "github.com/snapcore/snapd/snapdenv" "github.com/snapcore/snapd/strutil" + "github.com/snapcore/snapd/testutil" ) var testKeyLength = 1024 @@ -67,7 +73,7 @@ var signing assertstest.SignerDB = s.storeSigning switch model { - case "pc", "pc2": + case "pc", "pc2", "pc-20": fallthrough case "classic-alt-store": c.Check(brandID, Equals, "canonical") @@ -189,6 +195,9 @@ c.Check(privKey, NotNil) c.Check(device.KeyID, Equals, privKey.PublicKey().ID()) + + // check that keypair manager is under device + c.Check(osutil.IsDirectory(filepath.Join(dirs.SnapDeviceDir, "private-keys-v1")), Equals, true) } func (s *deviceMgrSerialSuite) TestFullDeviceRegistrationHappyWithProxy(c *C) { @@ -698,6 +707,12 @@ s.state.Lock() defer s.state.Unlock() + s.makeModelAssertionInState(c, "canonical", "pc", map[string]interface{}{ + "architecture": "amd64", + "kernel": "pc-kernel", + "gadget": "pc", + }) + devicestatetest.MockGadget(c, s.state, "gadget", snap.R(2), nil) devicestatetest.SetDevice(s.state, &auth.DeviceState{ @@ -1468,6 +1483,17 @@ s.state.Lock() defer s.state.Unlock() + // set model as seeding would + s.makeModelAssertionInState(c, "canonical", "pc", map[string]interface{}{ + "architecture": "amd64", + "kernel": "pc-kernel", + "gadget": "pc", + }) + devicestatetest.SetDevice(s.state, &auth.DeviceState{ + Brand: "canonical", + Model: "pc", + }) + scb := s.mgr.StoreContextBackend() // nothing there @@ -1867,3 +1893,116 @@ becomeOperational := s.findBecomeOperationalChange() c.Assert(becomeOperational, IsNil) } + +func (s *deviceMgrSerialSuite) TestFullDeviceRegistrationUC20Happy(c *C) { + defer sysdb.InjectTrusted([]asserts.Assertion{s.storeSigning.TrustedKey})() + + r1 := devicestate.MockKeyLength(testKeyLength) + defer r1() + + mockServer := s.mockServer(c, "REQID-1", nil) + defer mockServer.Close() + + r2 := devicestate.MockBaseStoreURL(mockServer.URL) + defer r2() + + // setup state as will be done by first-boot + s.state.Lock() + defer s.state.Unlock() + + s.makeModelAssertionInState(c, "canonical", "pc-20", map[string]interface{}{ + "architecture": "amd64", + // UC20 + "base": "core20", + "snaps": []interface{}{ + map[string]interface{}{ + "name": "pc-kernel", + "id": snaptest.AssertedSnapID("oc-kernel"), + "type": "kernel", + "default-channel": "20", + }, + map[string]interface{}{ + "name": "pc", + "id": snaptest.AssertedSnapID("pc"), + "type": "gadget", + "default-channel": "20", + }, + }, + }) + + devicestatetest.SetDevice(s.state, &auth.DeviceState{ + Brand: "canonical", + Model: "pc-20", + }) + + // save is available + devicestate.SetSaveAvailable(s.mgr, true) + + // avoid full seeding + s.seeding() + + becomeOperational := s.findBecomeOperationalChange() + c.Check(becomeOperational, IsNil) + + devicestatetest.MockGadget(c, s.state, "pc", snap.R(2), nil) + // mark it as seeded + s.state.Set("seeded", true) + // skip boot ok logic + devicestate.SetBootOkRan(s.mgr, true) + + // runs the whole device registration process + s.state.Unlock() + s.settle(c) + s.state.Lock() + + becomeOperational = s.findBecomeOperationalChange() + c.Assert(becomeOperational, NotNil) + + c.Check(becomeOperational.Status().Ready(), Equals, true) + c.Check(becomeOperational.Err(), IsNil) + + device, err := devicestatetest.Device(s.state) + c.Assert(err, IsNil) + c.Check(device.Brand, Equals, "canonical") + c.Check(device.Model, Equals, "pc-20") + c.Check(device.Serial, Equals, "9999") + + ok := false + select { + case <-s.mgr.Registered(): + ok = true + case <-time.After(5 * time.Second): + c.Fatal("should have been marked registered") + } + c.Check(ok, Equals, true) + + a, err := s.db.Find(asserts.SerialType, map[string]string{ + "brand-id": "canonical", + "model": "pc-20", + "serial": "9999", + }) + c.Assert(err, IsNil) + serial := a.(*asserts.Serial) + + privKey, err := devicestate.KeypairManager(s.mgr).Get(serial.DeviceKey().ID()) + c.Assert(err, IsNil) + c.Check(privKey, NotNil) + + c.Check(device.KeyID, Equals, privKey.PublicKey().ID()) + + // check that keypair manager is under save + c.Check(osutil.IsDirectory(filepath.Join(dirs.SnapDeviceSaveDir, "private-keys-v1")), Equals, true) + c.Check(filepath.Join(dirs.SnapDeviceDir, "private-keys-v1"), testutil.FileAbsent) + + // check that the serial was saved to the device save assertion db + // as well + savedb, err := sysdb.OpenAt(dirs.SnapDeviceSaveDir) + c.Assert(err, IsNil) + // a copy of serial was backed up there + _, err = savedb.Find(asserts.SerialType, map[string]string{ + "brand-id": "canonical", + "model": "pc-20", + "serial": "9999", + }) + c.Assert(err, IsNil) +} diff -Nru snapd-2.47.1+20.10.1build1/overlord/devicestate/devicestate_test.go snapd-2.48+21.04/overlord/devicestate/devicestate_test.go --- snapd-2.47.1+20.10.1build1/overlord/devicestate/devicestate_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/overlord/devicestate/devicestate_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -38,6 +38,7 @@ "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/interfaces" "github.com/snapcore/snapd/interfaces/builtin" + "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/overlord" "github.com/snapcore/snapd/overlord/assertstate" "github.com/snapcore/snapd/overlord/assertstate/assertstatetest" @@ -131,7 +132,13 @@ dirs.SetRootDir(c.MkDir()) s.AddCleanup(func() { dirs.SetRootDir("") }) - os.MkdirAll(dirs.SnapRunDir, 0755) + + err := os.MkdirAll(dirs.SnapRunDir, 0755) + c.Assert(err, IsNil) + err = os.MkdirAll(dirs.SnapdStateDir(dirs.GlobalRootDir), 0755) + c.Assert(err, IsNil) + + s.AddCleanup(osutil.MockMountInfo(``)) s.restartRequests = nil @@ -942,6 +949,19 @@ return info11 } +func makeInstalledMockKernelSnap(c *C, st *state.State, yml string) *snap.Info { + sideInfo11 := &snap.SideInfo{RealName: "pc-kernel", Revision: snap.R(11), SnapID: "pc-kernel-id"} + snapstate.Set(st, "pc-kernel", &snapstate.SnapState{ + Active: true, + Sequence: []*snap.SideInfo{sideInfo11}, + Current: sideInfo11.Revision, + SnapType: "kernel", + }) + info11 := snaptest.MockSnap(c, yml, sideInfo11) + + return info11 +} + func makeMockRepoWithConnectedSnaps(c *C, st *state.State, info11, core11 *snap.Info, ifname string) { repo := interfaces.NewRepository() for _, iface := range builtin.Interfaces() { @@ -1132,6 +1152,169 @@ c.Check(s.mgr.SystemMode(), Equals, "run") } +const ( + mountRunMntUbuntuSaveFmt = `26 27 8:3 / %s/run/mnt/ubuntu-save rw,relatime shared:7 - ext4 /dev/fakedevice0p1 rw,data=ordered` + mountSnapSaveFmt = `26 27 8:3 / %s/var/lib/snapd/save rw,relatime shared:7 - ext4 /dev/fakedevice0p1 rw,data=ordered` +) + +func (s *deviceMgrSuite) TestDeviceManagerStartupUC20UbuntuSaveFullHappy(c *C) { + modeEnv := &boot.Modeenv{Mode: "run"} + err := modeEnv.WriteTo("") + c.Assert(err, IsNil) + // create a new manager so that the modeenv we mocked in read + mgr, err := devicestate.Manager(s.state, s.hookMgr, s.o.TaskRunner(), s.newStore) + c.Assert(err, IsNil) + + cmd := testutil.MockCommand(c, "systemd-mount", "") + defer cmd.Restore() + + // ubuntu-save not mounted + err = mgr.StartUp() + c.Assert(err, IsNil) + c.Check(cmd.Calls(), HasLen, 0) + + restore := osutil.MockMountInfo(fmt.Sprintf(mountRunMntUbuntuSaveFmt, dirs.GlobalRootDir)) + defer restore() + + err = mgr.StartUp() + c.Assert(err, IsNil) + c.Check(cmd.Calls(), DeepEquals, [][]string{ + {"systemd-mount", "-o", "bind", boot.InitramfsUbuntuSaveDir, dirs.SnapSaveDir}, + }) + + // known as available + c.Check(devicestate.SaveAvailable(mgr), Equals, true) +} + +func (s *deviceMgrSuite) TestDeviceManagerStartupUC20UbuntuSaveAlreadyMounted(c *C) { + modeEnv := &boot.Modeenv{Mode: "run"} + err := modeEnv.WriteTo("") + c.Assert(err, IsNil) + // create a new manager so that the modeenv we mocked in read + mgr, err := devicestate.Manager(s.state, s.hookMgr, s.o.TaskRunner(), s.newStore) + c.Assert(err, IsNil) + + cmd := testutil.MockCommand(c, "systemd-mount", "") + defer cmd.Restore() + + // already mounted + restore := osutil.MockMountInfo(fmt.Sprintf(mountRunMntUbuntuSaveFmt, dirs.GlobalRootDir) + "\n" + + fmt.Sprintf(mountSnapSaveFmt, dirs.GlobalRootDir)) + defer restore() + + err = mgr.StartUp() + c.Assert(err, IsNil) + c.Check(cmd.Calls(), HasLen, 0) + + // known as available + c.Check(devicestate.SaveAvailable(mgr), Equals, true) +} + +func (s *deviceMgrSuite) TestDeviceManagerStartupUC20NoUbuntuSave(c *C) { + modeEnv := &boot.Modeenv{Mode: "run"} + err := modeEnv.WriteTo("") + c.Assert(err, IsNil) + // create a new manager so that the modeenv we mocked in read + mgr, err := devicestate.Manager(s.state, s.hookMgr, s.o.TaskRunner(), s.newStore) + c.Assert(err, IsNil) + + cmd := testutil.MockCommand(c, "systemd-mount", "") + defer cmd.Restore() + + // ubuntu-save not mounted + err = mgr.StartUp() + c.Assert(err, IsNil) + c.Check(cmd.Calls(), HasLen, 0) + + // known as available + c.Check(devicestate.SaveAvailable(mgr), Equals, true) +} + +func (s *deviceMgrSuite) TestDeviceManagerStartupUC20UbuntuSaveErr(c *C) { + modeEnv := &boot.Modeenv{Mode: "run"} + err := modeEnv.WriteTo("") + c.Assert(err, IsNil) + // create a new manager so that the modeenv we mocked in read + mgr, err := devicestate.Manager(s.state, s.hookMgr, s.o.TaskRunner(), s.newStore) + c.Assert(err, IsNil) + + cmd := testutil.MockCommand(c, "systemd-mount", "echo failed; exit 1") + defer cmd.Restore() + + restore := osutil.MockMountInfo(fmt.Sprintf(mountRunMntUbuntuSaveFmt, dirs.GlobalRootDir)) + defer restore() + + err = mgr.StartUp() + c.Assert(err, ErrorMatches, "cannot set up ubuntu-save: cannot bind mount .*/run/mnt/ubuntu-save under .*/var/lib/snapd/save: failed") + c.Check(cmd.Calls(), DeepEquals, [][]string{ + {"systemd-mount", "-o", "bind", boot.InitramfsUbuntuSaveDir, dirs.SnapSaveDir}, + }) + + // known as not available + c.Check(devicestate.SaveAvailable(mgr), Equals, false) +} + +func (s *deviceMgrSuite) TestDeviceManagerStartupNonUC20NoUbuntuSave(c *C) { + err := os.RemoveAll(dirs.SnapModeenvFileUnder(dirs.GlobalRootDir)) + c.Assert(err, IsNil) + // create a new manager so that we know it does not see the modeenv + mgr, err := devicestate.Manager(s.state, s.hookMgr, s.o.TaskRunner(), s.newStore) + c.Assert(err, IsNil) + + cmd := testutil.MockCommand(c, "systemd-mount", "") + defer cmd.Restore() + + // ubuntu-save not mounted + err = mgr.StartUp() + c.Assert(err, IsNil) + c.Check(cmd.Calls(), HasLen, 0) + + // known as not available + c.Check(devicestate.SaveAvailable(mgr), Equals, false) +} + +var kernelYamlNoFdeSetup = `name: pc-kernel +version: 1.0 +type: kernel +` + +var kernelYamlWithFdeSetup = `name: pc-kernel +version: 1.0 +type: kernel +hooks: + fde-setup: +` + +func (s *deviceMgrSuite) TestHasFdeSetupHook(c *C) { + st := s.state + st.Lock() + defer st.Unlock() + + s.makeModelAssertionInState(c, "canonical", "pc", map[string]interface{}{ + "architecture": "amd64", + "kernel": "pc-kernel", + "gadget": "pc", + }) + devicestatetest.SetDevice(s.state, &auth.DeviceState{ + Brand: "canonical", + Model: "pc", + }) + + for _, tc := range []struct { + kernelYaml string + hasFdeSetupHook bool + }{ + {kernelYamlNoFdeSetup, false}, + {kernelYamlWithFdeSetup, true}, + } { + makeInstalledMockKernelSnap(c, st, tc.kernelYaml) + + hasHook, err := devicestate.DeviceManagerHasFDESetupHook(s.mgr) + c.Assert(err, IsNil) + c.Check(hasHook, Equals, tc.hasFdeSetupHook) + } +} + type startOfOperationTimeSuite struct { state *state.State mgr *devicestate.DeviceManager diff -Nru snapd-2.47.1+20.10.1build1/overlord/devicestate/export_test.go snapd-2.48+21.04/overlord/devicestate/export_test.go --- snapd-2.47.1+20.10.1build1/overlord/devicestate/export_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/overlord/devicestate/export_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -80,8 +80,24 @@ } } -func KeypairManager(m *DeviceManager) asserts.KeypairManager { - return m.keypairMgr +func KeypairManager(m *DeviceManager) (keypairMgr asserts.KeypairManager) { + // XXX expose the with... method at some point + err := m.withKeypairMgr(func(km asserts.KeypairManager) error { + keypairMgr = km + return nil + }) + if err != nil { + panic(err) + } + return keypairMgr +} + +func SaveAvailable(m *DeviceManager) bool { + return m.saveAvailable +} + +func SetSaveAvailable(m *DeviceManager, avail bool) { + m.saveAvailable = avail } func EnsureOperationalShouldBackoff(m *DeviceManager, now time.Time) bool { @@ -248,7 +264,7 @@ } } -func MockInstallRun(f func(gadgetRoot, device string, options install.Options, observer install.SystemInstallObserver) error) (restore func()) { +func MockInstallRun(f func(gadgetRoot, device string, options install.Options, observer gadget.ContentObserver) (*install.InstalledSystemSideData, error)) (restore func()) { old := installRun installRun = f return func() { @@ -271,3 +287,7 @@ restrictCloudInit = old } } + +func DeviceManagerHasFDESetupHook(mgr *DeviceManager) (bool, error) { + return mgr.hasFDESetupHook() +} diff -Nru snapd-2.47.1+20.10.1build1/overlord/devicestate/firstboot20_test.go snapd-2.48+21.04/overlord/devicestate/firstboot20_test.go --- snapd-2.47.1+20.10.1build1/overlord/devicestate/firstboot20_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/overlord/devicestate/firstboot20_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -277,7 +277,7 @@ // * 1 from MarkBootSuccessful() from ensureBootOk() before we restart // * 1 from boot.SetNextBoot() from LinkSnap() from doInstall() from InstallPath() from // installSeedSnap() after restart - // * 1 from boot.GetCurrentBoot() from WaitRestart after restart + // * 1 from boot.GetCurrentBoot() from FinishRestart after restart _, numKernelCalls := bloader.GetRunKernelImageFunctionSnapCalls("Kernel") c.Assert(numKernelCalls, Equals, 3) } diff -Nru snapd-2.47.1+20.10.1build1/overlord/devicestate/firstboot.go snapd-2.48+21.04/overlord/devicestate/firstboot.go --- snapd-2.47.1+20.10.1build1/overlord/devicestate/firstboot.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/overlord/devicestate/firstboot.go 2020-11-19 16:51:02.000000000 +0000 @@ -170,6 +170,10 @@ if beginTask != nil { // hooks must wait for mark-preseeded hooksTask.WaitFor(preseedDoneTask) + if n := len(all); n > 0 { + // the first hook of the snap waits for all tasks of previous snap + hooksTask.WaitAll(all[n-1]) + } if lastBeforeHooksTask != nil { beginTask.WaitFor(lastBeforeHooksTask) } @@ -273,6 +277,7 @@ return nil, fmt.Errorf("cannot proceed, no snaps to seed") } + // ts is the taskset of the last snap ts := tsAll[len(tsAll)-1] endTs := state.NewTaskSet() @@ -290,6 +295,7 @@ } markSeeded.Set("seed-system", whatSeeds) + // mark-seeded waits for the taskset of last snap markSeeded.WaitAll(ts) endTs.AddTask(markSeeded) tsAll = append(tsAll, endTs) diff -Nru snapd-2.47.1+20.10.1build1/overlord/devicestate/firstboot_preseed_test.go snapd-2.48+21.04/overlord/devicestate/firstboot_preseed_test.go --- snapd-2.47.1+20.10.1build1/overlord/devicestate/firstboot_preseed_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/overlord/devicestate/firstboot_preseed_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -179,10 +179,6 @@ continue } - snapsup, err := snapstate.TaskSnapSetup(task0) - c.Assert(err, IsNil, Commentf("%#v", task0)) - c.Check(snapsup.InstanceName(), Equals, snaps[matched]) - matched++ if i == 0 { c.Check(waitTasks, HasLen, 0) } else { @@ -191,6 +187,51 @@ c.Check(waitTasks[0], Equals, prevTask) } + // make sure that install-hooks wait for the previous snap, and for + // mark-preseeded. + hookEdgeTask, err := ts.Edge(snapstate.HooksEdge) + c.Assert(err, IsNil) + c.Assert(hookEdgeTask.Kind(), Equals, "run-hook") + var hsup hookstate.HookSetup + c.Assert(hookEdgeTask.Get("hook-setup", &hsup), IsNil) + c.Check(hsup.Hook, Equals, "install") + switch hsup.Snap { + case "core", "core18", "snapd": + // ignore + default: + // snaps other than core/core18/snapd + var waitsForMarkPreseeded, waitsForPreviousSnapHook, waitsForPreviousSnap bool + for _, wt := range hookEdgeTask.WaitTasks() { + switch wt.Kind() { + case "setup-aliases": + continue + case "run-hook": + var wtsup hookstate.HookSetup + c.Assert(wt.Get("hook-setup", &wtsup), IsNil) + c.Check(wtsup.Snap, Equals, snaps[matched-1]) + waitsForPreviousSnapHook = true + case "mark-preseeded": + waitsForMarkPreseeded = true + case "prerequisites": + default: + snapsup, err := snapstate.TaskSnapSetup(wt) + c.Assert(err, IsNil, Commentf("%#v", wt)) + c.Check(snapsup.SnapName(), Equals, snaps[matched-1], Commentf("%s: %#v", hsup.Snap, wt)) + waitsForPreviousSnap = true + } + } + c.Assert(waitsForMarkPreseeded, Equals, true) + c.Assert(waitsForPreviousSnapHook, Equals, true) + if snaps[matched-1] != "core" && snaps[matched-1] != "core18" && snaps[matched-1] != "pc" { + c.Check(waitsForPreviousSnap, Equals, true, Commentf("%s", snaps[matched-1])) + } + } + + snapsup, err := snapstate.TaskSnapSetup(task0) + c.Assert(err, IsNil, Commentf("%#v", task0)) + c.Check(snapsup.InstanceName(), Equals, snaps[matched]) + matched++ + // find setup-aliases task in current taskset; its position // is not fixed due to e.g. optional update-gadget-assets task. var aliasesTask *state.Task @@ -273,6 +314,13 @@ fooFname, fooDecl, fooRev := s.MakeAssertedSnap(c, snapYaml, nil, snap.R(128), "developerid") s.WriteAssertions("foo.asserts", s.devAcct, fooRev, fooDecl) + // put a firstboot snap into the SnapBlobDir + snapYaml2 := `name: bar +version: 1.0 +` + barFname, barDecl, barRev := s.MakeAssertedSnap(c, snapYaml2, nil, snap.R(33), "developerid") + s.WriteAssertions("bar.asserts", s.devAcct, barRev, barDecl) + // add a model assertion and its chain assertsChain := s.makeModelAssertionChain(c, "my-model-classic", nil) s.WriteAssertions("model.asserts", assertsChain...) @@ -282,9 +330,11 @@ snaps: - name: foo file: %s + - name: bar + file: %s - name: core file: %s -`, fooFname, coreFname)) +`, fooFname, barFname, coreFname)) err := ioutil.WriteFile(filepath.Join(dirs.SnapSeedDir, "seed.yaml"), content, 0644) c.Assert(err, IsNil) @@ -304,7 +354,7 @@ } c.Assert(st.Changes(), HasLen, 1) - checkPreseedOrder(c, tsAll, "core", "foo") + checkPreseedOrder(c, tsAll, "core", "foo", "bar") st.Unlock() err = s.overlord.Settle(settleTimeout) @@ -330,6 +380,8 @@ c.Check(err, IsNil) _, err = snapstate.CurrentInfo(diskState, "foo") c.Check(err, IsNil) + _, err = snapstate.CurrentInfo(diskState, "bar") + c.Check(err, IsNil) // but we're not considered seeded var seeded bool @@ -389,8 +441,6 @@ tsAll, err := devicestate.PopulateStateFromSeedImpl(st, opts, s.perfTimings) c.Assert(err, IsNil) - checkPreseedOrder(c, tsAll, "snapd", "core18", "foo") - // now run the change and check the result chg := st.NewChange("seed", "run the populate from seed changes") for _, ts := range tsAll { @@ -399,6 +449,8 @@ c.Assert(st.Changes(), HasLen, 1) c.Assert(chg.Err(), IsNil) + checkPreseedOrder(c, tsAll, "snapd", "core18", "foo") + st.Unlock() err = s.overlord.Settle(settleTimeout) st.Lock() diff -Nru snapd-2.47.1+20.10.1build1/overlord/devicestate/firstboot_test.go snapd-2.48+21.04/overlord/devicestate/firstboot_test.go --- snapd-2.47.1+20.10.1build1/overlord/devicestate/firstboot_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/overlord/devicestate/firstboot_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -514,10 +514,6 @@ tsAll, err := devicestate.PopulateStateFromSeedImpl(st, opts, s.perfTimings) c.Assert(err, IsNil) - checkOrder(c, tsAll, "core", "pc-kernel", "pc", "foo", "local") - - checkTasks(c, tsAll) - // now run the change and check the result // use the expected kind otherwise settle with start another one chg := st.NewChange("seed", "run the populate from seed changes") @@ -526,6 +522,9 @@ } c.Assert(st.Changes(), HasLen, 1) + checkOrder(c, tsAll, "core", "pc-kernel", "pc", "foo", "local") + checkTasks(c, tsAll) + // avoid device reg chg1 := st.NewChange("become-operational", "init device") chg1.SetStatus(state.DoingStatus) diff -Nru snapd-2.47.1+20.10.1build1/overlord/devicestate/handlers_gadget.go snapd-2.48+21.04/overlord/devicestate/handlers_gadget.go --- snapd-2.47.1+20.10.1build1/overlord/devicestate/handlers_gadget.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/overlord/devicestate/handlers_gadget.go 2020-11-19 16:51:02.000000000 +0000 @@ -139,7 +139,7 @@ } var updateObserver gadget.ContentUpdateObserver - observeTrustedBootAssets, err := boot.TrustedAssetsUpdateObserverForModel(model) + observeTrustedBootAssets, err := boot.TrustedAssetsUpdateObserverForModel(model, updateData.RootDir) if err != nil && err != boot.ErrObserverNotApplicable { return fmt.Errorf("cannot setup asset update observer: %v", err) } diff -Nru snapd-2.47.1+20.10.1build1/overlord/devicestate/handlers_install.go snapd-2.48+21.04/overlord/devicestate/handlers_install.go --- snapd-2.47.1+20.10.1build1/overlord/devicestate/handlers_install.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/overlord/devicestate/handlers_install.go 2020-11-19 16:51:02.000000000 +0000 @@ -29,11 +29,13 @@ "github.com/snapcore/snapd/asserts" "github.com/snapcore/snapd/boot" "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/gadget" "github.com/snapcore/snapd/gadget/install" "github.com/snapcore/snapd/logger" "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/overlord/snapstate" "github.com/snapcore/snapd/overlord/state" + "github.com/snapcore/snapd/randutil" "github.com/snapcore/snapd/secboot" "github.com/snapcore/snapd/sysconfig" ) @@ -128,41 +130,74 @@ if err != nil { return err } + bopts.Encrypt = useEncryption + + // make sure that gadget is usable for the set up we want to use it in + gadgetContaints := gadget.ValidationConstraints{ + EncryptedData: useEncryption, + } + if err := gadget.Validate(gadgetDir, deviceCtx.Model(), &gadgetContaints); err != nil { + return fmt.Errorf("cannot use gadget: %v", err) + } var trustedInstallObserver *boot.TrustedAssetsInstallObserver // get a nice nil interface by default - var installObserver install.SystemInstallObserver - if useEncryption { - bopts.Encrypt = true - - trustedInstallObserver, err = boot.TrustedAssetsInstallObserverForModel(deviceCtx.Model(), gadgetDir) - if err != nil && err != boot.ErrObserverNotApplicable { - return fmt.Errorf("cannot setup asset install observer: %v", err) - } - if err == nil { - installObserver = trustedInstallObserver + var installObserver gadget.ContentObserver + trustedInstallObserver, err = boot.TrustedAssetsInstallObserverForModel(deviceCtx.Model(), gadgetDir, useEncryption) + if err != nil && err != boot.ErrObserverNotApplicable { + return fmt.Errorf("cannot setup asset install observer: %v", err) + } + if err == nil { + installObserver = trustedInstallObserver + if !useEncryption { + // there will be no key sealing, so past the + // installation pass no other methods need to be called + trustedInstallObserver = nil } } + var installedSystem *install.InstalledSystemSideData // run the create partition code logger.Noticef("create and deploy partitions") func() { st.Unlock() defer st.Lock() - err = installRun(gadgetDir, "", bopts, installObserver) + installedSystem, err = installRun(gadgetDir, "", bopts, installObserver) }() if err != nil { - return fmt.Errorf("cannot create partitions: %v", err) + return fmt.Errorf("cannot install system: %v", err) } if trustedInstallObserver != nil { + // sanity check + if installedSystem.KeysForRoles == nil || installedSystem.KeysForRoles[gadget.SystemData] == nil || installedSystem.KeysForRoles[gadget.SystemSave] == nil { + return fmt.Errorf("internal error: system encryption keys are unset") + } + dataKeySet := installedSystem.KeysForRoles[gadget.SystemData] + saveKeySet := installedSystem.KeysForRoles[gadget.SystemSave] + + // make note of the encryption keys + trustedInstallObserver.ChosenEncryptionKeys(dataKeySet.Key, saveKeySet.Key) + + // keep track of recovery assets if err := trustedInstallObserver.ObserveExistingTrustedRecoveryAssets(boot.InitramfsUbuntuSeedDir); err != nil { return fmt.Errorf("cannot observe existing trusted recovery assets: err") } + if err := saveKeys(installedSystem.KeysForRoles); err != nil { + return err + } + // write markers containing a secret to pair data and save + if err := writeMarkers(); err != nil { + return err + } } // keep track of the model we installed - err = writeModel(deviceCtx.Model(), filepath.Join(boot.InitramfsUbuntuBootDir, "model")) + err = os.MkdirAll(filepath.Join(boot.InitramfsUbuntuBootDir, "device"), 0755) + if err != nil { + return fmt.Errorf("cannot store the model: %v", err) + } + err = writeModel(deviceCtx.Model(), filepath.Join(boot.InitramfsUbuntuBootDir, "device/model")) if err != nil { return fmt.Errorf("cannot store the model: %v", err) } @@ -202,6 +237,67 @@ return nil } +// writeMarkers writes markers containing the same secret to pair data and save. +func writeMarkers() error { + // ensure directory for markers exists + if err := os.MkdirAll(boot.InstallHostFDEDataDir, 0755); err != nil { + return err + } + if err := os.MkdirAll(boot.InstallHostFDESaveDir, 0755); err != nil { + return err + } + + // generate a secret random marker + markerSecret, err := randutil.CryptoTokenBytes(32) + if err != nil { + return fmt.Errorf("cannot create ubuntu-data/save marker secret: %v", err) + } + + dataMarker := filepath.Join(boot.InstallHostFDEDataDir, "marker") + if err := osutil.AtomicWriteFile(dataMarker, markerSecret, 0600, 0); err != nil { + return err + } + + saveMarker := filepath.Join(boot.InstallHostFDESaveDir, "marker") + if err := osutil.AtomicWriteFile(saveMarker, markerSecret, 0600, 0); err != nil { + return err + } + + return nil +} + +func saveKeys(keysForRoles map[string]*install.EncryptionKeySet) error { + dataKeySet := keysForRoles[gadget.SystemData] + + // ensure directory for keys exists + if err := os.MkdirAll(boot.InstallHostFDEDataDir, 0755); err != nil { + return err + } + + // Write the recovery key + recoveryKeyFile := filepath.Join(boot.InstallHostFDEDataDir, "recovery.key") + if err := dataKeySet.RecoveryKey.Save(recoveryKeyFile); err != nil { + return fmt.Errorf("cannot store recovery key: %v", err) + } + + saveKeySet := keysForRoles[gadget.SystemSave] + if saveKeySet == nil { + // no system-save support + return nil + } + + saveKey := filepath.Join(boot.InstallHostFDEDataDir, "ubuntu-save.key") + reinstallSaveKey := filepath.Join(boot.InstallHostFDEDataDir, "reinstall.key") + + if err := saveKeySet.Key.Save(saveKey); err != nil { + return fmt.Errorf("cannot store system save key: %v", err) + } + if err := saveKeySet.RecoveryKey.Save(reinstallSaveKey); err != nil { + return fmt.Errorf("cannot store reinstall key: %v", err) + } + return nil +} + var secbootCheckKeySealingSupported = secboot.CheckKeySealingSupported // checkEncryption verifies whether encryption should be used based on the diff -Nru snapd-2.47.1+20.10.1build1/overlord/devicestate/handlers_serial.go snapd-2.48+21.04/overlord/devicestate/handlers_serial.go --- snapd-2.47.1+20.10.1build1/overlord/devicestate/handlers_serial.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/overlord/devicestate/handlers_serial.go 2020-11-19 16:51:02.000000000 +0000 @@ -106,7 +106,9 @@ } privKey := asserts.RSAPrivateKey(keyPair) - err = m.keypairMgr.Put(privKey) + err = m.withKeypairMgr(func(keypairMgr asserts.KeypairManager) error { + return keypairMgr.Put(privKey) + }) if err != nil { return fmt.Errorf("cannot store device key pair: %v", err) } @@ -683,7 +685,27 @@ } finish := func(serial *asserts.Serial) error { - if regCtx.FinishRegistration(serial); err != nil { + // save serial if appropriate into the device save + // assertion database + err := m.withSaveAssertDB(func(savedb *asserts.Database) error { + db := assertstate.DB(st) + retrieve := func(ref *asserts.Ref) (asserts.Assertion, error) { + return ref.Resolve(db.Find) + } + b := asserts.NewBatch(nil) + err := b.Fetch(savedb, retrieve, func(f asserts.Fetcher) error { + return f.Save(serial) + }) + if err != nil { + return err + } + return b.CommitTo(savedb, nil) + }) + if err != nil && err != errNoSaveSupport { + return fmt.Errorf("cannot save serial to device save assertion database: %v", err) + } + + if err := regCtx.FinishRegistration(serial); err != nil { return err } t.SetStatus(state.DoneStatus) diff -Nru snapd-2.47.1+20.10.1build1/overlord/devicestate/handlers_test.go snapd-2.48+21.04/overlord/devicestate/handlers_test.go --- snapd-2.47.1+20.10.1build1/overlord/devicestate/handlers_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/overlord/devicestate/handlers_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -631,7 +631,8 @@ c.Assert(st.Get("seed-restart-time", &seedRestartTime), IsNil) c.Check(seedRestartTime.Equal(devicestate.StartTime()), Equals, true) + // this runs on first boot c.Check(s.cmdSystemctl.Calls(), DeepEquals, [][]string{ - {"systemctl", "--root", dirs.GlobalRootDir, "enable", "snap.test-snap.srv.service"}, + {"systemctl", "enable", "snap.test-snap.srv.service"}, }) } diff -Nru snapd-2.47.1+20.10.1build1/overlord/ifacestate/export_test.go snapd-2.48+21.04/overlord/ifacestate/export_test.go --- snapd-2.47.1+20.10.1build1/overlord/ifacestate/export_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/overlord/ifacestate/export_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -58,7 +58,8 @@ AddHotplugSeqWaitTask = addHotplugSeqWaitTask AddHotplugSlot = addHotplugSlot - BatchConnectTasks = batchConnectTasks + BatchConnectTasks = batchConnectTasks + FirstTaskAfterBootWhenPreseeding = firstTaskAfterBootWhenPreseeding ) type ConnectOpts = connectOpts diff -Nru snapd-2.47.1+20.10.1build1/overlord/ifacestate/handlers.go snapd-2.48+21.04/overlord/ifacestate/handlers.go --- snapd-2.47.1+20.10.1build1/overlord/ifacestate/handlers.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/overlord/ifacestate/handlers.go 2020-11-19 16:51:02.000000000 +0000 @@ -32,6 +32,7 @@ "github.com/snapcore/snapd/interfaces" "github.com/snapcore/snapd/interfaces/hotplug" "github.com/snapcore/snapd/logger" + "github.com/snapcore/snapd/overlord/hookstate" "github.com/snapcore/snapd/overlord/snapstate" "github.com/snapcore/snapd/overlord/state" "github.com/snapcore/snapd/snap" @@ -1003,11 +1004,11 @@ // The "delayed-setup-profiles" flag is set on the connect tasks to // indicate that doConnect handler should not set security backends up // because this will be done later by the setup-profiles task. -func batchConnectTasks(st *state.State, snapsup *snapstate.SnapSetup, conns map[string]*interfaces.ConnRef, connOpts map[string]*connectOpts) (*state.TaskSet, error) { +func batchConnectTasks(st *state.State, snapsup *snapstate.SnapSetup, conns map[string]*interfaces.ConnRef, connOpts map[string]*connectOpts) (ts *state.TaskSet, hasInterfaceHooks bool, err error) { setupProfiles := st.NewTask("setup-profiles", fmt.Sprintf(i18n.G("Setup snap %q (%s) security profiles for auto-connections"), snapsup.InstanceName(), snapsup.Revision())) setupProfiles.Set("snap-setup", snapsup) - ts := state.NewTaskSet() + ts = state.NewTaskSet() for connID, conn := range conns { var opts connectOpts if providedOpts := connOpts[connID]; providedOpts != nil { @@ -1019,13 +1020,17 @@ opts.DelayedSetupProfiles = true connectTs, err := connect(st, conn.PlugRef.Snap, conn.PlugRef.Name, conn.SlotRef.Snap, conn.SlotRef.Name, opts) if err != nil { - return nil, fmt.Errorf("internal error: auto-connect of %q failed: %s", conn, err) + return nil, false, fmt.Errorf("internal error: auto-connect of %q failed: %s", conn, err) + } + + if len(connectTs.Tasks()) > 1 { + hasInterfaceHooks = true } // setup-profiles needs to wait for the main "connect" task connectTask, _ := connectTs.Edge(ConnectTaskEdge) if connectTask == nil { - return nil, fmt.Errorf("internal error: no 'connect' task found for %q", conn) + return nil, false, fmt.Errorf("internal error: no 'connect' task found for %q", conn) } setupProfiles.WaitFor(connectTask) @@ -1039,7 +1044,28 @@ if len(ts.Tasks()) > 0 { ts.AddTask(setupProfiles) } - return ts, nil + return ts, hasInterfaceHooks, nil +} + +// firstTaskAfterBootWhenPreseeding finds the first task to be run for thisSnap +// on first boot after mark-preseeded task, this is always the install hook. +// It is an internal error if install hook for thisSnap cannot be found. +func firstTaskAfterBootWhenPreseeding(thisSnap string, markPreseeded *state.Task) (*state.Task, error) { + if markPreseeded.Change() == nil { + return nil, fmt.Errorf("internal error: %s task not in change", markPreseeded.Kind()) + } + for _, ht := range markPreseeded.HaltTasks() { + if ht.Kind() == "run-hook" { + var hs hookstate.HookSetup + if err := ht.Get("hook-setup", &hs); err != nil { + return nil, fmt.Errorf("internal error: cannot get hook setup: %v", err) + } + if hs.Hook == "install" && hs.Snap == thisSnap { + return ht, nil + } + } + } + return nil, fmt.Errorf("internal error: cannot find install hook for snap %q", thisSnap) } func filterForSlot(slot *snap.SlotInfo) func(candSlots []*snap.SlotInfo) []*snap.SlotInfo { @@ -1078,7 +1104,7 @@ // if this is the case we can only proceed once the restart // has happened or we may not have all the interfaces of the // new core/base snap. - if err := snapstate.WaitRestart(task, snapsup); err != nil { + if err := snapstate.FinishRestart(task, snapsup); err != nil { return err } @@ -1173,18 +1199,45 @@ } } - autots, err := batchConnectTasks(st, snapsup, newconns, connOpts) + autots, hasInterfaceHooks, err := batchConnectTasks(st, snapsup, newconns, connOpts) if err != nil { return err } - if m.preseed && len(autots.Tasks()) > 2 { // connect task and setup-profiles tasks are 2 tasks, other tasks are hooks - // TODO: in preseed mode make interface hooks wait for mark-preseeded task. - for _, t := range autots.Tasks() { - if t.Kind() == "run-hook" { - return fmt.Errorf("interface hooks are not yet supported in preseed mode") + // If interface hooks are not present then connects can be executed during + // preseeding. + // Otherwise we will run all connects, their hooks and setup-profiles after + // preseeding (on first boot). Note, we may be facing multiple connections + // here where only some have hooks; however there is no point in running + // those without hooks before mark-preseeded, because only setup-profiles is + // performance-critical and it still needs to run after those with hooks. + if m.preseed && hasInterfaceHooks { + for _, t := range st.Tasks() { + if t.Kind() == "mark-preseeded" { + markPreseeded := t + // consistency check + if markPreseeded.Status() != state.DoStatus { + return fmt.Errorf("internal error: unexpected state of mark-preseeded task: %s", markPreseeded.Status()) + } + + firstTaskAfterBoot, err := firstTaskAfterBootWhenPreseeding(snapsup.InstanceName(), markPreseeded) + if err != nil { + return err + } + // first task of the snap that normally runs on first boot + // needs to wait on connects & interface hooks. + firstTaskAfterBoot.WaitAll(autots) + + // connect tasks and interface hooks need to wait for end of preseeding + // (they need to run on first boot, not during preseeding). + autots.WaitFor(markPreseeded) + t.Change().AddAll(autots) + task.SetStatus(state.DoneStatus) + st.EnsureBefore(0) + return nil } } + return fmt.Errorf("internal error: mark-preseeded task not found in preseeding mode") } if len(autots.Tasks()) > 0 { diff -Nru snapd-2.47.1+20.10.1build1/overlord/ifacestate/ifacestate_test.go snapd-2.48+21.04/overlord/ifacestate/ifacestate_test.go --- snapd-2.47.1+20.10.1build1/overlord/ifacestate/ifacestate_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/overlord/ifacestate/ifacestate_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -471,9 +471,10 @@ connOpts := make(map[string]*ifacestate.ConnectOpts) // no connections - ts, err := ifacestate.BatchConnectTasks(s.state, snapsup, conns, connOpts) + ts, hasInterfaceHooks, err := ifacestate.BatchConnectTasks(s.state, snapsup, conns, connOpts) c.Assert(err, IsNil) c.Check(ts.Tasks(), HasLen, 0) + c.Check(hasInterfaceHooks, Equals, false) // two connections cref1 := interfaces.ConnRef{PlugRef: interfaces.PlugRef{Snap: "consumer", Name: "plug"}, SlotRef: interfaces.SlotRef{Snap: "producer", Name: "slot"}} @@ -483,9 +484,10 @@ // connOpts for cref1 will default to AutoConnect: true connOpts[cref2.ID()] = &ifacestate.ConnectOpts{AutoConnect: true, ByGadget: true} - ts, err = ifacestate.BatchConnectTasks(s.state, snapsup, conns, connOpts) + ts, hasInterfaceHooks, err = ifacestate.BatchConnectTasks(s.state, snapsup, conns, connOpts) c.Assert(err, IsNil) c.Check(ts.Tasks(), HasLen, 9) + c.Check(hasInterfaceHooks, Equals, true) // "setup-profiles" task waits for "connect" tasks of both connections setupProfiles := ts.Tasks()[len(ts.Tasks())-1] @@ -523,6 +525,31 @@ } } +func (s *interfaceManagerSuite) TestBatchConnectTasksNoHooks(c *C) { + s.mockIfaces(c, &ifacetest.TestInterface{InterfaceName: "test"}, &ifacetest.TestInterface{InterfaceName: "test2"}) + s.mockSnap(c, consumer2Yaml) + s.mockSnap(c, producer2Yaml) + _ = s.manager(c) + + s.state.Lock() + defer s.state.Unlock() + + snapsup := &snapstate.SnapSetup{SideInfo: &snap.SideInfo{RealName: "snap"}} + conns := make(map[string]*interfaces.ConnRef) + connOpts := make(map[string]*ifacestate.ConnectOpts) + + // a connection + cref := interfaces.ConnRef{PlugRef: interfaces.PlugRef{Snap: "consumer2", Name: "plug"}, SlotRef: interfaces.SlotRef{Snap: "producer2", Name: "slot"}} + conns[cref.ID()] = &cref + + ts, hasInterfaceHooks, err := ifacestate.BatchConnectTasks(s.state, snapsup, conns, connOpts) + c.Assert(err, IsNil) + c.Assert(ts.Tasks(), HasLen, 2) + c.Check(ts.Tasks()[0].Kind(), Equals, "connect") + c.Check(ts.Tasks()[1].Kind(), Equals, "setup-profiles") + c.Check(hasInterfaceHooks, Equals, false) +} + type interfaceHooksTestData struct { consumer []string producer []string @@ -8190,10 +8217,7 @@ c.Check(repo.Interfaces().Connections, HasLen, 1) } -func (s *interfaceManagerSuite) TestPreseedAutoConnectErrorWithInterfaceHooks(c *C) { - restore := snapdenv.MockPreseeding(true) - defer restore() - +func (s *interfaceManagerSuite) autoconnectChangeForPreseeding(c *C, skipMarkPreseeded bool) (autoconnectTask, markPreseededTask *state.Task) { s.MockModel(c, nil) s.mockIfaces(c, &ifacetest.TestInterface{InterfaceName: "test"}, &ifacetest.TestInterface{InterfaceName: "test2"}) @@ -8203,21 +8227,177 @@ // Initialize the manager. This registers the OS snap. _ = s.manager(c) - // Run the setup-snap-security task and let it finish. - change := s.addSetupSnapSecurityChange(c, &snapstate.SnapSetup{ + snapsup := &snapstate.SnapSetup{ SideInfo: &snap.SideInfo{ RealName: snapInfo.SnapName(), Revision: snapInfo.Revision, }, - }) + } + + st := s.state + st.Lock() + defer st.Unlock() + + change := s.state.NewChange("test", "") + autoconnectTask = s.state.NewTask("auto-connect", "") + autoconnectTask.Set("snap-setup", snapsup) + change.AddTask(autoconnectTask) + if !skipMarkPreseeded { + markPreseededTask = s.state.NewTask("mark-preseeded", "") + markPreseededTask.WaitFor(autoconnectTask) + change.AddTask(markPreseededTask) + } + installHook := s.state.NewTask("run-hook", "") + hsup := &hookstate.HookSetup{ + Snap: snapInfo.InstanceName(), + Hook: "install", + } + installHook.Set("hook-setup", &hsup) + if markPreseededTask != nil { + installHook.WaitFor(markPreseededTask) + } else { + installHook.WaitFor(autoconnectTask) + } + change.AddTask(installHook) + return autoconnectTask, markPreseededTask +} + +func (s *interfaceManagerSuite) TestPreseedAutoConnectWithInterfaceHooks(c *C) { + restore := snapdenv.MockPreseeding(true) + defer restore() + + autoConnectTask, markPreseededTask := s.autoconnectChangeForPreseeding(c, false) + + st := s.state + s.settle(c) + st.Lock() + defer st.Unlock() + + change := markPreseededTask.Change() + c.Check(change.Status(), Equals, state.DoStatus) + c.Check(autoConnectTask.Status(), Equals, state.DoneStatus) + c.Check(markPreseededTask.Status(), Equals, state.DoStatus) + + checkWaitsForMarkPreseeded := func(t *state.Task) { + var foundMarkPreseeded bool + for _, wt := range t.WaitTasks() { + if wt.Kind() == "mark-preseeded" { + foundMarkPreseeded = true + break + } + } + c.Check(foundMarkPreseeded, Equals, true) + } + + var setupProfilesCount, connectCount, hookCount, installHook int + for _, t := range change.Tasks() { + switch t.Kind() { + case "setup-profiles": + c.Check(ifacestate.InSameChangeWaitChain(markPreseededTask, t), Equals, true) + checkWaitsForMarkPreseeded(t) + setupProfilesCount++ + case "connect": + c.Check(ifacestate.InSameChangeWaitChain(markPreseededTask, t), Equals, true) + checkWaitsForMarkPreseeded(t) + connectCount++ + case "run-hook": + c.Check(ifacestate.InSameChangeWaitChain(markPreseededTask, t), Equals, true) + var hsup hookstate.HookSetup + c.Assert(t.Get("hook-setup", &hsup), IsNil) + if hsup.Hook == "install" { + installHook++ + checkWaitsForMarkPreseeded(t) + } + hookCount++ + case "auto-connect": + case "mark-preseeded": + default: + c.Fatalf("unexpected task: %s", t.Kind()) + } + } + + c.Check(setupProfilesCount, Equals, 1) + c.Check(hookCount, Equals, 5) + c.Check(connectCount, Equals, 1) + c.Check(installHook, Equals, 1) +} + +func (s *interfaceManagerSuite) TestPreseedAutoConnectInternalErrorOnMarkPreseededState(c *C) { + restore := snapdenv.MockPreseeding(true) + defer restore() + + autoConnectTask, markPreseededTask := s.autoconnectChangeForPreseeding(c, false) + + st := s.state + st.Lock() + defer st.Unlock() + + markPreseededTask.SetStatus(state.DoingStatus) + st.Unlock() s.settle(c) + s.state.Lock() + c.Check(strings.Join(autoConnectTask.Log(), ""), Matches, `.* internal error: unexpected state of mark-preseeded task: Doing`) +} + +func (s *interfaceManagerSuite) TestPreseedAutoConnectInternalErrorMarkPreseededMissing(c *C) { + restore := snapdenv.MockPreseeding(true) + defer restore() + + skipMarkPreseeded := true + autoConnectTask, markPreseededTask := s.autoconnectChangeForPreseeding(c, skipMarkPreseeded) + c.Assert(markPreseededTask, IsNil) + + st := s.state + st.Lock() + defer st.Unlock() + + st.Unlock() + s.settle(c) s.state.Lock() - defer s.state.Unlock() - // Ensure that the task succeeded. - c.Check(change.Status(), Equals, state.ErrorStatus) - c.Check(change.Err(), ErrorMatches, `cannot perform the following tasks:\n.*interface hooks are not yet supported in preseed mode.*`) + c.Check(strings.Join(autoConnectTask.Log(), ""), Matches, `.* internal error: mark-preseeded task not found in preseeding mode`) +} + +func (s *interfaceManagerSuite) TestFirstTaskAfterBootWhenPreseeding(c *C) { + st := s.state + st.Lock() + defer st.Unlock() + + chg := st.NewChange("change", "") + + setupTask := st.NewTask("some-task", "") + setupTask.Set("snap-setup", &snapstate.SnapSetup{SideInfo: &snap.SideInfo{RealName: "test-snap"}}) + chg.AddTask(setupTask) + + markPreseeded := st.NewTask("fake-mark-preseeded", "") + markPreseeded.WaitFor(setupTask) + _, err := ifacestate.FirstTaskAfterBootWhenPreseeding("test-snap", markPreseeded) + c.Check(err, ErrorMatches, `internal error: fake-mark-preseeded task not in change`) + + chg.AddTask(markPreseeded) + + _, err = ifacestate.FirstTaskAfterBootWhenPreseeding("test-snap", markPreseeded) + c.Check(err, ErrorMatches, `internal error: cannot find install hook for snap "test-snap"`) + + // install hook of another snap + task1 := st.NewTask("run-hook", "") + hsup := hookstate.HookSetup{Hook: "install", Snap: "other-snap"} + task1.Set("hook-setup", &hsup) + task1.WaitFor(markPreseeded) + chg.AddTask(task1) + _, err = ifacestate.FirstTaskAfterBootWhenPreseeding("test-snap", markPreseeded) + c.Check(err, ErrorMatches, `internal error: cannot find install hook for snap "test-snap"`) + + // add install hook for the correct snap + task2 := st.NewTask("run-hook", "") + hsup = hookstate.HookSetup{Hook: "install", Snap: "test-snap"} + task2.Set("hook-setup", &hsup) + task2.WaitFor(markPreseeded) + chg.AddTask(task2) + hooktask, err := ifacestate.FirstTaskAfterBootWhenPreseeding("test-snap", markPreseeded) + c.Assert(err, IsNil) + c.Check(hooktask.ID(), Equals, task2.ID()) } // Tests for ResolveDisconnect() diff -Nru snapd-2.47.1+20.10.1build1/overlord/managers_test.go snapd-2.48+21.04/overlord/managers_test.go --- snapd-2.47.1+20.10.1build1/overlord/managers_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/overlord/managers_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -815,6 +815,13 @@ Name string `json:"name"` InstanceKey string `json:"instance-key"` Epoch snap.Epoch `json:"epoch"` + // assertions + Key string `json:"key"` + Assertions []struct { + Type string `json:"type"` + PrimaryKey []string `json:"primary-key"` + IfNewerThan *int `json:"if-newer-than"` + } } `json:"actions"` Context []struct { SnapID string `json:"snap-id"` @@ -834,9 +841,33 @@ Name string `json:"name"` Snap json.RawMessage `json:"snap"` InstanceKey string `json:"instance-key"` + // For assertions + Key string `json:"key"` + AssertionURLs []string `json:"assertion-stream-urls"` } var results []resultJSON for _, a := range input.Actions { + if a.Action == "fetch-assertions" { + urls := []string{} + for _, ar := range a.Assertions { + ref := &asserts.Ref{ + Type: asserts.Type(ar.Type), + PrimaryKey: ar.PrimaryKey, + } + _, err := ref.Resolve(s.storeSigning.Find) + if err != nil { + panic("missing assertions not supported") + } + urls = append(urls, fmt.Sprintf("%s/api/v1/snaps/assertions/%s", baseURL.String(), ref.Unique())) + + } + results = append(results, resultJSON{ + Result: "fetch-assertions", + Key: a.Key, + AssertionURLs: urls, + }) + continue + } name := s.serveIDtoName[a.SnapID] epoch := id2epoch[a.SnapID] if a.Action == "install" { diff -Nru snapd-2.47.1+20.10.1build1/overlord/servicestate/service_control_test.go snapd-2.48+21.04/overlord/servicestate/service_control_test.go --- snapd-2.47.1+20.10.1build1/overlord/servicestate/service_control_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/overlord/servicestate/service_control_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -467,8 +467,8 @@ c.Assert(t.Status(), Equals, state.DoneStatus) c.Check(s.sysctlArgs, DeepEquals, [][]string{ - {"--root", dirs.GlobalRootDir, "is-enabled", "snap.test-snap.foo.service"}, - {"--root", dirs.GlobalRootDir, "is-enabled", "snap.test-snap.bar.service"}, + {"is-enabled", "snap.test-snap.foo.service"}, + {"is-enabled", "snap.test-snap.bar.service"}, {"start", "snap.test-snap.foo.service"}, {"start", "snap.test-snap.bar.service"}, }) @@ -497,7 +497,7 @@ c.Assert(t.Status(), Equals, state.DoneStatus) c.Check(s.sysctlArgs, DeepEquals, [][]string{ - {"--root", dirs.GlobalRootDir, "is-enabled", "snap.test-snap.foo.service"}, + {"is-enabled", "snap.test-snap.foo.service"}, {"start", "snap.test-snap.foo.service"}, }) } @@ -526,7 +526,7 @@ c.Assert(t.Status(), Equals, state.DoneStatus) c.Check(s.sysctlArgs, DeepEquals, [][]string{ - {"--root", dirs.GlobalRootDir, "is-enabled", "snap.test-snap.foo.service"}, + {"is-enabled", "snap.test-snap.foo.service"}, {"start", "snap.test-snap.foo.service"}, }) } @@ -554,8 +554,8 @@ c.Assert(t.Status(), Equals, state.DoneStatus) c.Check(s.sysctlArgs, DeepEquals, [][]string{ - {"--root", dirs.GlobalRootDir, "is-enabled", "snap.test-snap.foo.service"}, - {"--root", dirs.GlobalRootDir, "is-enabled", "snap.test-snap.bar.service"}, + {"is-enabled", "snap.test-snap.foo.service"}, + {"is-enabled", "snap.test-snap.bar.service"}, {"start", "snap.test-snap.foo.service"}, {"start", "snap.test-snap.bar.service"}, }) @@ -615,7 +615,7 @@ c.Check(s.sysctlArgs, DeepEquals, [][]string{ {"stop", "snap.test-snap.foo.service"}, {"show", "--property=ActiveState", "snap.test-snap.foo.service"}, - {"--root", dirs.GlobalRootDir, "disable", "snap.test-snap.foo.service"}, + {"disable", "snap.test-snap.foo.service"}, }) } diff -Nru snapd-2.47.1+20.10.1build1/overlord/servicestate/servicestate.go snapd-2.48+21.04/overlord/servicestate/servicestate.go --- snapd-2.47.1+20.10.1build1/overlord/servicestate/servicestate.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/overlord/servicestate/servicestate.go 2020-11-19 16:51:02.000000000 +0000 @@ -25,7 +25,6 @@ "time" "github.com/snapcore/snapd/client" - "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/overlord/cmdstate" "github.com/snapcore/snapd/overlord/hookstate" "github.com/snapcore/snapd/overlord/snapstate" @@ -133,7 +132,7 @@ Notify(string) }) *StatusDecorator { return &StatusDecorator{ - sysd: systemd.New(dirs.GlobalRootDir, systemd.SystemMode, rep), + sysd: systemd.New(systemd.SystemMode, rep), } } diff -Nru snapd-2.47.1+20.10.1build1/overlord/snapshotstate/backend/backend.go snapd-2.48+21.04/overlord/snapshotstate/backend/backend.go --- snapd-2.47.1+20.10.1build1/overlord/snapshotstate/backend/backend.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/overlord/snapshotstate/backend/backend.go 2020-11-19 16:51:02.000000000 +0000 @@ -22,17 +22,21 @@ import ( "archive/tar" "archive/zip" + "bytes" "context" "crypto" "encoding/json" "errors" "fmt" "io" + "io/ioutil" "os" "path" "path/filepath" "runtime" "sort" + "strconv" + "strings" "syscall" "time" @@ -71,6 +75,40 @@ Auto bool } +// LastSnapshotSetID returns the highest set id number for the snapshots stored +// in snapshots directory; set ids are inferred from the filenames. +func LastSnapshotSetID() (uint64, error) { + dir, err := osOpen(dirs.SnapshotsDir) + if err != nil { + if osutil.IsDirNotExist(err) { + // no snapshots + return 0, nil + } + return 0, fmt.Errorf("cannot open snapshots directory: %v", err) + } + defer dir.Close() + + var maxSetID uint64 + + var readErr error + for readErr == nil { + var names []string + // note os.Readdirnames can return a non-empty names and a non-nil err + names, readErr = dirNames(dir, 100) + for _, name := range names { + if ok, setID := isSnapshotFilename(name); ok { + if setID > maxSetID { + maxSetID = setID + } + } + } + } + if readErr != nil && readErr != io.EOF { + return 0, readErr + } + return maxSetID, nil +} + // Iter loops over all snapshots in the snapshots directory, applying the given // function to each. The snapshot will be closed after the function returns. If // the function returns an error, iteration is stopped (and if the error isn't @@ -90,6 +128,7 @@ } defer dir.Close() + importsInProgress := map[uint64]bool{} var names []string var readErr error for readErr == nil && err == nil { @@ -100,8 +139,36 @@ break } + // filter out non-snapshot directory entries + ok, setID := isSnapshotFilename(name) + if !ok { + continue + } + // keep track of in-progress in a map as well + // to avoid races. E.g.: + // 1. The dirNnames() are read + // 2. 99_some-snap_1.0_x1.zip is returned + // 3. the code checks if 99_importing is there, + // it is so 99_some-snap is skipped + // 4. other snapshots are examined + // 5. in-parallel 99_importing finishes + // 7. 99_other-snap_1.0_x1.zip is now examined + // 8. code checks if 99_importing is there, but it + // is no longer there because import + // finished in the meantime. We still + // want to not call the callback with + // 99_other-snap or the callback would get + // an incomplete view about 99_snapshot. + if importsInProgress[setID] { + continue + } + if importInProgressFor(setID) { + importsInProgress[setID] = true + continue + } + filename := filepath.Join(dirs.SnapshotsDir, name) - reader, openError := backendOpen(filename) + reader, openError := backendOpen(filename, setID) // reader can be non-nil even when openError is not nil (in // which case reader.Broken will have a reason). f can // check and either ignore or return an error when @@ -164,6 +231,36 @@ return filepath.Join(dirs.SnapshotsDir, fmt.Sprintf("%d_%s_%s_%s.zip", snapshot.SetID, snapshot.Snap, snapshot.Version, snapshot.Revision)) } +// isSnapshotFilename checks if the given filePath is a snapshot file name, i.e. +// if it starts with a numeric set id and ends with .zip extension; +// filePath can be just a file name, or a full path. +func isSnapshotFilename(filePath string) (ok bool, setID uint64) { + fname := filepath.Base(filePath) + // XXX: we could use a regexp here to match very precisely all the elements + // of the filename following Filename() above, but perhaps it's better no to + // go overboard with it in case the format evolves in the future. Only check + // if the name starts with a set-id and ends with .zip. + // + // Filename is "__version_revision.zip", e.g. "16_snapcraft_4.2_5407.zip" + ext := filepath.Ext(fname) + if ext != ".zip" { + return false, 0 + } + parts := strings.SplitN(fname, "_", 2) + if len(parts) != 2 { + return false, 0 + } + // invalid: no parts following _ + if parts[1] == ext { + return false, 0 + } + id, err := strconv.Atoi(parts[0]) + if err != nil { + return false, 0 + } + return true, uint64(id) +} + // EstimateSnapshotSize calculates estimated size of the snapshot. func EstimateSnapshotSize(si *snap.Info, usernames []string) (uint64, error) { var total uint64 @@ -307,6 +404,7 @@ tarArgs := []string{ "--create", "--sparse", "--gzip", + "--format", "gnu", "--directory", parent, } @@ -375,6 +473,188 @@ return nil } +var ErrCannotCancel = errors.New("cannot cancel: import already finished") + +// importTransaction keeps track of the given snapshot ID import and +// ensures it can be committed/cancelled in an atomic way. +// +// Start() must be called before the first data is imported. When the +// import is successful Commit() should be called. +// +// Cancel() will cancel the given import and cleanup. It's always safe +// to defer a Cancel() it will just return a "ErrCannotCancel" after +// a commit. +type importTransaction struct { + id uint64 + committed bool +} + +func newImportTransaction(setID uint64) *importTransaction { + return &importTransaction{id: setID} +} + +func (t *importTransaction) importInProgressFilesGlob() string { + return filepath.Join(dirs.SnapshotsDir, fmt.Sprintf("%d_*.zip", t.id)) +} + +// Start marks the start of a snapshot import +func (t *importTransaction) Start() error { + return ioutil.WriteFile(importInProgressFilepath(t.id), nil, 0644) +} + +// InProgress returns true if there is an import for this transactions +// snapshot ID already. +func (t *importTransaction) InProgress() bool { + return osutil.FileExists(importInProgressFilepath(t.id)) +} + +func importInProgressFilepath(setID uint64) string { + return filepath.Join(dirs.SnapshotsDir, fmt.Sprintf("%d_importing", setID)) +} + +func importInProgressFor(setID uint64) bool { + return osutil.FileExists(importInProgressFilepath(setID)) +} + +// Cancel cancels a snapshot import and cleanups any files on disk belonging +// to this snapshot ID. +func (t *importTransaction) Cancel() error { + if t.committed { + return ErrCannotCancel + } + inProgressImports, err := filepath.Glob(t.importInProgressFilesGlob()) + if err != nil { + return err + } + var errs []error + for _, p := range inProgressImports { + if err := os.Remove(p); err != nil { + errs = append(errs, err) + } + } + if len(errs) > 0 { + buf := bytes.NewBuffer(nil) + for _, err := range errs { + fmt.Fprintf(buf, " - %v\n", err) + } + return fmt.Errorf("cannot cancel import for set id %d:\n%s", t.id, buf.String()) + } + return nil +} + +// Commit will commit a given transaction +func (t *importTransaction) Commit() error { + if err := os.Remove(importInProgressFilepath(t.id)); err != nil { + return err + } + t.committed = true + return nil +} + +// Import a snapshot from the export file format +func Import(ctx context.Context, id uint64, r io.Reader) (snapNames []string, err error) { + errPrefix := fmt.Sprintf("cannot import snapshot %d", id) + + tr := newImportTransaction(id) + if tr.InProgress() { + return nil, fmt.Errorf("%s: already in progress for this set id", errPrefix) + } + if err := tr.Start(); err != nil { + return nil, err + } + // Cancel once Committed is a NOP + defer tr.Cancel() + + // Unpack and validate the streamed data + snapNames, err = unpackVerifySnapshotImport(r, id) + if err != nil { + return nil, fmt.Errorf("%s: %v", errPrefix, err) + } + if err := tr.Commit(); err != nil { + return nil, err + } + + return snapNames, nil +} + +func writeOneSnapshotFile(targetPath string, tr io.Reader) error { + t, err := os.OpenFile(targetPath, os.O_CREATE|os.O_RDWR, 0600) + if err != nil { + return fmt.Errorf("cannot create snapshot file %q: %v", targetPath, err) + } + defer t.Close() + + if _, err := io.Copy(t, tr); err != nil { + return fmt.Errorf("cannot write snapshot file %q: %v", targetPath, err) + } + return nil +} + +func unpackVerifySnapshotImport(r io.Reader, realSetID uint64) (snapNames []string, err error) { + var exportFound bool + + tr := tar.NewReader(r) + var tarErr error + var header *tar.Header + + for tarErr == nil { + header, tarErr = tr.Next() + if tarErr == io.EOF { + break + } + switch { + case tarErr != nil: + return nil, fmt.Errorf("cannot read snapshot import: %v", tarErr) + case header == nil: + // should not happen + return nil, fmt.Errorf("tar header not found") + case header.Typeflag == tar.TypeDir: + return nil, errors.New("unexpected directory in import file") + } + + if header.Name == "export.json" { + // XXX: read into memory and validate once we + // hashes in export.json + exportFound = true + continue + } + + // Format of the snapshot import is: + // $setID_..... + // But because the setID is local this will not be correct + // for our system and we need to discard this setID. + // + // So chop off the incorrect (old) setID and just use + // the rest that is still valid. + l := strings.SplitN(header.Name, "_", 2) + if len(l) != 2 { + return nil, fmt.Errorf("unexpected filename in import stream: %v", header.Name) + } + targetPath := path.Join(dirs.SnapshotsDir, fmt.Sprintf("%d_%s", realSetID, l[1])) + if err := writeOneSnapshotFile(targetPath, tr); err != nil { + return snapNames, err + } + + r, err := backendOpen(targetPath, realSetID) + if err != nil { + return snapNames, fmt.Errorf("cannot open snapshot: %v", err) + } + err = r.Check(context.TODO(), nil) + r.Close() + snapNames = append(snapNames, r.Snap) + if err != nil { + return snapNames, fmt.Errorf("validation failed for %q: %v", targetPath, err) + } + } + + if !exportFound { + return nil, fmt.Errorf("no export.json file in uploaded data") + } + // XXX: validate using the unmarshalled export.json hashes here + + return snapNames, nil +} + type exportMetadata struct { Format int `json:"format"` Date time.Time `json:"date"` diff -Nru snapd-2.47.1+20.10.1build1/overlord/snapshotstate/backend/backend_test.go snapd-2.48+21.04/overlord/snapshotstate/backend/backend_test.go --- snapd-2.47.1+20.10.1build1/overlord/snapshotstate/backend/backend_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/overlord/snapshotstate/backend/backend_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -20,9 +20,12 @@ package backend_test import ( + "archive/tar" "archive/zip" "bytes" "context" + "crypto" + "encoding/json" "errors" "fmt" "io" @@ -30,6 +33,7 @@ "os" "os/exec" "os/user" + "path" "path/filepath" "sort" "strings" @@ -41,6 +45,7 @@ "github.com/snapcore/snapd/client" "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/logger" + "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/osutil/sys" "github.com/snapcore/snapd/overlord/snapshotstate/backend" "github.com/snapcore/snapd/snap" @@ -138,6 +143,59 @@ return keys } +func (s *snapshotSuite) TestLastSnapshotID(c *check.C) { + // LastSnapshotSetID is happy without any snapshots + setID, err := backend.LastSnapshotSetID() + c.Assert(err, check.IsNil) + c.Check(setID, check.Equals, uint64(0)) + + // create snapshots dir and dummy snapshots + os.MkdirAll(dirs.SnapshotsDir, os.ModePerm) + for _, name := range []string{ + "9_some-snap-1.zip", "1234_not-a-snapshot", "12_other-snap.zip", "3_foo.zip", + } { + c.Assert(ioutil.WriteFile(filepath.Join(dirs.SnapshotsDir, name), []byte{}, 0644), check.IsNil) + } + setID, err = backend.LastSnapshotSetID() + c.Assert(err, check.IsNil) + c.Check(setID, check.Equals, uint64(12)) +} + +func (s *snapshotSuite) TestLastSnapshotIDErrorOnDirNames(c *check.C) { + // we need snapshots dir, otherwise LastSnapshotSetID exits early. + c.Assert(os.MkdirAll(dirs.SnapshotsDir, os.ModePerm), check.IsNil) + + defer backend.MockDirNames(func(*os.File, int) ([]string, error) { + return nil, fmt.Errorf("fail") + })() + setID, err := backend.LastSnapshotSetID() + c.Assert(err, check.ErrorMatches, "fail") + c.Check(setID, check.Equals, uint64(0)) +} + +func (s *snapshotSuite) TestIsSnapshotFilename(c *check.C) { + tests := []struct { + name string + valid bool + setID uint64 + }{ + {"1_foo.zip", true, 1}, + {"14_hello-world_6.4_29.zip", true, 14}, + {"1_.zip", false, 0}, + {"1_foo.zip.bak", false, 0}, + {"foo_1_foo.zip", false, 0}, + {"foo_bar_baz.zip", false, 0}, + {"", false, 0}, + {"1_", false, 0}, + } + + for _, t := range tests { + ok, setID := backend.IsSnapshotFilename(t.name) + c.Check(ok, check.Equals, t.valid, check.Commentf("fail: %s", t.name)) + c.Check(setID, check.Equals, t.setID, check.Commentf("fail: %s", t.name)) + } +} + func (s *snapshotSuite) TestIterBailsIfContextDone(c *check.C) { ctx, cancel := context.WithCancel(context.Background()) cancel() @@ -166,7 +224,7 @@ return []string{"hello"}, nil })() triedToOpenSnapshot := false - defer backend.MockOpen(func(string) (*backend.Reader, error) { + defer backend.MockOpen(func(string, uint64) (*backend.Reader, error) { triedToOpenSnapshot = true return nil, nil })() @@ -217,10 +275,10 @@ if readNames > 1 { return nil, io.EOF } - return []string{"hello"}, nil + return []string{"1_hello.zip"}, nil })() triedToOpenSnapshot := false - defer backend.MockOpen(func(string) (*backend.Reader, error) { + defer backend.MockOpen(func(string, uint64) (*backend.Reader, error) { triedToOpenSnapshot = true return nil, os.ErrInvalid })() @@ -237,7 +295,7 @@ c.Check(triedToOpenDir, check.Equals, true) c.Check(readNames, check.Equals, 2) c.Check(triedToOpenSnapshot, check.Equals, true) - c.Check(logbuf.String(), check.Matches, `(?m).* Cannot open snapshot "hello": invalid argument.`) + c.Check(logbuf.String(), check.Matches, `(?m).* Cannot open snapshot "1_hello.zip": invalid argument.`) c.Check(calledF, check.Equals, false) } @@ -255,13 +313,14 @@ if readNames > 1 { return nil, io.EOF } - return []string{"hello"}, nil + return []string{"1_hello.zip"}, nil })() triedToOpenSnapshot := false - defer backend.MockOpen(func(string) (*backend.Reader, error) { + defer backend.MockOpen(func(string, uint64) (*backend.Reader, error) { triedToOpenSnapshot = true // NOTE non-nil reader, and error, returned r := backend.Reader{} + r.SetID = 1 r.Broken = "xyzzy" return &r, os.ErrInvalid })() @@ -297,10 +356,10 @@ if readNames > 1 { return nil, io.EOF } - return []string{"hello"}, nil + return []string{"42_hello.zip"}, nil })() triedToOpenSnapshot := false - defer backend.MockOpen(func(string) (*backend.Reader, error) { + defer backend.MockOpen(func(string, uint64) (*backend.Reader, error) { triedToOpenSnapshot = true r := backend.Reader{} r.SetID = 42 @@ -324,10 +383,98 @@ c.Check(calledF, check.Equals, true) } +func readerForFilename(fname string, c *check.C) *backend.Reader { + var snapname string + var id uint64 + fn := strings.TrimSuffix(filepath.Base(fname), ".zip") + _, err := fmt.Sscanf(fn, "%d_%s", &id, &snapname) + c.Assert(err, check.IsNil, check.Commentf(fn)) + f, err := os.Open(os.DevNull) + c.Assert(err, check.IsNil, check.Commentf(fn)) + return &backend.Reader{ + File: f, + Snapshot: client.Snapshot{ + SetID: id, + Snap: snapname, + }, + } +} + +func (s *snapshotSuite) TestIterIgnoresSnapshotsWithInvalidNames(c *check.C) { + logbuf, restore := logger.MockLogger() + defer restore() + + defer backend.MockOsOpen(func(string) (*os.File, error) { + return new(os.File), nil + })() + readNames := 0 + defer backend.MockDirNames(func(*os.File, int) ([]string, error) { + readNames++ + if readNames > 1 { + return nil, io.EOF + } + return []string{ + "_foo.zip", + "43_bar.zip", + "foo_bar.zip", + "bar.", + }, nil + })() + defer backend.MockOpen(func(fname string, setID uint64) (*backend.Reader, error) { + return readerForFilename(fname, c), nil + })() + + var calledF int + f := func(snapshot *backend.Reader) error { + calledF++ + c.Check(snapshot.SetID, check.Equals, uint64(43)) + return nil + } + + err := backend.Iter(context.Background(), f) + c.Check(err, check.IsNil) + c.Check(logbuf.String(), check.Equals, "") + c.Check(calledF, check.Equals, 1) +} + +func (s *snapshotSuite) TestIterSetIDoverride(c *check.C) { + if os.Geteuid() == 0 { + c.Skip("this test cannot run as root (runuser will fail)") + } + logger.SimpleSetup() + + epoch := snap.E("42*") + info := &snap.Info{SideInfo: snap.SideInfo{RealName: "hello-snap", Revision: snap.R(42), SnapID: "hello-id"}, Version: "v1.33", Epoch: epoch} + cfg := map[string]interface{}{"some-setting": false} + + shw, err := backend.Save(context.TODO(), 12, info, cfg, []string{"snapuser"}, &backend.Flags{}) + c.Assert(err, check.IsNil) + c.Check(shw.SetID, check.Equals, uint64(12)) + + snapshotPath := filepath.Join(dirs.SnapshotsDir, "12_hello-snap_v1.33_42.zip") + c.Check(backend.Filename(shw), check.Equals, snapshotPath) + c.Check(hashkeys(shw), check.DeepEquals, []string{"archive.tgz", "user/snapuser.tgz"}) + + // rename the snapshot, verify that set id from the filename is used by the reader. + c.Assert(os.Rename(snapshotPath, filepath.Join(dirs.SnapshotsDir, "33_hello.zip")), check.IsNil) + + var calledF int + f := func(snapshot *backend.Reader) error { + calledF++ + c.Check(snapshot.SetID, check.Equals, uint64(uint(33))) + c.Check(snapshot.Snap, check.Equals, "hello-snap") + return nil + } + + c.Assert(backend.Iter(context.Background(), f), check.IsNil) + c.Check(calledF, check.Equals, 1) +} + func (s *snapshotSuite) TestList(c *check.C) { logbuf, restore := logger.MockLogger() defer restore() defer backend.MockOsOpen(func(string) (*os.File, error) { return new(os.File), nil })() + readNames := 0 defer backend.MockDirNames(func(*os.File, int) ([]string, error) { readNames++ @@ -335,15 +482,16 @@ return nil, io.EOF } return []string{ - fmt.Sprintf("%d_foo", readNames), - fmt.Sprintf("%d_bar", readNames), - fmt.Sprintf("%d_baz", readNames), + fmt.Sprintf("%d_foo.zip", readNames), + fmt.Sprintf("%d_bar.zip", readNames), + fmt.Sprintf("%d_baz.zip", readNames), }, nil })() - defer backend.MockOpen(func(fn string) (*backend.Reader, error) { + defer backend.MockOpen(func(fn string, setID uint64) (*backend.Reader, error) { var id uint64 var snapname string - fn = filepath.Base(fn) + c.Assert(strings.HasSuffix(fn, ".zip"), check.Equals, true) + fn = strings.TrimSuffix(filepath.Base(fn), ".zip") _, err := fmt.Sscanf(fn, "%d_%s", &id, &snapname) c.Assert(err, check.IsNil, check.Commentf(fn)) f, err := os.Open(os.DevNull) @@ -509,7 +657,7 @@ c.Assert(shs, check.HasLen, 1) c.Assert(shs[0].Snapshots, check.HasLen, 1) - shr, err := backend.Open(backend.Filename(shw)) + shr, err := backend.Open(backend.Filename(shw), backend.ExtractFnameSetID) c.Assert(err, check.IsNil) defer shr.Close() @@ -555,6 +703,30 @@ } } +func (s *snapshotSuite) TestOpenSetIDoverride(c *check.C) { + if os.Geteuid() == 0 { + c.Skip("this test cannot run as root (runuser will fail)") + } + logger.SimpleSetup() + + epoch := snap.E("42*") + info := &snap.Info{SideInfo: snap.SideInfo{RealName: "hello-snap", Revision: snap.R(42), SnapID: "hello-id"}, Version: "v1.33", Epoch: epoch} + cfg := map[string]interface{}{"some-setting": false} + + shw, err := backend.Save(context.TODO(), 12, info, cfg, []string{"snapuser"}, &backend.Flags{}) + c.Assert(err, check.IsNil) + c.Check(shw.SetID, check.Equals, uint64(12)) + + c.Check(backend.Filename(shw), check.Equals, filepath.Join(dirs.SnapshotsDir, "12_hello-snap_v1.33_42.zip")) + c.Check(hashkeys(shw), check.DeepEquals, []string{"archive.tgz", "user/snapuser.tgz"}) + + shr, err := backend.Open(backend.Filename(shw), 99) + c.Assert(err, check.IsNil) + defer shr.Close() + + c.Check(shr.SetID, check.Equals, uint64(99)) +} + func (s *snapshotSuite) TestRestoreRoundtripDifferentRevision(c *check.C) { if os.Geteuid() == 0 { c.Skip("this test cannot run as root (runuser will fail)") @@ -569,7 +741,7 @@ c.Assert(err, check.IsNil) c.Check(shw.Revision, check.Equals, info.Revision) - shr, err := backend.Open(backend.Filename(shw)) + shr, err := backend.Open(backend.Filename(shw), backend.ExtractFnameSetID) c.Assert(err, check.IsNil) defer shr.Close() @@ -720,6 +892,152 @@ c.Check(strings.TrimSpace(logbuf.String()), check.Matches, ".* No user wrapper found.*") } +func (s *snapshotSuite) TestImport(c *check.C) { + tempdir := c.MkDir() + + // create snapshot export file + tarFile1 := path.Join(tempdir, "exported1.snapshot") + err := createTestExportFile(tarFile1, &createTestExportFlags{exportJSON: true}) + c.Check(err, check.IsNil) + + // create an exported snapshot with missing export.json + tarFile2 := path.Join(tempdir, "exported2.snapshot") + err = createTestExportFile(tarFile2, &createTestExportFlags{}) + c.Check(err, check.IsNil) + + // create invalid exported file + tarFile3 := path.Join(tempdir, "exported3.snapshot") + err = ioutil.WriteFile(tarFile3, []byte("invalid"), 0755) + c.Check(err, check.IsNil) + + // create an exported snapshot with a directory + tarFile4 := path.Join(tempdir, "exported4.snapshot") + flags := &createTestExportFlags{ + exportJSON: true, + withDir: true, + } + err = createTestExportFile(tarFile4, flags) + c.Check(err, check.IsNil) + + type tableT struct { + setID uint64 + filename string + inProgress bool + error string + } + + table := []tableT{ + {14, tarFile1, false, ""}, + {14, tarFile2, false, "cannot import snapshot 14: no export.json file in uploaded data"}, + {14, tarFile3, false, "cannot import snapshot 14: cannot read snapshot import: unexpected EOF"}, + {14, tarFile4, false, "cannot import snapshot 14: unexpected directory in import file"}, + {14, tarFile1, true, "cannot import snapshot 14: already in progress for this set id"}, + } + + for i, t := range table { + comm := check.Commentf("%d: %d %s", i, t.setID, t.filename) + + // reset + err = os.RemoveAll(dirs.SnapshotsDir) + c.Assert(err, check.IsNil, comm) + err := os.MkdirAll(dirs.SnapshotsDir, 0700) + c.Assert(err, check.IsNil, comm) + importingFile := filepath.Join(dirs.SnapshotsDir, fmt.Sprintf("%d_importing", t.setID)) + if t.inProgress { + err = ioutil.WriteFile(importingFile, nil, 0644) + c.Assert(err, check.IsNil, comm) + } else { + err = os.RemoveAll(importingFile) + c.Assert(err, check.IsNil, comm) + } + + f, err := os.Open(t.filename) + c.Assert(err, check.IsNil, comm) + defer f.Close() + + snapNames, err := backend.Import(context.Background(), t.setID, f) + if t.error != "" { + c.Check(err, check.ErrorMatches, t.error, comm) + continue + } + c.Check(err, check.IsNil, comm) + sort.Strings(snapNames) + c.Check(snapNames, check.DeepEquals, []string{"bar", "baz", "foo"}) + + dir, err := os.Open(dirs.SnapshotsDir) + c.Assert(err, check.IsNil, comm) + defer dir.Close() + names, err := dir.Readdirnames(100) + c.Assert(err, check.IsNil, comm) + c.Check(len(names), check.Equals, 3, comm) + } +} + +func (s *snapshotSuite) TestImportCheckErorr(c *check.C) { + err := os.MkdirAll(dirs.SnapshotsDir, 0755) + c.Assert(err, check.IsNil) + + // create snapshot export file + tarFile1 := path.Join(c.MkDir(), "exported1.snapshot") + flags := &createTestExportFlags{ + exportJSON: true, + corruptChecksum: true, + } + err = createTestExportFile(tarFile1, flags) + c.Assert(err, check.IsNil) + + f, err := os.Open(tarFile1) + c.Assert(err, check.IsNil) + _, err = backend.Import(context.Background(), 14, f) + c.Assert(err, check.ErrorMatches, `cannot import snapshot 14: validation failed for .+/14_foo_1.0_199.zip": snapshot entry "archive.tgz" expected hash \(d5ef563…\) does not match actual \(6655519…\)`) +} + +func (s *snapshotSuite) TestImportExportRoundtrip(c *check.C) { + err := os.MkdirAll(dirs.SnapshotsDir, 0755) + c.Assert(err, check.IsNil) + + ctx := context.TODO() + + epoch := snap.E("42*") + info := &snap.Info{SideInfo: snap.SideInfo{RealName: "hello-snap", Revision: snap.R(42), SnapID: "hello-id"}, Version: "v1.33", Epoch: epoch} + cfg := map[string]interface{}{"some-setting": false} + shID := uint64(12) + + shw, err := backend.Save(ctx, shID, info, cfg, []string{"snapuser"}, &backend.Flags{}) + c.Assert(err, check.IsNil) + c.Check(shw.SetID, check.Equals, shID) + + c.Check(backend.Filename(shw), check.Equals, filepath.Join(dirs.SnapshotsDir, "12_hello-snap_v1.33_42.zip")) + c.Check(hashkeys(shw), check.DeepEquals, []string{"archive.tgz", "user/snapuser.tgz"}) + + export, err := backend.NewSnapshotExport(ctx, shw.SetID) + c.Assert(err, check.IsNil) + c.Assert(export.Init(), check.IsNil) + + buf := bytes.NewBuffer(nil) + c.Assert(export.StreamTo(buf), check.IsNil) + c.Check(buf.Len(), check.Equals, int(export.Size())) + + // now import it + c.Assert(os.Remove(filepath.Join(dirs.SnapshotsDir, "12_hello-snap_v1.33_42.zip")), check.IsNil) + + names, err := backend.Import(ctx, 123, buf) + c.Assert(err, check.IsNil) + c.Check(names, check.DeepEquals, []string{"hello-snap"}) + + sets, err := backend.List(ctx, 0, nil) + c.Assert(err, check.IsNil) + c.Assert(sets, check.HasLen, 1) + c.Check(sets[0].ID, check.Equals, uint64(123)) + + rdr, err := backend.Open(filepath.Join(dirs.SnapshotsDir, "123_hello-snap_v1.33_42.zip"), backend.ExtractFnameSetID) + defer rdr.Close() + c.Check(err, check.IsNil) + c.Check(rdr.SetID, check.Equals, uint64(123)) + c.Check(rdr.Snap, check.Equals, "hello-snap") + c.Check(rdr.IsValid(), check.Equals, true) +} + func (s *snapshotSuite) TestEstimateSnapshotSize(c *check.C) { restore := backend.MockUsersForUsernames(func(usernames []string) ([]*user.User, error) { return []*user.User{{HomeDir: filepath.Join(s.root, "home/user1")}}, nil @@ -876,3 +1194,161 @@ c.Assert(err, check.ErrorMatches, "no snapshot data found for 5") c.Assert(se, check.IsNil) } + +type createTestExportFlags struct { + exportJSON bool + withDir bool + corruptChecksum bool +} + +func createTestExportFile(filename string, flags *createTestExportFlags) error { + tf, err := os.Create(filename) + if err != nil { + return err + } + defer tf.Close() + tw := tar.NewWriter(tf) + defer tw.Close() + + for _, s := range []string{"foo", "bar", "baz"} { + fname := fmt.Sprintf("5_%s_1.0_199.zip", s) + + buf := bytes.NewBuffer(nil) + zipW := zip.NewWriter(buf) + defer zipW.Close() + + sha := map[string]string{} + + // create dummy archive.tgz + archiveWriter, err := zipW.CreateHeader(&zip.FileHeader{Name: "archive.tgz"}) + if err != nil { + return err + } + var sz osutil.Sizer + hasher := crypto.SHA3_384.New() + out := io.MultiWriter(archiveWriter, hasher, &sz) + if _, err := out.Write([]byte(s)); err != nil { + return err + } + + if flags.corruptChecksum { + hasher.Write([]byte{0}) + } + sha["archive.tgz"] = fmt.Sprintf("%x", hasher.Sum(nil)) + + snapshot := backend.MockSnapshot(5, s, snap.Revision{N: 199}, sz.Size(), sha) + + // create meta.json + metaWriter, err := zipW.Create("meta.json") + if err != nil { + return err + } + hasher = crypto.SHA3_384.New() + enc := json.NewEncoder(io.MultiWriter(metaWriter, hasher)) + if err := enc.Encode(snapshot); err != nil { + return err + } + + // write meta.sha3_384 + metaSha3Writer, err := zipW.Create("meta.sha3_384") + if err != nil { + return err + } + fmt.Fprintf(metaSha3Writer, "%x\n", hasher.Sum(nil)) + zipW.Close() + + hdr := &tar.Header{ + Name: fname, + Mode: 0644, + Size: int64(buf.Len()), + } + if err := tw.WriteHeader(hdr); err != nil { + return err + } + if _, err := tw.Write(buf.Bytes()); err != nil { + return err + } + } + + if flags.withDir { + hdr := &tar.Header{ + Name: dirs.SnapshotsDir, + Mode: 0700, + Size: int64(0), + Typeflag: tar.TypeDir, + } + if err := tw.WriteHeader(hdr); err != nil { + return err + } + if _, err = tw.Write([]byte("")); err != nil { + return nil + } + } + + if flags.exportJSON { + exp := fmt.Sprintf(`{"format":1, "date":"%s"}`, time.Now().Format(time.RFC3339)) + hdr := &tar.Header{ + Name: "export.json", + Mode: 0644, + Size: int64(len(exp)), + } + if err := tw.WriteHeader(hdr); err != nil { + return err + } + if _, err = tw.Write([]byte(exp)); err != nil { + return nil + } + } + + return nil +} + +func makeMockSnapshotZipContent(c *check.C) []byte { + buf := bytes.NewBuffer(nil) + zipW := zip.NewWriter(buf) + + // create dummy archive.tgz + archiveWriter, err := zipW.CreateHeader(&zip.FileHeader{Name: "archive.tgz"}) + c.Assert(err, check.IsNil) + _, err = archiveWriter.Write([]byte("mock archive.tgz content")) + c.Assert(err, check.IsNil) + + // create dummy meta.json + archiveWriter, err = zipW.CreateHeader(&zip.FileHeader{Name: "meta.json"}) + c.Assert(err, check.IsNil) + _, err = archiveWriter.Write([]byte("{}")) + c.Assert(err, check.IsNil) + + zipW.Close() + return buf.Bytes() +} + +func (s *snapshotSuite) TestIterWithMockedSnapshotFiles(c *check.C) { + err := os.MkdirAll(dirs.SnapshotsDir, 0755) + c.Assert(err, check.IsNil) + + fn := "1_hello_1.0_x1.zip" + err = ioutil.WriteFile(filepath.Join(dirs.SnapshotsDir, fn), makeMockSnapshotZipContent(c), 0644) + c.Assert(err, check.IsNil) + + callbackCalled := 0 + f := func(snapshot *backend.Reader) error { + callbackCalled++ + return nil + } + + err = backend.Iter(context.Background(), f) + c.Check(err, check.IsNil) + c.Check(callbackCalled, check.Equals, 1) + + // now pretend we are importing snapshot id 1 + callbackCalled = 0 + fn = "1_importing" + err = ioutil.WriteFile(filepath.Join(dirs.SnapshotsDir, fn), nil, 0644) + c.Assert(err, check.IsNil) + + // and while importing Iter() does not call the callback + err = backend.Iter(context.Background(), f) + c.Check(err, check.IsNil) + c.Check(callbackCalled, check.Equals, 0) +} diff -Nru snapd-2.47.1+20.10.1build1/overlord/snapshotstate/backend/export_test.go snapd-2.48+21.04/overlord/snapshotstate/backend/export_test.go --- snapd-2.47.1+20.10.1build1/overlord/snapshotstate/backend/export_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/overlord/snapshotstate/backend/export_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -24,13 +24,17 @@ "os/user" "time" + "github.com/snapcore/snapd/client" "github.com/snapcore/snapd/osutil/sys" + "github.com/snapcore/snapd/snap" ) var ( AddDirToZip = addDirToZip TarAsUser = tarAsUser PickUserWrapper = pickUserWrapper + + IsSnapshotFilename = isSnapshotFilename ) func MockIsTesting(newIsTesting bool) func() { @@ -65,7 +69,7 @@ } } -func MockOpen(newOpen func(string) (*Reader, error)) func() { +func MockOpen(newOpen func(string, uint64) (*Reader, error)) func() { oldOpen := backendOpen backendOpen = newOpen return func() { @@ -112,3 +116,17 @@ timeNow = oldTimeNow } } + +func MockSnapshot(setID uint64, snapName string, revision snap.Revision, size int64, shaSums map[string]string) *client.Snapshot { + return &client.Snapshot{ + SetID: setID, + Snap: snapName, + SnapID: "id", + Revision: revision, + Version: "1.0", + Epoch: snap.Epoch{}, + Time: timeNow(), + SHA3_384: shaSums, + Size: size, + } +} diff -Nru snapd-2.47.1+20.10.1build1/overlord/snapshotstate/backend/helpers.go snapd-2.48+21.04/overlord/snapshotstate/backend/helpers.go --- snapd-2.47.1+20.10.1build1/overlord/snapshotstate/backend/helpers.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/overlord/snapshotstate/backend/helpers.go 2020-11-19 16:51:02.000000000 +0000 @@ -111,9 +111,23 @@ for _, username := range usernames { usr, err := userLookup(username) if err != nil { - if !isUnknownUser(err) { - return nil, err - } + // Treat all non-nil errors as user.Unknown{User,Group}Error's, as + // currently Go's handling of returned errno from get{pw,gr}nam_r + // in the cgo implementation of user.Lookup is lacking, and thus + // user.Unknown{User,Group}Error is returned only when errno is 0 + // and the list of users/groups is empty, but as per the man page + // for get{pw,gr}nam_r, there are many other errno's that typical + // systems could return to indicate that the user/group wasn't + // found, however unfortunately the POSIX standard does not actually + // dictate what errno should be used to indicate "user/group not + // found", and so even if Go is more robust, it may not ever be + // fully robust. See from the man page: + // + // > It [POSIX.1-2001] does not call "not found" an error, hence + // > does not specify what value errno might have in this situation. + // > But that makes it impossible to recognize errors. + // + // See upstream Go issue: https://github.com/golang/go/issues/40334 u, e := userLookupId(username) if e != nil { // return first error, as it's usually clearer @@ -154,9 +168,24 @@ seen[st.Uid] = true usr, err := userLookupId(strconv.FormatUint(uint64(st.Uid), 10)) if err != nil { - if !isUnknownUser(err) { - return nil, err - } + // Treat all non-nil errors as user.Unknown{User,Group}Error's, as + // currently Go's handling of returned errno from get{pw,gr}nam_r + // in the cgo implementation of user.Lookup is lacking, and thus + // user.Unknown{User,Group}Error is returned only when errno is 0 + // and the list of users/groups is empty, but as per the man page + // for get{pw,gr}nam_r, there are many other errno's that typical + // systems could return to indicate that the user/group wasn't + // found, however unfortunately the POSIX standard does not actually + // dictate what errno should be used to indicate "user/group not + // found", and so even if Go is more robust, it may not ever be + // fully robust. See from the man page: + // + // > It [POSIX.1-2001] does not call "not found" an error, hence + // > does not specify what value errno might have in this situation. + // > But that makes it impossible to recognize errors. + // + // See upstream Go issue: https://github.com/golang/go/issues/40334 + continue } else { users = append(users, usr) } diff -Nru snapd-2.47.1+20.10.1build1/overlord/snapshotstate/backend/reader.go snapd-2.48+21.04/overlord/snapshotstate/backend/reader.go --- snapd-2.47.1+20.10.1build1/overlord/snapshotstate/backend/reader.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/overlord/snapshotstate/backend/reader.go 2020-11-19 16:51:02.000000000 +0000 @@ -42,6 +42,10 @@ "github.com/snapcore/snapd/strutil" ) +// ExtractFnameSetID can be passed to Open() to have set ID inferred from +// snapshot filename. +const ExtractFnameSetID = 0 + // A Reader is a snapshot that's been opened for reading. type Reader struct { *os.File @@ -50,13 +54,17 @@ // Open a Snapshot given its full filename. // +// The returned reader will have its setID set to the value of the argument, +// or inferred from the snapshot filename if ExtractFnameSetID constant is +// passed. +// // If the returned error is nil, the caller must close the reader (or // its file) when done with it. // // If the returned error is non-nil, the returned Reader will be nil, // *or* have a non-empty Broken; in the latter case its file will be // closed. -func Open(fn string) (reader *Reader, e error) { +func Open(fn string, setID uint64) (reader *Reader, e error) { f, err := os.Open(fn) if err != nil { return nil, err @@ -84,6 +92,18 @@ return nil, err } + if setID == ExtractFnameSetID { + // set id from the filename has the authority and overrides the one from + // meta file. + var ok bool + ok, setID = isSnapshotFilename(fn) + if !ok { + return nil, fmt.Errorf("not a snapshot filename: %q", fn) + } + } + + reader.SetID = setID + // OK, from here on we have a Snapshot if !reader.IsValid() { diff -Nru snapd-2.47.1+20.10.1build1/overlord/snapshotstate/export_test.go snapd-2.48+21.04/overlord/snapshotstate/export_test.go --- snapd-2.47.1+20.10.1build1/overlord/snapshotstate/export_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/overlord/snapshotstate/export_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -22,6 +22,7 @@ import ( "context" "encoding/json" + "io" "time" "github.com/snapcore/snapd/overlord/snapshotstate/backend" @@ -102,7 +103,7 @@ } } -func MockBackendOpen(f func(string) (*backend.Reader, error)) (restore func()) { +func MockBackendOpen(f func(string, uint64) (*backend.Reader, error)) (restore func()) { old := backendOpen backendOpen = f return func() { @@ -142,6 +143,14 @@ } } +func MockBackendImport(f func(context.Context, uint64, io.Reader) ([]string, error)) (restore func()) { + old := backendImport + backendImport = f + return func() { + backendImport = old + } +} + func MockBackendEstimateSnapshotSize(f func(*snap.Info, []string) (uint64, error)) (restore func()) { old := backendEstimateSnapshotSize backendEstimateSnapshotSize = f diff -Nru snapd-2.47.1+20.10.1build1/overlord/snapshotstate/snapshotmgr.go snapd-2.48+21.04/overlord/snapshotstate/snapshotmgr.go --- snapd-2.47.1+20.10.1build1/overlord/snapshotstate/snapshotmgr.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/overlord/snapshotstate/snapshotmgr.go 2020-11-19 16:51:02.000000000 +0000 @@ -44,6 +44,7 @@ configSetSnapConfig = config.SetSnapConfig backendOpen = backend.Open backendSave = backend.Save + backendImport = backend.Import backendRestore = (*backend.Reader).Restore // TODO: look into using an interface instead backendCheck = (*backend.Reader).Check backendRevert = (*backend.RestoreState).Revert // ditto @@ -246,7 +247,7 @@ } } - reader, err = backendOpen(snapshot.Filename) + reader, err = backendOpen(snapshot.Filename, backend.ExtractFnameSetID) if err != nil { return nil, nil, nil, fmt.Errorf("cannot open snapshot: %v", err) } @@ -361,7 +362,7 @@ return taskGetErrMsg(task, err, "snapshot") } - reader, err := backendOpen(snapshot.Filename) + reader, err := backendOpen(snapshot.Filename, backend.ExtractFnameSetID) if err != nil { return fmt.Errorf("cannot open snapshot: %v", err) } diff -Nru snapd-2.47.1+20.10.1build1/overlord/snapshotstate/snapshotmgr_test.go snapd-2.48+21.04/overlord/snapshotstate/snapshotmgr_test.go --- snapd-2.47.1+20.10.1build1/overlord/snapshotstate/snapshotmgr_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/overlord/snapshotstate/snapshotmgr_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -478,7 +478,7 @@ rs.task.Set("snapshot-setup", map[string]interface{}{ // interestingly restore doesn't use the set-id "snap": "a-snap", - "filename": "/some/file.zip", + "filename": "/some/1_file.zip", "users": []string{"a-user", "b-user"}, }) st.Unlock() @@ -497,7 +497,7 @@ rs.calls = append(rs.calls, "set config") return nil }), - snapshotstate.MockBackendOpen(func(string) (*backend.Reader, error) { + snapshotstate.MockBackendOpen(func(string, uint64) (*backend.Reader, error) { rs.calls = append(rs.calls, "open") return &backend.Reader{}, nil }), @@ -531,9 +531,11 @@ buf := json.RawMessage(`{"old": "conf"}`) return &buf, nil })() - defer snapshotstate.MockBackendOpen(func(filename string) (*backend.Reader, error) { + defer snapshotstate.MockBackendOpen(func(filename string, setID uint64) (*backend.Reader, error) { rs.calls = append(rs.calls, "open") - c.Check(filename, check.Equals, "/some/file.zip") + // set id 0 tells backend.Open to use set id from the filename + c.Check(setID, check.Equals, uint64(0)) + c.Check(filename, check.Equals, "/some/1_file.zip") return &backend.Reader{ Snapshot: client.Snapshot{Conf: map[string]interface{}{"hello": "there"}}, }, nil @@ -597,7 +599,7 @@ } func (rs *readerSuite) TestDoRestoreFailsOpenError(c *check.C) { - defer snapshotstate.MockBackendOpen(func(string) (*backend.Reader, error) { + defer snapshotstate.MockBackendOpen(func(string, uint64) (*backend.Reader, error) { rs.calls = append(rs.calls, "open") return nil, errors.New("bzzt") })() @@ -608,7 +610,7 @@ } func (rs *readerSuite) TestDoRestoreFailsUnserialisableSnapshotConfigError(c *check.C) { - defer snapshotstate.MockBackendOpen(func(string) (*backend.Reader, error) { + defer snapshotstate.MockBackendOpen(func(string, uint64) (*backend.Reader, error) { rs.calls = append(rs.calls, "open") return &backend.Reader{ Snapshot: client.Snapshot{Conf: map[string]interface{}{"hello": func() {}}}, @@ -675,9 +677,11 @@ } func (rs *readerSuite) TestDoCheck(c *check.C) { - defer snapshotstate.MockBackendOpen(func(filename string) (*backend.Reader, error) { + defer snapshotstate.MockBackendOpen(func(filename string, setID uint64) (*backend.Reader, error) { rs.calls = append(rs.calls, "open") - c.Check(filename, check.Equals, "/some/file.zip") + c.Check(filename, check.Equals, "/some/1_file.zip") + // set id 0 tells backend.Open to use set id from the filename + c.Check(setID, check.Equals, uint64(0)) return &backend.Reader{ Snapshot: client.Snapshot{Conf: map[string]interface{}{"hello": "there"}}, }, nil @@ -691,12 +695,11 @@ err := snapshotstate.DoCheck(rs.task, &tomb.Tomb{}) c.Assert(err, check.IsNil) c.Check(rs.calls, check.DeepEquals, []string{"open", "check"}) - } func (rs *readerSuite) TestDoRemove(c *check.C) { defer snapshotstate.MockOsRemove(func(filename string) error { - c.Check(filename, check.Equals, "/some/file.zip") + c.Check(filename, check.Equals, "/some/1_file.zip") rs.calls = append(rs.calls, "remove") return nil })() diff -Nru snapd-2.47.1+20.10.1build1/overlord/snapshotstate/snapshotstate.go snapd-2.48+21.04/overlord/snapshotstate/snapshotstate.go --- snapd-2.47.1+20.10.1build1/overlord/snapshotstate/snapshotstate.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/overlord/snapshotstate/snapshotstate.go 2020-11-19 16:51:02.000000000 +0000 @@ -23,6 +23,7 @@ "context" "encoding/json" "fmt" + "io" "sort" "time" @@ -52,13 +53,28 @@ } func newSnapshotSetID(st *state.State) (uint64, error) { - var lastSetID uint64 + var lastDiskSetID, lastStateSetID uint64 - err := st.Get("last-snapshot-set-id", &lastSetID) + // get last set id from state + err := st.Get("last-snapshot-set-id", &lastStateSetID) if err != nil && err != state.ErrNoState { return 0, err } + // get highest set id from the snapshots/ directory + lastDiskSetID, err = backend.LastSnapshotSetID() + if err != nil { + return 0, fmt.Errorf("cannot determine last snapshot set id: %v", err) + } + + // take the larger of the two numbers and store it back in the state. + // the value in state acts as an allocation of IDs for scheduled snapshots, + // they allocate set id early before any file gets created, so we cannot + // rely on disk only. + lastSetID := lastDiskSetID + if lastStateSetID > lastSetID { + lastSetID = lastStateSetID + } lastSetID++ st.Set("last-snapshot-set-id", lastSetID) @@ -276,6 +292,27 @@ // Note that the state must be locked by the caller. var List = backend.List +// XXX: Something needs to cleanup incomplete imports. This is conceptually +// very simple: on startup, do: +// for setID in *_importing: +// newImportTransaction(setID).Cancel() +// But it needs to happen early *before* anything can start new imports + +// Import a given snapshot ID from an exported snapshot +func Import(ctx context.Context, st *state.State, r io.Reader) (setID uint64, snapNames []string, err error) { + st.Lock() + setID, err = newSnapshotSetID(st) + st.Unlock() + if err != nil { + return 0, nil, err + } + snapNames, err = backendImport(ctx, setID, r) + if err != nil { + return 0, nil, err + } + return setID, snapNames, nil +} + // Save creates a taskset for taking snapshots of snaps' data. // Note that the state must be locked by the caller. func Save(st *state.State, instanceNames []string, users []string) (setID uint64, snapsSaved []string, ts *state.TaskSet, err error) { diff -Nru snapd-2.47.1+20.10.1build1/overlord/snapshotstate/snapshotstate_test.go snapd-2.48+21.04/overlord/snapshotstate/snapshotstate_test.go --- snapd-2.47.1+20.10.1build1/overlord/snapshotstate/snapshotstate_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/overlord/snapshotstate/snapshotstate_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -20,10 +20,13 @@ package snapshotstate_test import ( + "bytes" "context" "encoding/json" "errors" "fmt" + "io" + "io/ioutil" "os" "os/exec" "os/user" @@ -60,6 +63,7 @@ func (snapshotSuite) SetUpTest(c *check.C) { dirs.SetRootDir(c.MkDir()) + os.MkdirAll(dirs.SnapshotsDir, os.ModePerm) } func (snapshotSuite) TearDownTest(c *check.C) { @@ -70,13 +74,43 @@ st := state.New(nil) st.Lock() defer st.Unlock() + + // Disk last set id unset, state set id unset, use 1 sid, err := snapshotstate.NewSnapshotSetID(st) c.Assert(err, check.IsNil) c.Check(sid, check.Equals, uint64(1)) + var stateSetID uint64 + c.Assert(st.Get("last-snapshot-set-id", &stateSetID), check.IsNil) + c.Check(stateSetID, check.Equals, uint64(1)) + + c.Assert(ioutil.WriteFile(filepath.Join(dirs.SnapshotsDir, "9_some-snap-1.zip"), []byte{}, 0644), check.IsNil) + + // Disk last set id 9 > state set id 1, use 9++ = 10 + sid, err = snapshotstate.NewSnapshotSetID(st) + c.Assert(err, check.IsNil) + c.Check(sid, check.Equals, uint64(10)) + + c.Assert(st.Get("last-snapshot-set-id", &stateSetID), check.IsNil) + c.Check(stateSetID, check.Equals, uint64(10)) + + // Disk last set id 9 < state set id 10, use 10++ = 11 + sid, err = snapshotstate.NewSnapshotSetID(st) + c.Assert(err, check.IsNil) + c.Check(sid, check.Equals, uint64(11)) + + c.Assert(st.Get("last-snapshot-set-id", &stateSetID), check.IsNil) + c.Check(stateSetID, check.Equals, uint64(11)) + + c.Assert(ioutil.WriteFile(filepath.Join(dirs.SnapshotsDir, "88_some-snap-1.zip"), []byte{}, 0644), check.IsNil) + + // Disk last set id 88 > state set id 11, use 88++ = 89 sid, err = snapshotstate.NewSnapshotSetID(st) c.Assert(err, check.IsNil) - c.Check(sid, check.Equals, uint64(2)) + c.Check(sid, check.Equals, uint64(89)) + + c.Assert(st.Get("last-snapshot-set-id", &stateSetID), check.IsNil) + c.Check(stateSetID, check.Equals, uint64(89)) } func (snapshotSuite) TestAllActiveSnapNames(c *check.C) { @@ -1004,7 +1038,6 @@ o.AddManager(o.TaskRunner()) st.Lock() - defer st.Unlock() for i, name := range []string{"one-snap", "too-snap", "tri-snap"} { sideInfo := &snap.SideInfo{RealName: name, Revision: snap.R(i + 1)} @@ -1042,6 +1075,7 @@ c.Assert(o.Settle(5*time.Second), check.IsNil) st.Lock() c.Check(change.Err(), check.IsNil) + defer st.Unlock() // the three restores warn about the missing home (but no errors, no panics) for _, task := range change.Tasks() { @@ -1083,7 +1117,6 @@ o.AddManager(o.TaskRunner()) st.Lock() - defer st.Unlock() for i, name := range []string{"one-snap", "too-snap", "tri-snap"} { sideInfo := &snap.SideInfo{RealName: name, Revision: snap.R(i + 1)} @@ -1120,6 +1153,7 @@ c.Assert(o.Settle(5*time.Second), check.IsNil) st.Lock() c.Check(change.Err(), check.NotNil) + defer st.Unlock() tasks := change.Tasks() c.Check(tasks, check.HasLen, 3) @@ -1480,6 +1514,41 @@ c.Assert(du, check.Equals, time.Duration(0)) } +func (snapshotSuite) TestImportSnapshotHappy(c *check.C) { + st := state.New(nil) + + fakeSnapNames := []string{"baz", "bar", "foo"} + fakeSnapshotData := "fake-import-data" + + buf := bytes.NewBufferString(fakeSnapshotData) + restore := snapshotstate.MockBackendImport(func(ctx context.Context, id uint64, r io.Reader) ([]string, error) { + d, err := ioutil.ReadAll(r) + c.Assert(err, check.IsNil) + c.Check(fakeSnapshotData, check.Equals, string(d)) + return fakeSnapNames, nil + }) + defer restore() + + sid, names, err := snapshotstate.Import(context.TODO(), st, buf) + c.Assert(err, check.IsNil) + c.Check(sid, check.Equals, uint64(1)) + c.Check(names, check.DeepEquals, fakeSnapNames) +} + +func (snapshotSuite) TestImportSnapshotImportError(c *check.C) { + st := state.New(nil) + + restore := snapshotstate.MockBackendImport(func(ctx context.Context, id uint64, r io.Reader) ([]string, error) { + return nil, errors.New("some-error") + }) + defer restore() + + r := bytes.NewBufferString("faked-import-data") + sid, _, err := snapshotstate.Import(context.TODO(), st, r) + c.Assert(err, check.NotNil) + c.Assert(err.Error(), check.Equals, "some-error") + c.Check(sid, check.Equals, uint64(0)) +} func (snapshotSuite) TestEstimateSnapshotSize(c *check.C) { st := state.New(nil) st.Lock() diff -Nru snapd-2.47.1+20.10.1build1/overlord/snapstate/autorefresh.go snapd-2.48+21.04/overlord/snapstate/autorefresh.go --- snapd-2.47.1+20.10.1build1/overlord/snapstate/autorefresh.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/overlord/snapstate/autorefresh.go 2020-11-19 16:51:02.000000000 +0000 @@ -20,6 +20,7 @@ package snapstate import ( + "context" "fmt" "os" "time" @@ -35,6 +36,7 @@ "github.com/snapcore/snapd/strutil" "github.com/snapcore/snapd/timeutil" "github.com/snapcore/snapd/timings" + userclient "github.com/snapcore/snapd/usersession/client" ) // the default refresh pattern @@ -44,7 +46,7 @@ const maxPostponement = 60 * 24 * time.Hour // cannot inhibit refreshes for more than maxInhibition -const maxInhibition = 7 * 24 * time.Hour +const maxInhibition = 14 * 24 * time.Hour // hooks setup by devicestate var ( @@ -128,6 +130,31 @@ return holdTime, nil } +func (m *autoRefresh) ensureRefreshHoldAtLeast(duration time.Duration) error { + now := time.Now() + + // get the effective refresh hold and check if it is sooner than the + // specified duration in the future + effective, err := m.EffectiveRefreshHold() + if err != nil { + return err + } + + if effective.IsZero() || effective.Sub(now) < duration { + // the effective refresh hold is sooner than the desired delay, so + // move it out to the specified duration + holdTime := now.Add(duration) + tr := config.NewTransaction(m.state) + err := tr.Set("core", "refresh.hold", &holdTime) + if err != nil && !config.IsNoOption(err) { + return err + } + tr.Commit() + } + + return nil +} + // clearRefreshHold clears refresh.hold configuration. func (m *autoRefresh) clearRefreshHold() { tr := config.NewTransaction(m.state) @@ -254,54 +281,74 @@ logger.Debugf("Next refresh scheduled for %s.", m.nextRefresh.Format(time.RFC3339)) } - // should we hold back refreshes? - holdTime, err := m.EffectiveRefreshHold() + held, holdTime, err := m.isRefreshHeld(refreshSchedule) if err != nil { return err } - if holdTime.After(now) { - return nil - } - if !holdTime.IsZero() { - // expired hold case - m.clearRefreshHold() - if m.nextRefresh.Before(holdTime) { - // next refresh is obsolete, compute the next one - delta := timeutil.Next(refreshSchedule, holdTime, maxPostponement) - now = time.Now() - m.nextRefresh = now.Add(delta) - } - } // do refresh attempt (if needed) - if !m.nextRefresh.After(now) { - var can bool - can, err = m.canRefreshRespectingMetered(now, lastRefresh) - if err != nil { - return err - } - if !can { - // clear nextRefresh so that another refresh time is calculated - m.nextRefresh = time.Time{} - return nil + if !held { + if !holdTime.IsZero() { + // expired hold case + m.clearRefreshHold() + if m.nextRefresh.Before(holdTime) { + // next refresh is obsolete, compute the next one + delta := timeutil.Next(refreshSchedule, holdTime, maxPostponement) + now = time.Now() + m.nextRefresh = now.Add(delta) + } } - // Check that we have reasonable delays between attempts. - // If the store is under stress we need to make sure we do not - // hammer it too often - if !m.lastRefreshAttempt.IsZero() && m.lastRefreshAttempt.Add(refreshRetryDelay).After(time.Now()) { - return nil - } + // refresh is also "held" if the next time is in the future + // note that the two times here could be exactly equal, so we use + // !After() because that is true in the case that the next refresh is + // before now, and the next refresh is equal to now without requiring an + // or operation + if !m.nextRefresh.After(now) { + var can bool + can, err = m.canRefreshRespectingMetered(now, lastRefresh) + if err != nil { + return err + } + if !can { + // clear nextRefresh so that another refresh time is calculated + m.nextRefresh = time.Time{} + return nil + } - err = m.launchAutoRefresh() - if _, ok := err.(*httputil.PerstistentNetworkError); !ok { - m.nextRefresh = time.Time{} - } // else - refresh will be retried after refreshRetryDelay + // Check that we have reasonable delays between attempts. + // If the store is under stress we need to make sure we do not + // hammer it too often + if !m.lastRefreshAttempt.IsZero() && m.lastRefreshAttempt.Add(refreshRetryDelay).After(time.Now()) { + return nil + } + + err = m.launchAutoRefresh(refreshSchedule) + if _, ok := err.(*httputil.PersistentNetworkError); !ok { + m.nextRefresh = time.Time{} + } // else - refresh will be retried after refreshRetryDelay + } } return err } +// isRefreshHeld returns whether an auto-refresh is currently held back or not, +// as indicated by m.EffectiveRefreshHold(). +func (m *autoRefresh) isRefreshHeld(refreshSchedule []*timeutil.Schedule) (bool, time.Time, error) { + now := time.Now() + // should we hold back refreshes? + holdTime, err := m.EffectiveRefreshHold() + if err != nil { + return false, time.Time{}, err + } + if holdTime.After(now) { + return true, holdTime, nil + } + + return false, holdTime, nil +} + func (m *autoRefresh) ensureLastRefreshAnchor() { seedTime, _ := getTime(m.state, "seed-time") if !seedTime.IsZero() { @@ -384,7 +431,7 @@ } // launchAutoRefresh creates the auto-refresh taskset and a change for it. -func (m *autoRefresh) launchAutoRefresh() error { +func (m *autoRefresh) launchAutoRefresh(refreshSchedule []*timeutil.Schedule) error { perfTimings := timings.New(map[string]string{"ensure": "auto-refresh"}) tm := perfTimings.StartSpan("auto-refresh", "query store and setup auto-refresh change") defer func() { @@ -393,8 +440,30 @@ }() m.lastRefreshAttempt = time.Now() + + // NOTE: this will unlock and re-lock state for network ops updated, tasksets, err := AutoRefresh(auth.EnsureContextTODO(), m.state) - if _, ok := err.(*httputil.PerstistentNetworkError); ok { + + // TODO: we should have some way to lock just creating and starting changes, + // as that would alleviate this race condition we are guarding against + // with this check and probably would eliminate other similar race + // conditions elsewhere + + // re-check if the refresh is held because it could have been re-held and + // pushed back, in which case we need to abort the auto-refresh and wait + held, _, holdErr := m.isRefreshHeld(refreshSchedule) + if holdErr != nil { + return holdErr + } + + if held { + // then a request came in that pushed the refresh out, so we will need + // to try again later + logger.Noticef("Auto-refresh was delayed mid-way through launching, aborting to try again later") + return nil + } + + if _, ok := err.(*httputil.PersistentNetworkError); ok { logger.Noticef("Cannot prepare auto-refresh change due to a permanent network error: %s", err) return err } @@ -489,40 +558,66 @@ return t1, nil } +// asyncPendingRefreshNotification broadcasts desktop notification in a goroutine. +// +// This allows the, possibly slow, communication with each snapd session agent, +// to be performed without holding the snap state lock. +var asyncPendingRefreshNotification = func(context context.Context, client *userclient.Client, refreshInfo *userclient.PendingSnapRefreshInfo) { + go func() { + if err := client.PendingRefreshNotification(context, refreshInfo); err != nil { + logger.Noticef("Cannot send notification about pending refresh: %v", err) + } + }() +} + // inhibitRefresh returns an error if refresh is inhibited by running apps. // // Internally the snap state is updated to remember when the inhibition first // took place. Apps can inhibit refreshes for up to "maxInhibition", beyond // that period the refresh will go ahead despite application activity. func inhibitRefresh(st *state.State, snapst *SnapState, info *snap.Info, checker func(*snap.Info) error) error { - if err := checker(info); err != nil { - days := int(maxInhibition.Truncate(time.Hour).Hours() / 24) - now := time.Now() - if snapst.RefreshInhibitedTime == nil { - // Store the instant when the snap was first inhibited. - // This is reset to nil on successful refresh. - snapst.RefreshInhibitedTime = &now - Set(st, info.InstanceName(), snapst) - if _, ok := err.(*BusySnapError); ok { - st.Warnf(i18n.NG( - "snap %q is currently in use. Its refresh will be postponed for up to %d day to wait for the snap to no longer be in use.", - "snap %q is currently in use. Its refresh will be postponed for up to %d days to wait for the snap to no longer be in use.", days), - info.SnapName(), days) - } - return err - } + checkerErr := checker(info) + if checkerErr == nil { + return nil + } - if now.Sub(*snapst.RefreshInhibitedTime) < maxInhibition { - // If we are still in the allowed window then just return - // the error but don't change the snap state again. - return err - } - if _, ok := err.(*BusySnapError); ok { - st.Warnf(i18n.NG( - "snap %q has been running for the maximum allowable %d day since its refresh was postponed. It will now be refreshed.", - "snap %q has been running for the maximum allowable %d days since its refresh was postponed. It will now be refreshed.", days), - info.SnapName(), days) + // Get pending refresh information from compatible errors or synthesize a new one. + var refreshInfo *userclient.PendingSnapRefreshInfo + if err, ok := checkerErr.(*BusySnapError); ok { + refreshInfo = err.PendingSnapRefreshInfo() + } else { + refreshInfo = &userclient.PendingSnapRefreshInfo{ + InstanceName: info.InstanceName(), } } - return nil + + // Decide on what to do depending on the state of the snap and the remaining + // inhibition time. + now := time.Now() + switch { + case snapst.RefreshInhibitedTime == nil: + // If the snap did not have inhibited refresh yet then commence a new + // window, during which refreshes are postponed, by storing the current + // time in the snap state's RefreshInhibitedTime field. This field is + // reset to nil on successful refresh. + snapst.RefreshInhibitedTime = &now + refreshInfo.TimeRemaining = (maxInhibition - now.Sub(*snapst.RefreshInhibitedTime)).Truncate(time.Second) + Set(st, info.InstanceName(), snapst) + case now.Sub(*snapst.RefreshInhibitedTime) < maxInhibition: + // If we are still in the allowed window then just return the error but + // don't change the snap state again. + // TODO: as time left shrinks, send additional notifications with + // increasing frequency, allowing the user to understand the urgency. + refreshInfo.TimeRemaining = (maxInhibition - now.Sub(*snapst.RefreshInhibitedTime)).Truncate(time.Second) + default: + // If we run out of time then consume the error that would normally + // inhibit refresh and notify the user that the snap is refreshing right + // now, by not setting the TimeRemaining field of the refresh + // notification message. + checkerErr = nil + } + + // Send the notification asynchronously to avoid holding the state lock. + asyncPendingRefreshNotification(context.TODO(), userclient.New(), refreshInfo) + return checkerErr } diff -Nru snapd-2.47.1+20.10.1build1/overlord/snapstate/autorefresh_test.go snapd-2.48+21.04/overlord/snapstate/autorefresh_test.go --- snapd-2.47.1+20.10.1build1/overlord/snapstate/autorefresh_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/overlord/snapstate/autorefresh_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -41,7 +41,9 @@ "github.com/snapcore/snapd/snap" "github.com/snapcore/snapd/store" "github.com/snapcore/snapd/store/storetest" + "github.com/snapcore/snapd/testutil" "github.com/snapcore/snapd/timeutil" + userclient "github.com/snapcore/snapd/usersession/client" ) type autoRefreshStore struct { @@ -50,9 +52,19 @@ ops []string err error + + snapActionOpsFunc func() } func (r *autoRefreshStore) SnapAction(ctx context.Context, currentSnaps []*store.CurrentSnap, actions []*store.SnapAction, assertQuery store.AssertionQuery, user *auth.UserState, opts *store.RefreshOptions) ([]store.SnapActionResult, []store.AssertionResult, error) { + // this is a bit of a hack to simulate race conditions where while the store + // has unlocked the global state lock something else could come in and + // change the auto-refresh hold + if r.snapActionOpsFunc != nil { + r.snapActionOpsFunc() + return nil, nil, r.err + } + if assertQuery != nil { panic("no assertion query support") } @@ -71,11 +83,14 @@ panic("expected refresh actions") } } + r.ops = append(r.ops, "list-refresh") + return nil, nil, r.err } type autoRefreshTestSuite struct { + testutil.BaseTest state *state.State store *autoRefreshStore @@ -87,11 +102,14 @@ func (s *autoRefreshTestSuite) SetUpTest(c *C) { dirs.SetRootDir(c.MkDir()) + s.AddCleanup(func() { dirs.SetRootDir("") }) s.state = state.New(nil) s.store = &autoRefreshStore{} + s.AddCleanup(func() { s.store.snapActionOpsFunc = nil }) + s.state.Lock() defer s.state.Unlock() snapstate.ReplaceStore(s.state, s.store) @@ -107,22 +125,17 @@ }) snapstate.CanAutoRefresh = func(*state.State) (bool, error) { return true, nil } + s.AddCleanup(func() { snapstate.CanAutoRefresh = nil }) snapstate.AutoAliases = func(*state.State, *snap.Info) (map[string]string, error) { return nil, nil } + s.AddCleanup(func() { snapstate.AutoAliases = nil }) snapstate.IsOnMeteredConnection = func() (bool, error) { return false, nil } s.state.Set("seeded", true) s.state.Set("seed-time", time.Now()) s.state.Set("refresh-privacy-key", "privacy-key") - s.restore = snapstatetest.MockDeviceModel(DefaultModel()) -} - -func (s *autoRefreshTestSuite) TearDownTest(c *C) { - snapstate.CanAutoRefresh = nil - snapstate.AutoAliases = nil - s.restore() - dirs.SetRootDir("") + s.AddCleanup(snapstatetest.MockDeviceModel(DefaultModel())) } func (s *autoRefreshTestSuite) TestLastRefresh(c *C) { @@ -342,7 +355,7 @@ s.state.Set("last-refresh", initialLastRefresh) s.state.Unlock() - s.store.err = &httputil.PerstistentNetworkError{Err: fmt.Errorf("error")} + s.store.err = &httputil.PersistentNetworkError{Err: fmt.Errorf("error")} af := snapstate.NewAutoRefresh(s.state) err := af.Ensure() c.Check(err, ErrorMatches, "persistent network error: error") @@ -439,6 +452,84 @@ c.Assert(config.IsNoOption(err), Equals, true) } +func (s *autoRefreshTestSuite) TestLastRefreshRefreshHoldExpiredButResetWhileLockUnlocked(c *C) { + s.state.Lock() + defer s.state.Unlock() + + t0 := time.Now() + twelveHoursAgo := t0.Add(-12 * time.Hour) + fiveMinutesAgo := t0.Add(-5 * time.Minute) + oneHourInFuture := t0.Add(time.Hour) + s.state.Set("last-refresh", twelveHoursAgo) + + holdTime := fiveMinutesAgo + tr := config.NewTransaction(s.state) + tr.Set("core", "refresh.hold", holdTime) + tr.Commit() + + logbuf, restore := logger.MockLogger() + defer restore() + + sent := false + ch := make(chan struct{}) + // make the store snap action function trigger a background go routine to + // change the held-time underneath the auto-refresh + go func() { + // wait to be triggered by the snap action ops func + <-ch + s.state.Lock() + defer s.state.Unlock() + + // now change the refresh.hold time to be an hour in the future + tr := config.NewTransaction(s.state) + tr.Set("core", "refresh.hold", oneHourInFuture) + tr.Commit() + + // trigger the snap action ops func to proceed + ch <- struct{}{} + }() + + s.store.snapActionOpsFunc = func() { + // only need to send once, this will be invoked multiple times for + // multiple snaps + if !sent { + ch <- struct{}{} + sent = true + // wait for a response to ensure that we block waiting for the new + // refresh time to be committed in time for us to read it after + // returning in this go routine + <-ch + } + } + + af := snapstate.NewAutoRefresh(s.state) + s.state.Unlock() + err := af.Ensure() + s.state.Lock() + c.Check(err, IsNil) + + var lastRefresh time.Time + s.state.Get("last-refresh", &lastRefresh) + c.Check(lastRefresh.Year(), Equals, time.Now().Year()) + + // hold was reset mid-way to a new value one hour into the future + tr = config.NewTransaction(s.state) + var t1 time.Time + err = tr.Get("core", "refresh.hold", &t1) + c.Assert(err, IsNil) + + // when traversing json through the core config transaction, there will be + // different wall/monotonic clock times, we remove this ambiguity by + // formatting as rfc3339 which will strip this negligible difference in time + c.Assert(t1.Format(time.RFC3339), Equals, oneHourInFuture.Format(time.RFC3339)) + + // we shouldn't have had a message about "all snaps are up to date", we + // should have a message about being aborted mid way + + c.Assert(logbuf.String(), testutil.Contains, "Auto-refresh was delayed mid-way through launching, aborting to try again later") + c.Assert(logbuf.String(), Not(testutil.Contains), "auto-refresh: all snaps are up-to-date") +} + func (s *autoRefreshTestSuite) TestLastRefreshRefreshHoldExpiredReschedule(c *C) { s.state.Lock() defer s.state.Unlock() @@ -478,6 +569,98 @@ c.Check(nextRefresh1.Before(nextRefresh), Equals, false) } +func (s *autoRefreshTestSuite) TestEnsureRefreshHoldAtLeastZeroTimes(c *C) { + s.state.Lock() + defer s.state.Unlock() + + // setup hold-time as time.Time{} and next-refresh as now to simulate real + // console-conf-start situations + t0 := time.Now() + + tr := config.NewTransaction(s.state) + tr.Set("core", "refresh.hold", time.Time{}) + tr.Commit() + + af := snapstate.NewAutoRefresh(s.state) + snapstate.MockNextRefresh(af, t0) + + err := af.EnsureRefreshHoldAtLeast(time.Hour) + c.Assert(err, IsNil) + + s.state.Unlock() + err = af.Ensure() + s.state.Lock() + c.Check(err, IsNil) + + // refresh did not happen + c.Check(s.store.ops, HasLen, 0) + + // hold is now more than an hour later than when the test started + tr = config.NewTransaction(s.state) + var t1 time.Time + err = tr.Get("core", "refresh.hold", &t1) + c.Assert(err, IsNil) + + // use After() == false here in case somehow the t0 + 1hr is exactly t1, + // Before() and After() are false for the same time instants + c.Assert(t0.Add(time.Hour).After(t1), Equals, false) +} + +func (s *autoRefreshTestSuite) TestEnsureRefreshHoldAtLeast(c *C) { + s.state.Lock() + defer s.state.Unlock() + + // setup last-refresh as happening a long time ago, and refresh-hold as + // having been expired + t0 := time.Now() + s.state.Set("last-refresh", t0.Add(-12*time.Hour)) + + holdTime := t0.Add(-1 * time.Minute) + tr := config.NewTransaction(s.state) + tr.Set("core", "refresh.hold", holdTime) + + tr.Commit() + + af := snapstate.NewAutoRefresh(s.state) + snapstate.MockNextRefresh(af, holdTime.Add(-2*time.Minute)) + + err := af.EnsureRefreshHoldAtLeast(time.Hour) + c.Assert(err, IsNil) + + s.state.Unlock() + err = af.Ensure() + s.state.Lock() + c.Check(err, IsNil) + + // refresh did not happen + c.Check(s.store.ops, HasLen, 0) + + // hold is now more than an hour later than when the test started + tr = config.NewTransaction(s.state) + var t1 time.Time + err = tr.Get("core", "refresh.hold", &t1) + c.Assert(err, IsNil) + + // use After() == false here in case somehow the t0 + 1hr is exactly t1, + // Before() and After() are false for the same time instants + c.Assert(t0.Add(time.Hour).After(t1), Equals, false) + + // setting it to a shorter time will not change it + err = af.EnsureRefreshHoldAtLeast(30 * time.Minute) + c.Assert(err, IsNil) + + // time is still equal to t1 + tr = config.NewTransaction(s.state) + var t2 time.Time + err = tr.Get("core", "refresh.hold", &t2) + c.Assert(err, IsNil) + + // when traversing json through the core config transaction, there will be + // different wall/monotonic clock times, we remove this ambiguity by + // formatting as rfc3339 which will strip this negligible difference in time + c.Assert(t1.Format(time.RFC3339), Equals, t2.Format(time.RFC3339)) +} + func (s *autoRefreshTestSuite) TestEffectiveRefreshHold(c *C) { s.state.Lock() defer s.state.Unlock() @@ -710,10 +893,18 @@ c.Check(s.store.ops, DeepEquals, []string{"list-refresh"}) } -func (s *autoRefreshTestSuite) TestInhibitRefreshWithinInhibitWindow(c *C) { +func (s *autoRefreshTestSuite) TestInitialInhibitRefreshWithinInhibitWindow(c *C) { s.state.Lock() defer s.state.Unlock() + notificationCount := 0 + restore := snapstate.MockAsyncPendingRefreshNotification(func(ctx context.Context, client *userclient.Client, refreshInfo *userclient.PendingSnapRefreshInfo) { + notificationCount++ + c.Check(refreshInfo.InstanceName, Equals, "pkg") + c.Check(refreshInfo.TimeRemaining, Equals, time.Hour*14*24) + }) + defer restore() + si := &snap.SideInfo{RealName: "pkg", Revision: snap.R(1)} info := &snap.Info{SideInfo: *si} snapst := &snapstate.SnapState{ @@ -721,19 +912,56 @@ Current: si.Revision, } err := snapstate.InhibitRefresh(s.state, snapst, info, func(si *snap.Info) error { - return &snapstate.BusySnapError{SnapName: "pkg"} + return &snapstate.BusySnapError{SnapInfo: si} }) c.Assert(err, ErrorMatches, `snap "pkg" has running apps or hooks`) + c.Check(notificationCount, Equals, 1) +} + +func (s *autoRefreshTestSuite) TestSubsequentInhibitRefreshWithinInhibitWindow(c *C) { + s.state.Lock() + defer s.state.Unlock() + + notificationCount := 0 + restore := snapstate.MockAsyncPendingRefreshNotification(func(ctx context.Context, client *userclient.Client, refreshInfo *userclient.PendingSnapRefreshInfo) { + notificationCount++ + c.Check(refreshInfo.InstanceName, Equals, "pkg") + // XXX: This test measures real time, with second granularity. + // It takes non-zero (hence the subtracted second) to execute the test. + c.Check(refreshInfo.TimeRemaining, Equals, time.Hour*14*24/2-time.Second) + }) + defer restore() - pending, _ := s.state.PendingWarnings() - c.Assert(pending, HasLen, 1) - c.Check(pending[0].String(), Equals, `snap "pkg" is currently in use. Its refresh will be postponed for up to 7 days to wait for the snap to no longer be in use.`) + instant := time.Now() + pastInstant := instant.Add(-snapstate.MaxInhibition / 2) // In the middle of the allowed window + + si := &snap.SideInfo{RealName: "pkg", Revision: snap.R(1)} + info := &snap.Info{SideInfo: *si} + snapst := &snapstate.SnapState{ + Sequence: []*snap.SideInfo{si}, + Current: si.Revision, + RefreshInhibitedTime: &pastInstant, + } + + err := snapstate.InhibitRefresh(s.state, snapst, info, func(si *snap.Info) error { + return &snapstate.BusySnapError{SnapInfo: si} + }) + c.Assert(err, ErrorMatches, `snap "pkg" has running apps or hooks`) + c.Check(notificationCount, Equals, 1) } -func (s *autoRefreshTestSuite) TestInhibitRefreshWarnsAndRefreshesWhenOverdue(c *C) { +func (s *autoRefreshTestSuite) TestInhibitRefreshRefreshesWhenOverdue(c *C) { s.state.Lock() defer s.state.Unlock() + notificationCount := 0 + restore := snapstate.MockAsyncPendingRefreshNotification(func(ctx context.Context, client *userclient.Client, refreshInfo *userclient.PendingSnapRefreshInfo) { + notificationCount++ + c.Check(refreshInfo.InstanceName, Equals, "pkg") + c.Check(refreshInfo.TimeRemaining, Equals, time.Duration(0)) + }) + defer restore() + instant := time.Now() pastInstant := instant.Add(-snapstate.MaxInhibition * 2) @@ -745,11 +973,8 @@ RefreshInhibitedTime: &pastInstant, } err := snapstate.InhibitRefresh(s.state, snapst, info, func(si *snap.Info) error { - return &snapstate.BusySnapError{SnapName: "pkg"} + return &snapstate.BusySnapError{SnapInfo: si} }) c.Assert(err, IsNil) - - pending, _ := s.state.PendingWarnings() - c.Assert(pending, HasLen, 1) - c.Check(pending[0].String(), Equals, `snap "pkg" has been running for the maximum allowable 7 days since its refresh was postponed. It will now be refreshed.`) + c.Check(notificationCount, Equals, 1) } diff -Nru snapd-2.47.1+20.10.1build1/overlord/snapstate/backend/link.go snapd-2.48+21.04/overlord/snapstate/backend/link.go --- snapd-2.47.1+20.10.1build1/overlord/snapstate/backend/link.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/overlord/snapstate/backend/link.go 2020-11-19 16:51:02.000000000 +0000 @@ -25,6 +25,7 @@ "path/filepath" "github.com/snapcore/snapd/boot" + "github.com/snapcore/snapd/cmd/snaplock/runinhibit" "github.com/snapcore/snapd/logger" "github.com/snapcore/snapd/progress" "github.com/snapcore/snapd/snap" @@ -45,6 +46,10 @@ // VitalityRank is used to hint how much the services should be // protected from the OOM killer VitalityRank int + + // RunInhibitHint is used only in Unlink snap, and can be used to + // establish run inhibition lock for refresh operations. + RunInhibitHint runinhibit.Hint } func updateCurrentSymlinks(info *snap.Info) (e error) { @@ -147,6 +152,11 @@ }) } + // Stop inhibiting application startup by removing the inhibitor file. + if err := runinhibit.Unlock(info.InstanceName()); err != nil { + return false, err + } + return reboot, nil } @@ -172,7 +182,7 @@ disabledSvcs := linkCtx.PrevDisabledServices if s.Type() == snap.TypeSnapd { // snapd services are handled separately - return generateSnapdWrappers(s) + return GenerateSnapdWrappers(s) } // add the CLI apps from the snap.yaml @@ -236,7 +246,7 @@ return firstErr(err1, err2, err3, err4) } -func generateSnapdWrappers(s *snap.Info) error { +func GenerateSnapdWrappers(s *snap.Info) error { // snapd services are handled separately via an explicit helper return wrappers.AddSnapdSnapServices(s, progress.Null) } @@ -255,6 +265,12 @@ // symlinks. The firstInstallUndo is true when undoing the first installation of // the snap. func (b Backend) UnlinkSnap(info *snap.Info, linkCtx LinkContext, meter progress.Meter) error { + var err0 error + if hint := linkCtx.RunInhibitHint; hint != runinhibit.HintNotInhibited { + // inhibit startup of new programs + err0 = runinhibit.LockWithHint(info.InstanceName(), hint) + } + // remove generated services, binaries etc err1 := removeGeneratedWrappers(info, linkCtx.FirstInstall, meter) @@ -262,7 +278,7 @@ err2 := removeCurrentSymlinks(info) // FIXME: aggregate errors instead - return firstErr(err1, err2) + return firstErr(err0, err1, err2) } // ServicesEnableState returns the current enabled/disabled states of a snap's diff -Nru snapd-2.47.1+20.10.1build1/overlord/snapstate/backend/link_test.go snapd-2.48+21.04/overlord/snapstate/backend/link_test.go --- snapd-2.47.1+20.10.1build1/overlord/snapstate/backend/link_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/overlord/snapstate/backend/link_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -34,6 +34,7 @@ "github.com/snapcore/snapd/boot/boottest" "github.com/snapcore/snapd/bootloader" "github.com/snapcore/snapd/bootloader/bootloadertest" + "github.com/snapcore/snapd/cmd/snaplock/runinhibit" "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/progress" @@ -240,6 +241,8 @@ currentDataDir, err := filepath.EvalSymlinks(currentDataSymlink) c.Assert(err, IsNil) c.Assert(currentDataDir, Equals, dataDir) + + c.Check(filepath.Join(runinhibit.InhibitDir, "hello.lock"), testutil.FileAbsent) } func (s *linkSuite) TestLinkUndoIdempotent(c *C) { @@ -278,6 +281,9 @@ currentDataSymlink := filepath.Join(info.DataDir(), "..", "current") c.Check(osutil.FileExists(currentActiveSymlink), Equals, false) c.Check(osutil.FileExists(currentDataSymlink), Equals, false) + + // no inhibition lock + c.Check(filepath.Join(runinhibit.InhibitDir, "hello.lock"), testutil.FileAbsent) } func (s *linkSuite) TestLinkFailsForUnsetRevision(c *C) { @@ -578,6 +584,7 @@ c.Check(filepath.Join(dirs.SnapUserServicesDir, "snapd.session-agent.service"), checker) c.Check(filepath.Join(dirs.SnapUserServicesDir, "snapd.session-agent.socket"), checker) c.Check(filepath.Join(dirs.SnapServicesDir, "usr-lib-snapd.mount"), checker) + c.Check(filepath.Join(runinhibit.InhibitDir, "snapd.lock"), testutil.FileAbsent) } func (s *linkCleanupSuite) TestLinkCleanupFailedSnapdSnapFirstInstallOnCore(c *C) { @@ -638,9 +645,12 @@ for _, entry := range generatedSnapdUnits { c.Check(toEtcUnitPath(entry[0]), testutil.FilePresent) } + // linked snaps do not have a run inhibition lock + c.Check(filepath.Join(runinhibit.InhibitDir, "snapd.lock"), testutil.FileAbsent) linkCtx := backend.LinkContext{ - FirstInstall: true, + FirstInstall: true, + RunInhibitHint: runinhibit.HintInhibitedForRefresh, } err = s.be.UnlinkSnap(info, linkCtx, nil) c.Assert(err, IsNil) @@ -649,10 +659,13 @@ for _, entry := range generatedSnapdUnits { c.Check(toEtcUnitPath(entry[0]), testutil.FileAbsent) } + // unlinked snaps have a run inhibition lock + c.Check(filepath.Join(runinhibit.InhibitDir, "snapd.lock"), testutil.FilePresent) // unlink is idempotent err = s.be.UnlinkSnap(info, linkCtx, nil) c.Assert(err, IsNil) + c.Check(filepath.Join(runinhibit.InhibitDir, "snapd.lock"), testutil.FilePresent) } func (s *snapdOnCoreUnlinkSuite) TestUnlinkNonFirstSnapdOnCoreDoesNothing(c *C) { @@ -676,9 +689,17 @@ } // content list uses absolute paths already snaptest.PopulateDir("/", units) - err = s.be.UnlinkSnap(info, backend.LinkContext{FirstInstall: false}, nil) + linkCtx := backend.LinkContext{ + FirstInstall: false, + RunInhibitHint: runinhibit.HintInhibitedForRefresh, + } + err = s.be.UnlinkSnap(info, linkCtx, nil) c.Assert(err, IsNil) for _, unit := range units { c.Check(unit[0], testutil.FileEquals, "precious") } + + // unlinked snaps have a run inhibition lock. XXX: the specific inhibition hint can change. + c.Check(filepath.Join(runinhibit.InhibitDir, "snapd.lock"), testutil.FilePresent) + c.Check(filepath.Join(runinhibit.InhibitDir, "snapd.lock"), testutil.FileEquals, "refresh") } diff -Nru snapd-2.47.1+20.10.1build1/overlord/snapstate/backend/locking.go snapd-2.48+21.04/overlord/snapstate/backend/locking.go --- snapd-2.47.1+20.10.1build1/overlord/snapstate/backend/locking.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48+21.04/overlord/snapstate/backend/locking.go 2020-11-19 16:51:02.000000000 +0000 @@ -0,0 +1,93 @@ +// -*- 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 backend + +import ( + "github.com/snapcore/snapd/cmd/snaplock" + "github.com/snapcore/snapd/cmd/snaplock/runinhibit" + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/snap" +) + +func (b Backend) RunInhibitSnapForUnlink(info *snap.Info, hint runinhibit.Hint, decision func() error) (lock *osutil.FileLock, err error) { + // A process may be created after the soft refresh done upon + // the request to refresh a snap. If such process is alive by + // the time this code is reached the refresh process is stopped. + + // Grab per-snap lock to prevent new processes from starting. This is + // sufficient to perform the check, even though individual processes + // may fork or exit, we will have per-security-tag information about + // what is running. + lock, err = snaplock.OpenLock(info.InstanceName()) + if err != nil { + return nil, err + } + // Keep a copy of lock, so that we can close it in the function below. + // The regular lock variable is assigned to by return, due to the named + // return values. + lockToClose := lock + defer func() { + // If we have a lock but we are returning an error then unlock the lock + // by closing it. + if lockToClose != nil && err != nil { + lockToClose.Close() + } + }() + if err := lock.Lock(); err != nil { + return nil, err + } + // + if err := decision(); err != nil { + return nil, err + } + // Decision function did not fail so we can, while we still hold the snap + // lock, install the snap run inhibition hint, returning the snap lock to + // the caller. + // + // XXX: should we move this logic to the place that calls the "soft" + // check instead? Doing so would somewhat change the semantic of soft + // and hard checks, as it would effectively make hard check a no-op, + // but it might provide a nicer user experience. + if err := runinhibit.LockWithHint(info.InstanceName(), hint); err != nil { + return nil, err + } + return lock, nil +} + +// WithSnapLock executes given action with the snap lock held. +// +// The lock is also used by snap-confine during pre-snap mount namespace +// initialization. Holding it allows to ensure mutual exclusion during the +// process of preparing a new snap app or hook processes. It does not prevent +// existing application or hook processes from forking. +// +// Note that this is not a method of the Backend type, so that it can be +// invoked from doInstall, which does not have access to a backend object. +func WithSnapLock(info *snap.Info, action func() error) error { + lock, err := snaplock.OpenLock(info.InstanceName()) + if err != nil { + return err + } + // Closing the lock also unlocks it, if locked. + defer lock.Close() + if err := lock.Lock(); err != nil { + return err + } + return action() +} diff -Nru snapd-2.47.1+20.10.1build1/overlord/snapstate/backend/locking_test.go snapd-2.48+21.04/overlord/snapstate/backend/locking_test.go --- snapd-2.47.1+20.10.1build1/overlord/snapstate/backend/locking_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48+21.04/overlord/snapstate/backend/locking_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -0,0 +1,99 @@ +// -*- 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 backend_test + +import ( + "errors" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/cmd/snaplock" + "github.com/snapcore/snapd/cmd/snaplock/runinhibit" + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/overlord/snapstate/backend" + "github.com/snapcore/snapd/snap/snaptest" +) + +type lockingSuite struct { + be backend.Backend +} + +var _ = Suite(&lockingSuite{}) + +func (s *lockingSuite) SetUpTest(c *C) { + dirs.SetRootDir(c.MkDir()) +} + +func (s *lockingSuite) TestRunInhibitSnapForUnlinkPositiveDescision(c *C) { + const yaml = `name: snap-name +version: 1 +` + info := snaptest.MockInfo(c, yaml, nil) + lock, err := s.be.RunInhibitSnapForUnlink(info, "hint", func() error { + // This decision function returns nil so the lock is established and + // the inhibition hint is set. + return nil + }) + c.Assert(err, IsNil) + c.Assert(lock, NotNil) + lock.Close() + hint, err := runinhibit.IsLocked(info.InstanceName()) + c.Assert(err, IsNil) + c.Check(string(hint), Equals, "hint") +} + +func (s *lockingSuite) TestRunInhibitSnapForUnlinkNegativeDecision(c *C) { + const yaml = `name: snap-name +version: 1 +` + info := snaptest.MockInfo(c, yaml, nil) + lock, err := s.be.RunInhibitSnapForUnlink(info, "hint", func() error { + // This decision function returns an error so the lock is not + // established and the inhibition hint is not set. + return errors.New("propagated") + }) + c.Assert(err, ErrorMatches, "propagated") + c.Assert(lock, IsNil) + hint, err := runinhibit.IsLocked(info.InstanceName()) + c.Assert(err, IsNil) + c.Check(string(hint), Equals, "") +} + +func (s *lockingSuite) TestWithSnapLock(c *C) { + const yaml = `name: snap-name +version: 1 +` + info := snaptest.MockInfo(c, yaml, nil) + + lock, err := snaplock.OpenLock(info.InstanceName()) + c.Assert(err, IsNil) + defer lock.Close() + c.Assert(lock.TryLock(), IsNil) // Lock is not held + lock.Unlock() + + err = backend.WithSnapLock(info, func() error { + c.Assert(lock.TryLock(), Equals, osutil.ErrAlreadyLocked) // Lock is held + return errors.New("error-is-propagated") + }) + c.Check(err, ErrorMatches, "error-is-propagated") + + c.Assert(lock.TryLock(), IsNil) // Lock is not held +} diff -Nru snapd-2.47.1+20.10.1build1/overlord/snapstate/backend/mountunit.go snapd-2.48+21.04/overlord/snapstate/backend/mountunit.go --- snapd-2.47.1+20.10.1build1/overlord/snapstate/backend/mountunit.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/overlord/snapstate/backend/mountunit.go 2020-11-19 16:51:02.000000000 +0000 @@ -34,13 +34,13 @@ if preseed { sysd = systemd.NewEmulationMode(dirs.GlobalRootDir) } else { - sysd = systemd.New(dirs.GlobalRootDir, systemd.SystemMode, meter) + sysd = systemd.New(systemd.SystemMode, meter) } _, err := sysd.AddMountUnitFile(s.InstanceName(), s.Revision.String(), squashfsPath, whereDir, "squashfs") return err } func removeMountUnit(mountDir string, meter progress.Meter) error { - sysd := systemd.New(dirs.GlobalRootDir, systemd.SystemMode, meter) + sysd := systemd.New(systemd.SystemMode, meter) return sysd.RemoveMountUnitFile(mountDir) } diff -Nru snapd-2.47.1+20.10.1build1/overlord/snapstate/backend/setup.go snapd-2.48+21.04/overlord/snapstate/backend/setup.go --- snapd-2.47.1+20.10.1build1/overlord/snapstate/backend/setup.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/overlord/snapstate/backend/setup.go 2020-11-19 16:51:02.000000000 +0000 @@ -25,6 +25,7 @@ "path/filepath" "github.com/snapcore/snapd/boot" + "github.com/snapcore/snapd/cmd/snaplock/runinhibit" "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/progress" "github.com/snapcore/snapd/snap" @@ -155,3 +156,8 @@ func (b Backend) UndoSetupSnap(s snap.PlaceInfo, typ snap.Type, installRecord *InstallRecord, dev boot.Device, meter progress.Meter) error { return b.RemoveSnapFiles(s, typ, installRecord, dev, meter) } + +// RemoveSnapInhibitLock removes the file controlling inhibition of "snap run". +func (b Backend) RemoveSnapInhibitLock(instanceName string) error { + return runinhibit.RemoveLockFile(instanceName) +} diff -Nru snapd-2.47.1+20.10.1build1/overlord/snapstate/backend.go snapd-2.48+21.04/overlord/snapstate/backend.go --- snapd-2.47.1+20.10.1build1/overlord/snapstate/backend.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/overlord/snapstate/backend.go 2020-11-19 16:51:02.000000000 +0000 @@ -26,6 +26,8 @@ "github.com/snapcore/snapd/asserts" "github.com/snapcore/snapd/boot" "github.com/snapcore/snapd/client" + "github.com/snapcore/snapd/cmd/snaplock/runinhibit" + "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/overlord/auth" "github.com/snapcore/snapd/overlord/snapstate/backend" "github.com/snapcore/snapd/progress" @@ -86,6 +88,7 @@ RemoveSnapCommonData(info *snap.Info) error RemoveSnapDataDir(info *snap.Info, hasOtherInstances bool) error DiscardSnapNamespace(snapName string) error + RemoveSnapInhibitLock(snapName string) error // alias related UpdateAliases(add []*backend.Alias, remove []*backend.Alias) error @@ -94,4 +97,9 @@ // testing helpers CurrentInfo(cur *snap.Info) Candidate(sideInfo *snap.SideInfo) + + // refresh related + RunInhibitSnapForUnlink(info *snap.Info, hint runinhibit.Hint, decision func() error) (*osutil.FileLock, error) + // (not a backend method because doInstall cannot access the backend) + // WithSnapLock(info *snap.Info, action func() error) error } diff -Nru snapd-2.47.1+20.10.1build1/overlord/snapstate/backend_test.go snapd-2.48+21.04/overlord/snapstate/backend_test.go --- snapd-2.47.1+20.10.1build1/overlord/snapstate/backend_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/overlord/snapstate/backend_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -24,12 +24,16 @@ "errors" "fmt" "io" + "os" "path/filepath" "sort" "strings" "sync" + . "gopkg.in/check.v1" + "github.com/snapcore/snapd/boot" + "github.com/snapcore/snapd/cmd/snaplock/runinhibit" "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/overlord/auth" "github.com/snapcore/snapd/overlord/snapstate" @@ -71,10 +75,23 @@ disabledServices []string vitalityRank int + + inhibitHint runinhibit.Hint } type fakeOps []fakeOp +func (ops fakeOps) MustFindOp(c *C, opName string) *fakeOp { + for _, op := range ops { + if op.op == opName { + return &op + } + } + c.Errorf("cannot find operation with op: %q, all ops: %v", opName, ops.Ops()) + c.FailNow() + return nil +} + func (ops fakeOps) Ops() []string { opsOps := make([]string, len(ops)) for i, op := range ops { @@ -660,6 +677,8 @@ emptyContainer snap.Container servicesCurrentlyDisabled []string + + lockDir string } func (f *fakeSnappyBackend) OpenSnapFile(snapFilePath string, si *snap.SideInfo) (*snap.Info, snap.Container, error) { @@ -1045,6 +1064,14 @@ return nil } +func (f *fakeSnappyBackend) RemoveSnapInhibitLock(snapName string) error { + f.appendOp(&fakeOp{ + op: "remove-inhibit-lock", + name: snapName, + }) + return nil +} + func (f *fakeSnappyBackend) Candidate(sideInfo *snap.SideInfo) { var sinfo snap.SideInfo if sideInfo != nil { @@ -1108,6 +1135,22 @@ return nil } +func (f *fakeSnappyBackend) RunInhibitSnapForUnlink(info *snap.Info, hint runinhibit.Hint, decision func() error) (lock *osutil.FileLock, err error) { + f.appendOp(&fakeOp{ + op: "run-inhibit-snap-for-unlink", + name: info.InstanceName(), + inhibitHint: hint, + }) + if err := decision(); err != nil { + return nil, err + } + if f.lockDir == "" { + f.lockDir = os.TempDir() + } + // XXX: returning a real lock is somewhat annoying + return osutil.NewFileLock(filepath.Join(f.lockDir, info.InstanceName()+".lock")) +} + func (f *fakeSnappyBackend) appendOp(op *fakeOp) { f.mu.Lock() defer f.mu.Unlock() diff -Nru snapd-2.47.1+20.10.1build1/overlord/snapstate/booted_test.go snapd-2.48+21.04/overlord/snapstate/booted_test.go --- snapd-2.47.1+20.10.1build1/overlord/snapstate/booted_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/overlord/snapstate/booted_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -283,7 +283,7 @@ c.Assert(chg.Err(), ErrorMatches, `(?ms).*Make snap "core" \(1\) available to the system \(fail\).*`) } -func (bs *bootedSuite) TestWaitRestartCore(c *C) { +func (bs *bootedSuite) TestFinishRestartCore(c *C) { st := bs.state st.Lock() defer st.Unlock() @@ -293,7 +293,7 @@ // not core snap si := &snap.SideInfo{RealName: "some-app"} snaptest.MockSnap(c, "name: some-app\nversion: 1", si) - err := snapstate.WaitRestart(task, &snapstate.SnapSetup{SideInfo: si}) + err := snapstate.FinishRestart(task, &snapstate.SnapSetup{SideInfo: si}) c.Check(err, IsNil) si = &snap.SideInfo{RealName: "core"} @@ -302,13 +302,13 @@ // core snap, restarting ... wait state.MockRestarting(st, state.RestartSystem) snaptest.MockSnap(c, "name: core\ntype: os\nversion: 1", si) - err = snapstate.WaitRestart(task, snapsup) + err = snapstate.FinishRestart(task, snapsup) c.Check(err, FitsTypeOf, &state.Retry{}) // core snap, restarted, waiting for current core revision state.MockRestarting(st, state.RestartUnset) bs.bootloader.BootVars["snap_mode"] = boot.TryingStatus - err = snapstate.WaitRestart(task, snapsup) + err = snapstate.FinishRestart(task, snapsup) c.Check(err, DeepEquals, &state.Retry{After: 5 * time.Second}) // core snap updated @@ -317,16 +317,16 @@ // core snap, restarted, right core revision, no rollback bs.bootloader.BootVars["snap_mode"] = "" - err = snapstate.WaitRestart(task, snapsup) + err = snapstate.FinishRestart(task, snapsup) c.Check(err, IsNil) // core snap, restarted, wrong core revision, rollback! bs.bootloader.SetBootBase("core_1.snap") - err = snapstate.WaitRestart(task, snapsup) + err = snapstate.FinishRestart(task, snapsup) c.Check(err, ErrorMatches, `cannot finish core installation, there was a rollback across reboot`) } -func (bs *bootedSuite) TestWaitRestartBootableBase(c *C) { +func (bs *bootedSuite) TestFinishRestartBootableBase(c *C) { r := snapstatetest.MockDeviceModel(ModelWithBase("core18")) defer r() @@ -339,13 +339,13 @@ // not core snap si := &snap.SideInfo{RealName: "some-app", Revision: snap.R(1)} snaptest.MockSnap(c, "name: some-app\nversion: 1", si) - err := snapstate.WaitRestart(task, &snapstate.SnapSetup{SideInfo: si}) + err := snapstate.FinishRestart(task, &snapstate.SnapSetup{SideInfo: si}) c.Check(err, IsNil) // core snap but we are on a model with a different base si = &snap.SideInfo{RealName: "core"} snaptest.MockSnap(c, "name: core\ntype: os\nversion: 1", si) - err = snapstate.WaitRestart(task, &snapstate.SnapSetup{SideInfo: si, Type: snap.TypeOS}) + err = snapstate.FinishRestart(task, &snapstate.SnapSetup{SideInfo: si, Type: snap.TypeOS}) c.Check(err, IsNil) si = &snap.SideInfo{RealName: "core18"} @@ -353,13 +353,13 @@ snaptest.MockSnap(c, "name: core18\ntype: base\nversion: 1", si) // core snap, restarting ... wait state.MockRestarting(st, state.RestartSystem) - err = snapstate.WaitRestart(task, snapsup) + err = snapstate.FinishRestart(task, snapsup) c.Check(err, FitsTypeOf, &state.Retry{}) // core snap, restarted, waiting for current core revision state.MockRestarting(st, state.RestartUnset) bs.bootloader.BootVars["snap_mode"] = boot.TryingStatus - err = snapstate.WaitRestart(task, snapsup) + err = snapstate.FinishRestart(task, snapsup) c.Check(err, DeepEquals, &state.Retry{After: 5 * time.Second}) // core18 snap updated @@ -369,16 +369,16 @@ // core snap, restarted, right core revision, no rollback bs.bootloader.BootVars["snap_mode"] = "" bs.bootloader.SetBootBase("core18_2.snap") - err = snapstate.WaitRestart(task, snapsup) + err = snapstate.FinishRestart(task, snapsup) c.Check(err, IsNil) // core snap, restarted, wrong core revision, rollback! bs.bootloader.SetBootBase("core18_1.snap") - err = snapstate.WaitRestart(task, snapsup) + err = snapstate.FinishRestart(task, snapsup) c.Check(err, ErrorMatches, `cannot finish core18 installation, there was a rollback across reboot`) } -func (bs *bootedSuite) TestWaitRestartKernel(c *C) { +func (bs *bootedSuite) TestFinishRestartKernel(c *C) { r := snapstatetest.MockDeviceModel(DefaultModel()) defer r() @@ -391,13 +391,13 @@ // not kernel snap si := &snap.SideInfo{RealName: "some-app", Revision: snap.R(1)} snaptest.MockSnap(c, "name: some-app\nversion: 1", si) - err := snapstate.WaitRestart(task, &snapstate.SnapSetup{SideInfo: si}) + err := snapstate.FinishRestart(task, &snapstate.SnapSetup{SideInfo: si}) c.Check(err, IsNil) // different kernel (may happen with remodel) si = &snap.SideInfo{RealName: "other-kernel"} snaptest.MockSnap(c, "name: other-kernel\ntype: kernel\nversion: 1", si) - err = snapstate.WaitRestart(task, &snapstate.SnapSetup{SideInfo: si, Type: snap.TypeKernel}) + err = snapstate.FinishRestart(task, &snapstate.SnapSetup{SideInfo: si, Type: snap.TypeKernel}) c.Check(err, IsNil) si = &snap.SideInfo{RealName: "kernel"} @@ -405,13 +405,13 @@ snaptest.MockSnap(c, "name: kernel\ntype: kernel\nversion: 1", si) // kernel snap, restarting ... wait state.MockRestarting(st, state.RestartSystem) - err = snapstate.WaitRestart(task, snapsup) + err = snapstate.FinishRestart(task, snapsup) c.Check(err, FitsTypeOf, &state.Retry{}) // kernel snap, restarted, waiting for current core revision state.MockRestarting(st, state.RestartUnset) bs.bootloader.BootVars["snap_mode"] = boot.TryingStatus - err = snapstate.WaitRestart(task, snapsup) + err = snapstate.FinishRestart(task, snapsup) c.Check(err, DeepEquals, &state.Retry{After: 5 * time.Second}) // kernel snap updated @@ -421,16 +421,16 @@ // kernel snap, restarted, right kernel revision, no rollback bs.bootloader.BootVars["snap_mode"] = "" bs.bootloader.SetBootKernel("kernel_2.snap") - err = snapstate.WaitRestart(task, snapsup) + err = snapstate.FinishRestart(task, snapsup) c.Check(err, IsNil) // kernel snap, restarted, wrong core revision, rollback! bs.bootloader.SetBootKernel("kernel_1.snap") - err = snapstate.WaitRestart(task, snapsup) + err = snapstate.FinishRestart(task, snapsup) c.Check(err, ErrorMatches, `cannot finish kernel installation, there was a rollback across reboot`) } -func (bs *bootedSuite) TestWaitRestartEphemeralModeSkipsRollbackDetection(c *C) { +func (bs *bootedSuite) TestFinishRestartEphemeralModeSkipsRollbackDetection(c *C) { r := snapstatetest.MockDeviceModel(DefaultModel()) defer r() @@ -445,13 +445,13 @@ snaptest.MockSnap(c, "name: kernel\ntype: kernel\nversion: 1", si) // kernel snap, restarted, wrong core revision, rollback detected! bs.bootloader.SetBootKernel("kernel_1.snap") - err := snapstate.WaitRestart(task, snapsup) + err := snapstate.FinishRestart(task, snapsup) c.Check(err, ErrorMatches, `cannot finish kernel installation, there was a rollback across reboot`) // but *not* in an ephemeral mode like "recover" - we skip the rollback // detection here r = snapstatetest.MockDeviceModelAndMode(DefaultModel(), "install") defer r() - err = snapstate.WaitRestart(task, snapsup) + err = snapstate.FinishRestart(task, snapsup) c.Check(err, IsNil) } diff -Nru snapd-2.47.1+20.10.1build1/overlord/snapstate/catalogrefresh.go snapd-2.48+21.04/overlord/snapstate/catalogrefresh.go --- snapd-2.47.1+20.10.1build1/overlord/snapstate/catalogrefresh.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/overlord/snapstate/catalogrefresh.go 2020-11-19 16:51:02.000000000 +0000 @@ -73,6 +73,19 @@ return nil } + // similar to the not yet seeded case, on uc20 install mode it doesn't make + // sense to refresh the catalog for an ephemeral system + deviceCtx, err := DeviceCtx(r.state, nil, nil) + if err != nil { + // if we are seeded we should have a device context + return err + } + + if deviceCtx.SystemMode() == "install" { + // skip the refresh + return nil + } + now := time.Now() delay := catalogRefreshDelayBase if r.nextCatalogRefresh.IsZero() { diff -Nru snapd-2.47.1+20.10.1build1/overlord/snapstate/catalogrefresh_test.go snapd-2.48+21.04/overlord/snapstate/catalogrefresh_test.go --- snapd-2.47.1+20.10.1build1/overlord/snapstate/catalogrefresh_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/overlord/snapstate/catalogrefresh_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -34,6 +34,7 @@ "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/overlord/auth" "github.com/snapcore/snapd/overlord/snapstate" + "github.com/snapcore/snapd/overlord/snapstate/snapstatetest" "github.com/snapcore/snapd/overlord/state" "github.com/snapcore/snapd/store" "github.com/snapcore/snapd/store/storetest" @@ -77,6 +78,8 @@ store *catalogStore tmpdir string + + testutil.BaseTest } var _ = Suite(&catalogRefreshTestSuite{}) @@ -92,11 +95,11 @@ // mark system as seeded s.state.Set("seeded", true) - snapstate.CanAutoRefresh = func(*state.State) (bool, error) { return true, nil } -} + // setup a simple deviceCtx since we check that for install mode + s.AddCleanup(snapstatetest.MockDeviceModel(DefaultModel())) -func (s *catalogRefreshTestSuite) TearDownTest(c *C) { - snapstate.CanAutoRefresh = nil + snapstate.CanAutoRefresh = func(*state.State) (bool, error) { return true, nil } + s.AddCleanup(func() { snapstate.CanAutoRefresh = nil }) } func (s *catalogRefreshTestSuite) TestCatalogRefresh(c *C) { @@ -210,6 +213,31 @@ cr7 := snapstate.NewCatalogRefresh(s.state) // next is initially zero + c.Check(snapstate.NextCatalogRefresh(cr7).IsZero(), Equals, true) + + err := cr7.Ensure() + c.Assert(err, IsNil) + + // next should be still zero as we skipped refresh on unseeded system + c.Check(snapstate.NextCatalogRefresh(cr7).IsZero(), Equals, true) + // nothing got created + c.Check(osutil.FileExists(dirs.SnapSectionsFile), Equals, false) + c.Check(osutil.FileExists(dirs.SnapNamesFile), Equals, false) + c.Check(osutil.FileExists(dirs.SnapCommandsDB), Equals, false) +} + +func (s *catalogRefreshTestSuite) TestCatalogRefreshUC20InstallMode(c *C) { + // mark system as being in install mode + trivialInstallDevice := &snapstatetest.TrivialDeviceContext{ + DeviceModel: DefaultModel(), + SysMode: "install", + } + + r := snapstatetest.MockDeviceContext(trivialInstallDevice) + defer r() + + cr7 := snapstate.NewCatalogRefresh(s.state) + // next is initially zero c.Check(snapstate.NextCatalogRefresh(cr7).IsZero(), Equals, true) err := cr7.Ensure() diff -Nru snapd-2.47.1+20.10.1build1/overlord/snapstate/check_snap_test.go snapd-2.48+21.04/overlord/snapstate/check_snap_test.go --- snapd-2.47.1+20.10.1build1/overlord/snapstate/check_snap_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/overlord/snapstate/check_snap_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -22,6 +22,7 @@ import ( "errors" "fmt" + "os/user" . "gopkg.in/check.v1" @@ -1132,13 +1133,23 @@ } } -func (s *checkSnapSuite) TestCheckSnapSystemUsernamesCalls(c *C) { - // FIXME: this test fails on machines where the user was already - // created by the system snapd - _, err := osutil.FindUid("snapd-range-524288-root") - if err == nil { - c.Skip("FIXME") - } +func (s *checkSnapSuite) TestCheckSnapSystemUsernamesCallsSnapDaemon(c *C) { + r := osutil.MockFindGid(func(groupname string) (uint64, error) { + if groupname == "snap_daemon" || groupname == "snapd-range-524288-root" { + return 0, user.UnknownGroupError(groupname) + } + return 0, fmt.Errorf("unexpected call to FindGid for %s", groupname) + }) + defer r() + + r = osutil.MockFindUid(func(username string) (uint64, error) { + if username == "snap_daemon" || username == "snapd-range-524288-root" { + return 0, user.UnknownUserError(username) + } + return 0, fmt.Errorf("unexpected call to FindUid for %s", username) + }) + defer r() + falsePath := osutil.LookPathDefault("false", "/bin/false") for _, classic := range []bool{false, true} { restore := release.MockOnClassic(classic) diff -Nru snapd-2.47.1+20.10.1build1/overlord/snapstate/export_test.go snapd-2.48+21.04/overlord/snapstate/export_test.go --- snapd-2.47.1+20.10.1build1/overlord/snapstate/export_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/overlord/snapstate/export_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -26,6 +26,7 @@ "github.com/snapcore/snapd/overlord/state" "github.com/snapcore/snapd/snap" "github.com/snapcore/snapd/store" + userclient "github.com/snapcore/snapd/usersession/client" ) type ManagerBackend managerBackend @@ -137,6 +138,9 @@ NewCatalogRefresh = newCatalogRefresh CatalogRefreshDelayBase = catalogRefreshDelayBase CatalogRefreshDelayWithDelta = catalogRefreshDelayWithDelta + + SoftCheckNothingRunningForRefresh = softCheckNothingRunningForRefresh + HardEnsureNothingRunningDuringRefresh = hardEnsureNothingRunningDuringRefresh ) func MockNextRefresh(ar *autoRefresh, when time.Time) { @@ -187,6 +191,14 @@ } } +func MockAsyncPendingRefreshNotification(fn func(context.Context, *userclient.Client, *userclient.PendingSnapRefreshInfo)) (restore func()) { + old := asyncPendingRefreshNotification + asyncPendingRefreshNotification = fn + return func() { + asyncPendingRefreshNotification = old + } +} + // re-refresh related var ( RefreshedSnaps = refreshedSnaps @@ -254,8 +266,39 @@ } } +func MockGenerateSnapdWrappers(f func(snapInfo *snap.Info) error) func() { + old := generateSnapdWrappers + generateSnapdWrappers = f + return func() { + generateSnapdWrappers = old + } +} + +var ( + NotifyLinkParticipants = notifyLinkParticipants +) + // autorefresh var ( InhibitRefresh = inhibitRefresh MaxInhibition = maxInhibition ) + +func NewBusySnapError(info *snap.Info, pids []int, busyAppNames, busyHookNames []string) *BusySnapError { + return &BusySnapError{ + SnapInfo: info, + pids: pids, + busyAppNames: busyAppNames, + busyHookNames: busyHookNames, + } +} + +func MockGenericRefreshCheck(fn func(info *snap.Info, canAppRunDuringRefresh func(app *snap.AppInfo) bool) error) (restore func()) { + old := genericRefreshCheck + genericRefreshCheck = fn + return func() { genericRefreshCheck = old } +} + +func (m *autoRefresh) EnsureRefreshHoldAtLeast(d time.Duration) error { + return m.ensureRefreshHoldAtLeast(d) +} diff -Nru snapd-2.47.1+20.10.1build1/overlord/snapstate/flags.go snapd-2.48+21.04/overlord/snapstate/flags.go --- snapd-2.47.1+20.10.1build1/overlord/snapstate/flags.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/overlord/snapstate/flags.go 2020-11-19 16:51:02.000000000 +0000 @@ -42,6 +42,9 @@ // to ignore refresh control validation. IgnoreValidation bool `json:"ignore-validation,omitempty"` + // IgnoreRunning is set to indicate that running apps or hooks should be ignored. + IgnoreRunning bool `json:"ignore-running,omitempty"` + // Required is set to mark that a snap is required // and cannot be removed Required bool `json:"required,omitempty"` diff -Nru snapd-2.47.1+20.10.1build1/overlord/snapstate/handlers.go snapd-2.48+21.04/overlord/snapstate/handlers.go --- snapd-2.47.1+20.10.1build1/overlord/snapstate/handlers.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/overlord/snapstate/handlers.go 2020-11-19 16:51:02.000000000 +0000 @@ -33,6 +33,7 @@ "gopkg.in/tomb.v2" "github.com/snapcore/snapd/boot" + "github.com/snapcore/snapd/cmd/snaplock/runinhibit" "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/features" "github.com/snapcore/snapd/i18n" @@ -833,17 +834,15 @@ return err } - if experimentalRefreshAppAwareness { - // A process may be created after the soft refresh done upon - // the request to refresh a snap. If such process is alive by - // the time this code is reached the refresh process is stopped. - // In case of failure the snap state is modified to indicate - // when the refresh was first inhibited. If the first - // inhibition is outside of a grace period then refresh - // proceeds regardless of the existing processes. - if err := inhibitRefresh(st, snapst, oldInfo, HardNothingRunningRefreshCheck); err != nil { + if experimentalRefreshAppAwareness && !snapsup.Flags.IgnoreRunning { + // Invoke the hard refresh flow. Upon success the returned lock will be + // held to prevent snap-run from advancing until UnlinkSnap, executed + // below, completes. + lock, err := hardEnsureNothingRunningDuringRefresh(m.backend, st, snapst, oldInfo) + if err != nil { return err } + defer lock.Close() } snapst.Active = false @@ -851,6 +850,9 @@ // do the final unlink linkCtx := backend.LinkContext{ FirstInstall: false, + // This task is only used for unlinking a snap during refreshes so we + // can safely hard-code this condition here. + RunInhibitHint: runinhibit.HintInhibitedForRefresh, } err = m.backend.UnlinkSnap(oldInfo, linkCtx, NewTaskProgressAdapterLocked(t)) if err != nil { @@ -859,6 +861,9 @@ // mark as inactive Set(st, snapsup.InstanceName(), snapst) + + // Notify link snap participants about link changes. + notifyLinkParticipants(t, snapsup.InstanceName()) return nil } @@ -917,10 +922,12 @@ // mark as active again Set(st, snapsup.InstanceName(), snapst) + // Notify link snap participants about link changes. + notifyLinkParticipants(t, snapsup.InstanceName()) + // if we just put back a previous a core snap, request a restart // so that we switch executing its snapd m.maybeRestart(t, oldInfo, reboot, deviceCtx) - return nil } @@ -1106,6 +1113,47 @@ return 0, nil } +// LinkSnapParticipant is an interface for interacting with snap link/unlink +// operations. +// +// Unlike the interface for a task handler, only one notification method is +// used. The method notifies a participant that linkage of a snap has changed. +// This method is invoked in link-snap, unlink-snap, the undo path of those +// methods and the undo handler for link-snap. +// +// In all cases it is invoked after all other operations are completed but +// before the task completes. +type LinkSnapParticipant interface { + // SnapLinkageChanged is called when a snap is linked or unlinked. + // The error is only logged and does not stop the task it is used from. + SnapLinkageChanged(st *state.State, instanceName string) error +} + +var linkSnapParticipants []LinkSnapParticipant + +// AddLinkSnapParticipant adds a participant in the link/unlink operations. +func AddLinkSnapParticipant(p LinkSnapParticipant) { + linkSnapParticipants = append(linkSnapParticipants, p) +} + +// MockLinkSnapParticipants replaces the list of link snap participants for testing. +func MockLinkSnapParticipants(ps []LinkSnapParticipant) (restore func()) { + old := linkSnapParticipants + linkSnapParticipants = ps + return func() { + linkSnapParticipants = old + } +} + +func notifyLinkParticipants(t *state.Task, instanceName string) { + st := t.State() + for _, p := range linkSnapParticipants { + if err := p.SnapLinkageChanged(st, instanceName); err != nil { + t.Errorf("%v", err) + } + } +} + func (m *SnapManager) doLinkSnap(t *state.Task, _ *tomb.Tomb) (err error) { st := t.State() st.Lock() @@ -1234,6 +1282,7 @@ if unlinkErr != nil { t.Errorf("cannot cleanup failed attempt at making snap %q available to the system: %v", snapsup.InstanceName(), unlinkErr) } + notifyLinkParticipants(t, snapsup.InstanceName()) }() if err != nil { return err @@ -1281,9 +1330,6 @@ // Record the fact that the snap was refreshed successfully. snapst.RefreshInhibitedTime = nil - // Do at the end so we only preserve the new state if it worked. - Set(st, snapsup.InstanceName(), snapst) - if cand.SnapID != "" { // write the auxiliary store info aux := &auxStoreInfo{ @@ -1334,6 +1380,12 @@ } } + // Do at the end so we only preserve the new state if it worked. + Set(st, snapsup.InstanceName(), snapst) + + // Notify link snap participants about link changes. + notifyLinkParticipants(t, snapsup.InstanceName()) + // Make sure if state commits and snapst is mutated we won't be rerun t.SetStatus(state.DoneStatus) @@ -1650,12 +1702,16 @@ m.maybeRestart(t, newInfo, rebootRequired, deviceCtx) } - // mark as inactive - Set(st, snapsup.InstanceName(), snapst) // write sequence file for failover helpers if err := writeSeqFile(snapsup.InstanceName(), snapst); err != nil { return err } + // mark as inactive + Set(st, snapsup.InstanceName(), snapst) + + // Notify link snap participants about link changes. + notifyLinkParticipants(t, snapsup.InstanceName()) + // Make sure if state commits and snapst is mutated we won't be rerun t.SetStatus(state.UndoneStatus) @@ -1872,9 +1928,81 @@ snapst.Active = false Set(st, snapsup.InstanceName(), snapst) + // Notify link snap participants about link changes. + notifyLinkParticipants(t, snapsup.InstanceName()) + return err } +func (m *SnapManager) undoUnlinkSnap(t *state.Task, _ *tomb.Tomb) error { + st := t.State() + st.Lock() + defer st.Unlock() + + perfTimings := state.TimingsForTask(t) + defer perfTimings.Save(st) + + snapsup, snapst, err := snapSetupAndState(t) + if err != nil { + return err + } + + isInstalled := snapst.IsInstalled() + if !isInstalled { + return fmt.Errorf("internal error: snap %q not installed anymore", snapsup.InstanceName()) + } + + info, err := snapst.CurrentInfo() + if err != nil { + return err + } + + deviceCtx, err := DeviceCtx(st, t, nil) + if err != nil { + return err + } + + // undo here may be part of failed snap remove change, in which case a later + // "clear-snap" task could have been executed and some or all of the + // data of this snap could be lost. If that's the case, then we should not + // enable the snap back. + // XXX: should make an exception for snapd/core? + place := snapsup.placeInfo() + for _, dir := range []string{place.DataDir(), place.CommonDataDir()} { + if exists, _, _ := osutil.DirExists(dir); !exists { + t.Logf("cannot link snap %q back, some of its data has already been removed", snapsup.InstanceName()) + // TODO: mark the snap broken at the SnapState level when we have + // such concept. + return nil + } + } + + snapst.Active = true + Set(st, snapsup.InstanceName(), snapst) + + vitalityRank, err := vitalityRank(st, snapsup.InstanceName()) + if err != nil { + return err + } + linkCtx := backend.LinkContext{ + FirstInstall: false, + VitalityRank: vitalityRank, + } + reboot, err := m.backend.LinkSnap(info, deviceCtx, linkCtx, perfTimings) + if err != nil { + return err + } + + // Notify link snap participants about link changes. + notifyLinkParticipants(t, snapsup.InstanceName()) + + // if we just linked back a core snap, request a restart + // so that we switch executing its snapd. + m.maybeRestart(t, info, reboot, deviceCtx) + + return nil +} + func (m *SnapManager) doClearSnapData(t *state.Task, _ *tomb.Tomb) error { st := t.State() st.Lock() @@ -1975,6 +2103,10 @@ t.Errorf("cannot discard snap namespace %q, will retry in 3 mins: %s", snapsup.InstanceName(), err) return &state.Retry{After: 3 * time.Minute} } + err = m.backend.RemoveSnapInhibitLock(snapsup.InstanceName()) + if err != nil { + return err + } if err := m.removeSnapCookie(st, snapsup.InstanceName()); err != nil { return fmt.Errorf("cannot remove snap cookie: %v", err) } diff -Nru snapd-2.47.1+20.10.1build1/overlord/snapstate/handlers_link_test.go snapd-2.48+21.04/overlord/snapstate/handlers_link_test.go --- snapd-2.47.1+20.10.1build1/overlord/snapstate/handlers_link_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/overlord/snapstate/handlers_link_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -92,6 +92,10 @@ // we start without the auxiliary store info c.Check(snapstate.AuxStoreInfoFilename("foo-id"), testutil.FileAbsent) + lp := &testLinkParticipant{} + restore := snapstate.MockLinkSnapParticipants([]snapstate.LinkSnapParticipant{lp}) + defer restore() + s.state.Lock() t := s.state.NewTask("link-snap", "test") t.Set("snap-setup", &snapstate.SnapSetup{ @@ -133,6 +137,9 @@ // we end with the auxiliary store info c.Check(snapstate.AuxStoreInfoFilename("foo-id"), testutil.FilePresent) + + // link snap participant was invoked + c.Check(lp.instanceNames, DeepEquals, []string{"foo"}) } func (s *linkSnapSuite) TestDoLinkSnapSuccessWithCohort(c *C) { @@ -328,6 +335,28 @@ func (s *linkSnapSuite) TestDoUndoLinkSnap(c *C) { s.state.Lock() defer s.state.Unlock() + + linkChangeCount := 0 + lp := &testLinkParticipant{ + linkageChanged: func(st *state.State, instanceName string) error { + var snapst snapstate.SnapState + err := snapstate.Get(st, instanceName, &snapst) + linkChangeCount++ + switch linkChangeCount { + case 1: + // Initially the snap gets linked. + c.Check(err, IsNil) + c.Check(snapst.Active, Equals, true) + case 2: + // Then link-snap is undone and the snap gets unlinked. + c.Check(err, Equals, state.ErrNoState) + } + return nil + }, + } + restore := snapstate.MockLinkSnapParticipants([]snapstate.LinkSnapParticipant{lp}) + defer restore() + // a hook might have set some config cfg := json.RawMessage(`{"c":true}`) err := config.SetSnapConfig(s.state, "foo", &cfg) @@ -374,6 +403,71 @@ c.Check(config, HasLen, 1) _, ok := config["core"] c.Check(ok, Equals, true) + + // link snap participant was invoked, once for do, once for undo. + c.Check(lp.instanceNames, DeepEquals, []string{"foo", "foo"}) +} + +func (s *linkSnapSuite) TestDoUnlinkCurrentSnapWithIgnoreRunning(c *C) { + s.state.Lock() + defer s.state.Unlock() + + // With refresh-app-awareness enabled + tr := config.NewTransaction(s.state) + tr.Set("core", "experimental.refresh-app-awareness", true) + tr.Commit() + + // With a snap "pkg" at revision 42 + si := &snap.SideInfo{RealName: "pkg", Revision: snap.R(42)} + snapstate.Set(s.state, "pkg", &snapstate.SnapState{ + Sequence: []*snap.SideInfo{si}, + Current: si.Revision, + Active: true, + }) + + // With an app belonging to the snap that is apparently running. + snapstate.MockSnapReadInfo(func(name string, si *snap.SideInfo) (*snap.Info, error) { + c.Assert(name, Equals, "pkg") + info := &snap.Info{SuggestedName: name, SideInfo: *si, SnapType: snap.TypeApp} + info.Apps = map[string]*snap.AppInfo{ + "app": {Snap: info, Name: "app"}, + } + return info, nil + }) + restore := snapstate.MockPidsOfSnap(func(instanceName string) (map[string][]int, error) { + c.Assert(instanceName, Equals, "pkg") + return map[string][]int{"snap.pkg.app": {1234}}, nil + }) + defer restore() + + // We can unlink the current revision of that snap, by setting IgnoreRunning flag. + task := s.state.NewTask("unlink-current-snap", "") + task.Set("snap-setup", &snapstate.SnapSetup{ + SideInfo: si, + Flags: snapstate.Flags{IgnoreRunning: true}, + }) + chg := s.state.NewChange("dummy", "...") + chg.AddTask(task) + + // Run the task we created + s.state.Unlock() + s.se.Ensure() + s.se.Wait() + s.state.Lock() + + // And observe the results. + var snapst snapstate.SnapState + err := snapstate.Get(s.state, "pkg", &snapst) + c.Assert(err, IsNil) + c.Check(snapst.Active, Equals, false) + c.Check(snapst.Sequence, HasLen, 1) + c.Check(snapst.Current, Equals, snap.R(42)) + c.Check(task.Status(), Equals, state.DoneStatus) + expected := fakeOps{{ + op: "unlink-snap", + path: filepath.Join(dirs.SnapMountDir, "pkg/42"), + }} + c.Check(s.fakeBackend.ops, DeepEquals, expected) } func (s *linkSnapSuite) TestDoUndoUnlinkCurrentSnapWithVitalityScore(c *C) { @@ -482,6 +576,11 @@ func (s *linkSnapSuite) TestDoLinkSnapTryToCleanupOnError(c *C) { s.state.Lock() defer s.state.Unlock() + + lp := &testLinkParticipant{} + restore := snapstate.MockLinkSnapParticipants([]snapstate.LinkSnapParticipant{lp}) + defer restore() + si := &snap.SideInfo{ RealName: "foo", Revision: snap.R(35), @@ -527,6 +626,9 @@ // start with an easier-to-read error if this fails: c.Check(s.fakeBackend.ops.Ops(), DeepEquals, expected.Ops()) c.Check(s.fakeBackend.ops, DeepEquals, expected) + + // link snap participant was invoked + c.Check(lp.instanceNames, DeepEquals, []string{"foo"}) } func (s *linkSnapSuite) TestDoLinkSnapSuccessCoreRestarts(c *C) { @@ -823,6 +925,28 @@ restore := release.MockOnClassic(true) defer restore() + linkChangeCount := 0 + lp := &testLinkParticipant{ + linkageChanged: func(st *state.State, instanceName string) error { + var snapst snapstate.SnapState + err := snapstate.Get(st, instanceName, &snapst) + linkChangeCount++ + switch linkChangeCount { + case 1: + // Initially the snap gets unlinked. + c.Check(err, IsNil) + c.Check(snapst.Active, Equals, false) + case 2: + // Then the undo handler re-links it. + c.Check(err, IsNil) + c.Check(snapst.Active, Equals, true) + } + return nil + }, + } + restore = snapstate.MockLinkSnapParticipants([]snapstate.LinkSnapParticipant{lp}) + defer restore() + s.state.Lock() defer s.state.Unlock() si1 := &snap.SideInfo{ @@ -867,6 +991,7 @@ c.Check(t.Status(), Equals, state.UndoneStatus) c.Check(s.stateBackend.restartRequested, DeepEquals, []state.RestartType{state.RestartDaemon}) + c.Check(lp.instanceNames, DeepEquals, []string{"core", "core"}) } func (s *linkSnapSuite) TestDoUndoUnlinkCurrentSnapCoreBase(c *C) { @@ -1300,7 +1425,7 @@ c.Check(t.Log()[1], Matches, `.*INFO unlink`) } -func (s *linkSnapSuite) TestDoUnlinkSnapRefreshAwarenessHardCheck(c *C) { +func (s *linkSnapSuite) TestDoUnlinkSnapRefreshAwarenessHardCheckOn(c *C) { s.state.Lock() defer s.state.Unlock() @@ -1317,6 +1442,10 @@ s.state.Lock() defer s.state.Unlock() + tr := config.NewTransaction(s.state) + tr.Set("core", "experimental.refresh-app-awareness", false) + tr.Commit() + chg := s.testDoUnlinkSnapRefreshAwareness(c) c.Check(chg.Err(), IsNil) diff -Nru snapd-2.47.1+20.10.1build1/overlord/snapstate/handlers_test.go snapd-2.48+21.04/overlord/snapstate/handlers_test.go --- snapd-2.47.1+20.10.1build1/overlord/snapstate/handlers_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/overlord/snapstate/handlers_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -20,11 +20,15 @@ package snapstate_test import ( + "fmt" + . "gopkg.in/check.v1" "github.com/snapcore/snapd/overlord/snapstate" "github.com/snapcore/snapd/overlord/snapstate/snapstatetest" + "github.com/snapcore/snapd/overlord/state" "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/testutil" ) type handlersSuite struct { @@ -217,3 +221,65 @@ c.Assert(err, Equals, tt.err, Commentf(tt.comment)) } } + +type testLinkParticipant struct { + callCount int + instanceNames []string + linkageChanged func(st *state.State, instanceName string) error +} + +func (lp *testLinkParticipant) SnapLinkageChanged(st *state.State, instanceName string) error { + lp.callCount++ + lp.instanceNames = append(lp.instanceNames, instanceName) + if lp.linkageChanged != nil { + return lp.linkageChanged(st, instanceName) + } + return nil +} + +func (s *handlersSuite) TestAddLinkParticipant(c *C) { + s.state.Lock() + defer s.state.Unlock() + + // Mock link snap participants. This ensures we can add a participant + // without affecting the other tests, as the original list will be + // restored. + restore := snapstate.MockLinkSnapParticipants(nil) + defer restore() + + lp := &testLinkParticipant{ + linkageChanged: func(st *state.State, instanceName string) error { + c.Assert(st, NotNil) + c.Check(instanceName, Equals, "snap-name") + return nil + }, + } + snapstate.AddLinkSnapParticipant(lp) + + t := s.state.NewTask("link-snap", "test") + snapstate.NotifyLinkParticipants(t, "snap-name") + c.Assert(lp.callCount, Equals, 1) +} + +func (s *handlersSuite) TestNotifyLinkParticipantsErrorHandling(c *C) { + s.state.Lock() + defer s.state.Unlock() + + // See comment in TestAddLinkParticipant for details. + restore := snapstate.MockLinkSnapParticipants(nil) + defer restore() + + lp := &testLinkParticipant{ + linkageChanged: func(st *state.State, instanceName string) error { + return fmt.Errorf("something failed") + }, + } + snapstate.AddLinkSnapParticipant(lp) + + t := s.state.NewTask("link-snap", "test") + snapstate.NotifyLinkParticipants(t, "snap-name") + c.Assert(lp.callCount, Equals, 1) + logs := t.Log() + c.Assert(logs, HasLen, 1) + c.Check(logs[0], testutil.Contains, "ERROR something failed") +} diff -Nru snapd-2.47.1+20.10.1build1/overlord/snapstate/refresh.go snapd-2.48+21.04/overlord/snapstate/refresh.go --- snapd-2.47.1+20.10.1build1/overlord/snapstate/refresh.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/overlord/snapstate/refresh.go 2020-11-19 16:51:02.000000000 +0000 @@ -21,54 +21,29 @@ import ( "fmt" + "path/filepath" "sort" "strings" - "github.com/snapcore/snapd/cmd/snaplock" + "github.com/snapcore/snapd/cmd/snaplock/runinhibit" + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/overlord/snapstate/backend" + "github.com/snapcore/snapd/overlord/state" "github.com/snapcore/snapd/sandbox/cgroup" "github.com/snapcore/snapd/snap" + userclient "github.com/snapcore/snapd/usersession/client" ) // pidsOfSnap is a mockable version of PidsOfSnap var pidsOfSnap = cgroup.PidsOfSnap -func genericRefreshCheck(info *snap.Info, canAppRunDuringRefresh func(app *snap.AppInfo) bool) error { - // Grab per-snap lock to prevent new processes from starting. This is - // sufficient to perform the check, even though individual processes - // may fork or exit, we will have per-security-tag information about - // what is running. - lock, err := snaplock.OpenLock(info.SnapName()) - if err != nil { - return err - } - // Closing the lock also unlocks it, if locked. - defer lock.Close() - if err := lock.Lock(); err != nil { - return err - } +var genericRefreshCheck = func(info *snap.Info, canAppRunDuringRefresh func(app *snap.AppInfo) bool) error { knownPids, err := pidsOfSnap(info.InstanceName()) if err != nil { return err } - // As soon as the lock is released the guarantee promised by pidsOfSnap is - // no longer true. This is an existing limitation. To cite the - // documentation of pidsOfSnap: - // - // > If the per-snap lock is held while computing the set, then the following - // > guarantee is true: If a security tag is not among the result then no such - // > tag can come into existence while the lock is held. - // - // This lock will be wrapped by another lock, the snap-inhibition-lock, - // which stalls startup of new apps and hooks. Unlike the snap-lock it can - // be held for many minutes or longer, enough to complete arbitrary data - // copy and download operations. The idea is that this refresh check will - // be performed while holding the snap lock (externally, the locking code - // will move to the call site), and if successful (the check indicated that - // refresh is possible) an inhibition lock will be grabbed before releasing - // the snap lock. This will remove the race condition and give the caller a - // chance to perform time-consuming operations. - lock.Unlock() + // Due to specific of the interaction with locking, all locking is performed by the caller. var busyAppNames []string var busyHookNames []string var busyPIDs []int @@ -106,7 +81,7 @@ sort.Strings(busyHookNames) sort.Ints(busyPIDs) return &BusySnapError{ - SnapName: info.SnapName(), + SnapInfo: info, busyAppNames: busyAppNames, busyHookNames: busyHookNames, pids: busyPIDs, @@ -150,26 +125,49 @@ // BusySnapError indicates that snap has apps or hooks running and cannot refresh. type BusySnapError struct { - SnapName string + SnapInfo *snap.Info pids []int busyAppNames []string busyHookNames []string } +// PendingSnapRefreshInfo computes information necessary to perform user notification +// of postponed refresh of a snap, based on the information about snap "business". +// +// The returned value contains the instance name of the snap as well as, if possible, +// information relevant for desktop notification services, such as application name +// and the snapd-generated desktop file name. +func (err *BusySnapError) PendingSnapRefreshInfo() *userclient.PendingSnapRefreshInfo { + refreshInfo := &userclient.PendingSnapRefreshInfo{ + InstanceName: err.SnapInfo.InstanceName(), + } + for _, appName := range err.busyAppNames { + if app, ok := err.SnapInfo.Apps[appName]; ok { + path := app.DesktopFile() + if osutil.FileExists(path) { + refreshInfo.BusyAppName = appName + refreshInfo.BusyAppDesktopEntry = strings.SplitN(filepath.Base(path), ".", 2)[0] + break + } + } + } + return refreshInfo +} + // Error formats an error string describing what is running. func (err *BusySnapError) Error() string { switch { case len(err.busyAppNames) > 0 && len(err.busyHookNames) > 0: return fmt.Sprintf("snap %q has running apps (%s) and hooks (%s)", - err.SnapName, strings.Join(err.busyAppNames, ", "), strings.Join(err.busyHookNames, ", ")) + err.SnapInfo.InstanceName(), strings.Join(err.busyAppNames, ", "), strings.Join(err.busyHookNames, ", ")) case len(err.busyAppNames) > 0: return fmt.Sprintf("snap %q has running apps (%s)", - err.SnapName, strings.Join(err.busyAppNames, ", ")) + err.SnapInfo.InstanceName(), strings.Join(err.busyAppNames, ", ")) case len(err.busyHookNames) > 0: return fmt.Sprintf("snap %q has running hooks (%s)", - err.SnapName, strings.Join(err.busyHookNames, ", ")) + err.SnapInfo.InstanceName(), strings.Join(err.busyHookNames, ", ")) default: - return fmt.Sprintf("snap %q has running apps or hooks", err.SnapName) + return fmt.Sprintf("snap %q has running apps or hooks", err.SnapInfo.InstanceName()) } } @@ -184,3 +182,50 @@ func (err BusySnapError) Pids() []int { return err.pids } + +// hardEnsureNothingRunningDuringRefresh performs the complete hard refresh interaction. +// +// This check uses HardNothingRunningRefreshCheck along with interaction with +// two locks - the snap lock, shared by snap-confine and snapd and the snap run +// inhibition lock, shared by snapd and snap run. +// +// On success this function returns a locked snap lock, allowing the caller to +// atomically, with regards to "snap-confine", finish any action that required +// the apps and hooks not to be running. In addition, the persistent run +// inhibition lock is established, forcing snap-run to pause and postpone +// startup of applications from the given snap. +// +// In practice, we either inhibit app startup and refresh the snap _or_ inhibit +// the refresh change and continue running existing app processes. +func hardEnsureNothingRunningDuringRefresh(backend managerBackend, st *state.State, snapst *SnapState, info *snap.Info) (*osutil.FileLock, error) { + return backend.RunInhibitSnapForUnlink(info, runinhibit.HintInhibitedForRefresh, func() error { + // In case of successful refresh inhibition the snap state is modified + // to indicate when the refresh was first inhibited. If the first + // refresh inhibition is outside of a grace period then refresh + // proceeds regardless of the existing processes. + return inhibitRefresh(st, snapst, info, HardNothingRunningRefreshCheck) + }) +} + +// softCheckNothingRunningForRefresh checks if non-service apps are off for a snap refresh. +// +// The details of the check are explained by SoftNothingRunningRefreshCheck. +// The check is performed while holding the snap lock, which ensures that we +// are not racing with snap-confine, which is starting a new process in the +// context of the given snap. +// +// In the case that the check fails, the state is modified to reflect when the +// refresh was first postponed. Eventually the check does not fail, even if +// non-service apps are running, because this mechanism only allows postponing +// refreshes for a bounded amount of time. +func softCheckNothingRunningForRefresh(st *state.State, snapst *SnapState, info *snap.Info) error { + // Grab per-snap lock to prevent new processes from starting. This is + // sufficient to perform the check, even though individual processes may + // fork or exit, we will have per-security-tag information about what is + // running. + return backend.WithSnapLock(info, func() error { + // Perform the soft refresh viability check, possibly writing to the state + // on failure. + return inhibitRefresh(st, snapst, info, SoftNothingRunningRefreshCheck) + }) +} diff -Nru snapd-2.47.1+20.10.1build1/overlord/snapstate/refresh_test.go snapd-2.48+21.04/overlord/snapstate/refresh_test.go --- snapd-2.47.1+20.10.1build1/overlord/snapstate/refresh_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/overlord/snapstate/refresh_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -20,8 +20,13 @@ package snapstate_test import ( + "io/ioutil" + "os" + "path/filepath" + . "gopkg.in/check.v1" + "github.com/snapcore/snapd/cmd/snaplock/runinhibit" "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/overlord/snapstate" "github.com/snapcore/snapd/overlord/state" @@ -61,6 +66,7 @@ }) s.AddCleanup(restore) s.AddCleanup(func() { dirs.SetRootDir("") }) + s.state = state.New(nil) } func (s *refreshSuite) TestSoftNothingRunningRefreshCheck(c *C) { @@ -142,3 +148,137 @@ c.Check(err.Error(), Equals, `snap "pkg" has running hooks (configure)`) c.Check(err.(*snapstate.BusySnapError).Pids(), DeepEquals, []int{105}) } + +func (s *refreshSuite) TestPendingSnapRefreshInfo(c *C) { + err := snapstate.NewBusySnapError(s.info, nil, nil, nil) + refreshInfo := err.PendingSnapRefreshInfo() + c.Check(refreshInfo.InstanceName, Equals, s.info.InstanceName()) + // The information about a busy app is not populated because + // the original error did not have the corresponding information. + c.Check(refreshInfo.BusyAppName, Equals, "") + c.Check(refreshInfo.BusyAppDesktopEntry, Equals, "") + + // If we create a matching desktop entry then relevant meta-data is added. + err = snapstate.NewBusySnapError(s.info, nil, []string{"app"}, nil) + desktopFile := s.info.Apps["app"].DesktopFile() + c.Assert(os.MkdirAll(filepath.Dir(desktopFile), 0755), IsNil) + c.Assert(ioutil.WriteFile(desktopFile, nil, 0644), IsNil) + refreshInfo = err.PendingSnapRefreshInfo() + c.Check(refreshInfo.InstanceName, Equals, s.info.InstanceName()) + c.Check(refreshInfo.BusyAppName, Equals, "app") + c.Check(refreshInfo.BusyAppDesktopEntry, Equals, "pkg_app") +} + +func (s *refreshSuite) addInstalledSnap(snapst *snapstate.SnapState) (*snapstate.SnapState, *snap.Info) { + snapName := snapst.Sequence[0].RealName + snapstate.Set(s.state, snapName, snapst) + info := &snap.Info{SideInfo: snap.SideInfo{RealName: snapName, Revision: snapst.Current}} + return snapst, info +} + +func (s *refreshSuite) addDummyInstalledSnap() (*snapstate.SnapState, *snap.Info) { + return s.addInstalledSnap(&snapstate.SnapState{ + Active: true, + Sequence: []*snap.SideInfo{ + {RealName: "pkg", Revision: snap.R(5), SnapID: "pkg-snap-id"}, + }, + Current: snap.R(5), + SnapType: "app", + UserID: 1, + }) +} + +func (s *refreshSuite) TestDoSoftRefreshCheckAllowed(c *C) { + // Pretend we have a snap + s.state.Lock() + defer s.state.Unlock() + snapst, info := s.addDummyInstalledSnap() + + // Pretend that snaps can refresh normally. + restore := snapstate.MockGenericRefreshCheck(func(info *snap.Info, canAppRunDuringRefresh func(app *snap.AppInfo) bool) error { + return nil + }) + defer restore() + + // Soft refresh should not fail. + err := snapstate.SoftCheckNothingRunningForRefresh(s.state, snapst, info) + c.Assert(err, IsNil) + + // In addition, the inhibition lock is not set. + hint, err := runinhibit.IsLocked(info.InstanceName()) + c.Assert(err, IsNil) + c.Check(hint, Equals, runinhibit.HintNotInhibited) +} + +func (s *refreshSuite) TestDoSoftRefreshCheckDisallowed(c *C) { + // Pretend we have a snap + s.state.Lock() + defer s.state.Unlock() + snapst, info := s.addDummyInstalledSnap() + + // Pretend that snaps cannot refresh. + restore := snapstate.MockGenericRefreshCheck(func(info *snap.Info, canAppRunDuringRefresh func(app *snap.AppInfo) bool) error { + return &snapstate.BusySnapError{SnapInfo: info} + }) + defer restore() + + // Soft refresh should fail with a proper error. + err := snapstate.SoftCheckNothingRunningForRefresh(s.state, snapst, info) + c.Assert(err, ErrorMatches, `snap "pkg" has running apps or hooks`) + + // Sanity check: the inhibition lock was not set. + hint, err := runinhibit.IsLocked(info.InstanceName()) + c.Assert(err, IsNil) + c.Check(hint, Equals, runinhibit.HintNotInhibited) +} + +func (s *refreshSuite) TestDoHardRefreshFlowRefreshAllowed(c *C) { + backend := &fakeSnappyBackend{} + // Pretend we have a snap + s.state.Lock() + defer s.state.Unlock() + snapst, info := s.addDummyInstalledSnap() + + // Pretend that snaps can refresh normally. + restore := snapstate.MockGenericRefreshCheck(func(info *snap.Info, canAppRunDuringRefresh func(app *snap.AppInfo) bool) error { + return nil + }) + defer restore() + + // Hard refresh should not fail and return a valid lock. + lock, err := snapstate.HardEnsureNothingRunningDuringRefresh(backend, s.state, snapst, info) + c.Assert(err, IsNil) + c.Assert(lock, NotNil) + defer lock.Close() + + // We should be able to unlock the lock without an error because + // it was acquired in the same process by the tested logic. + c.Assert(lock.Unlock(), IsNil) + + // In addition, the fake backend recorded that a lock was established. + op := backend.ops.MustFindOp(c, "run-inhibit-snap-for-unlink") + c.Check(op.inhibitHint, Equals, runinhibit.Hint("refresh")) +} + +func (s *refreshSuite) TestDoHardRefreshFlowRefreshDisallowed(c *C) { + backend := &fakeSnappyBackend{} + // Pretend we have a snap + s.state.Lock() + defer s.state.Unlock() + snapst, info := s.addDummyInstalledSnap() + + // Pretend that snaps cannot refresh. + restore := snapstate.MockGenericRefreshCheck(func(info *snap.Info, canAppRunDuringRefresh func(app *snap.AppInfo) bool) error { + return &snapstate.BusySnapError{SnapInfo: info} + }) + defer restore() + + // Hard refresh should fail and not return a lock. + lock, err := snapstate.HardEnsureNothingRunningDuringRefresh(backend, s.state, snapst, info) + c.Assert(err, ErrorMatches, `snap "pkg" has running apps or hooks`) + c.Assert(lock, IsNil) + + // Sanity check: the inhibition lock was not set. + op := backend.ops.MustFindOp(c, "run-inhibit-snap-for-unlink") + c.Check(op.inhibitHint, Equals, runinhibit.Hint("refresh")) +} diff -Nru snapd-2.47.1+20.10.1build1/overlord/snapstate/snapmgr.go snapd-2.48+21.04/overlord/snapstate/snapmgr.go --- snapd-2.47.1+20.10.1build1/overlord/snapstate/snapmgr.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/overlord/snapstate/snapmgr.go 2020-11-19 16:51:02.000000000 +0000 @@ -448,7 +448,7 @@ // remove related runner.AddHandler("stop-snap-services", m.stopSnapServices, m.startSnapServices) - runner.AddHandler("unlink-snap", m.doUnlinkSnap, nil) + runner.AddHandler("unlink-snap", m.doUnlinkSnap, m.undoUnlinkSnap) runner.AddHandler("clear-snap", m.doClearSnapData, nil) runner.AddHandler("discard-snap", m.doDiscardSnap, nil) @@ -556,6 +556,29 @@ return m.autoRefresh.RefreshSchedule() } +// EnsureAutoRefreshesAreDelayed will delay refreshes for the specified amount +// of time, as well as return any active auto-refresh changes that are currently +// not ready so that the client can wait for those. +func (m *SnapManager) EnsureAutoRefreshesAreDelayed(delay time.Duration) ([]*state.Change, error) { + // always delay for at least the specified time, this ensures that even if + // there are active refreshes right now, there won't be more auto-refreshes + // that happen after the current set finish + err := m.autoRefresh.ensureRefreshHoldAtLeast(delay) + if err != nil { + return nil, err + } + + // look for auto refresh changes in progress + autoRefreshChgsInFlight := []*state.Change{} + for _, chg := range m.state.Changes() { + if chg.Kind() == "auto-refresh" && !chg.Status().Ready() { + autoRefreshChgsInFlight = append(autoRefreshChgsInFlight, chg) + } + } + + return autoRefreshChgsInFlight, nil +} + // ensureForceDevmodeDropsDevmodeFromState undoes the forced devmode // in snapstate for forced devmode distros. func (m *SnapManager) ensureForceDevmodeDropsDevmodeFromState() error { diff -Nru snapd-2.47.1+20.10.1build1/overlord/snapstate/snapstate.go snapd-2.48+21.04/overlord/snapstate/snapstate.go --- snapd-2.47.1+20.10.1build1/overlord/snapstate/snapstate.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/overlord/snapstate/snapstate.go 2020-11-19 16:51:02.000000000 +0000 @@ -174,10 +174,11 @@ } snapsup.PlugsOnly = snapsup.PlugsOnly && (len(info.Slots) == 0) - if experimentalRefreshAppAwareness { - // Note that because we are modifying the snap state this block - // must be located after the conflict check done above. - if err := inhibitRefresh(st, snapst, info, SoftNothingRunningRefreshCheck); err != nil { + if experimentalRefreshAppAwareness && !snapsup.Flags.IgnoreRunning { + // Note that because we are modifying the snap state inside + // softCheckNothingRunningForRefresh, this block must be located + // after the conflict check done above. + if err := softCheckNothingRunningForRefresh(st, snapst, info); err != nil { return nil, err } } @@ -456,20 +457,43 @@ panic("internal error: snapstate.CheckHealthHook is unset") } -// WaitRestart will return a Retry error if there is a pending restart +var generateSnapdWrappers = backend.GenerateSnapdWrappers + +// FinishRestart will return a Retry error if there is a pending restart // and a real error if anything went wrong (like a rollback across -// restarts) -func WaitRestart(task *state.Task, snapsup *SnapSetup) (err error) { +// restarts). +// For snapd snap updates this will also rerun wrappers generation to fully +// catch up with any change. +func FinishRestart(task *state.Task, snapsup *SnapSetup) (err error) { if ok, _ := task.State().Restarting(); ok { // don't continue until we are in the restarted snapd task.Logf("Waiting for automatic snapd restart...") return &state.Retry{} } - if snapsup.Type == snap.TypeSnapd && os.Getenv("SNAPD_REVERT_TO_REV") != "" { - return fmt.Errorf("there was a snapd rollback across the restart") + if snapsup.Type == snap.TypeSnapd { + if os.Getenv("SNAPD_REVERT_TO_REV") != "" { + return fmt.Errorf("there was a snapd rollback across the restart") + } + + // if we have restarted and snapd was refreshed, then we need to generate + // snapd wrappers again with current snapd, as the logic of generating + // wrappers may have changed between previous and new snapd code. + if !release.OnClassic { + snapdInfo, err := snap.ReadCurrentInfo(snapsup.SnapName()) + if err != nil { + return fmt.Errorf("cannot get current snapd snap info: %v", err) + } + // TODO: if future changes to wrappers need one more snapd restart, + // then it should be handled here as well. + if err := generateSnapdWrappers(snapdInfo); err != nil { + return err + } + } } + // consider kernel and base + deviceCtx, err := DeviceCtx(task.State(), task, nil) if err != nil { return err @@ -2123,9 +2147,17 @@ if removeAll { seq := snapst.Sequence + currentIndex := snapst.LastIndex(snapst.Current) for i := len(seq) - 1; i >= 0; i-- { - si := seq[i] - addNext(removeInactiveRevision(st, name, info.SnapID, si.Revision)) + if i != currentIndex { + si := seq[i] + addNext(removeInactiveRevision(st, name, info.SnapID, si.Revision)) + } + } + // add tasks for removing the current revision last, + // this is then also when common data will be removed + if currentIndex >= 0 { + addNext(removeInactiveRevision(st, name, info.SnapID, seq[currentIndex].Revision)) } } else { addNext(removeInactiveRevision(st, name, info.SnapID, revision)) @@ -2409,6 +2441,9 @@ if !ok { return state.ErrNoState } + + // XXX: &snapst pointer isn't needed here but it is likely historical + // (a bug in old JSON marshaling probably). err = json.Unmarshal([]byte(*raw), &snapst) if err != nil { return fmt.Errorf("cannot unmarshal snap state: %v", err) diff -Nru snapd-2.47.1+20.10.1build1/overlord/snapstate/snapstate_install_test.go snapd-2.48+21.04/overlord/snapstate/snapstate_install_test.go --- snapd-2.47.1+20.10.1build1/overlord/snapstate/snapstate_install_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/overlord/snapstate/snapstate_install_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -344,6 +344,63 @@ c.Check(snapst.RefreshInhibitedTime, NotNil) } +func (s *snapmgrTestSuite) TestInstallWithIgnoreValidationProceedsOnBusySnap(c *C) { + s.state.Lock() + defer s.state.Unlock() + + // With the refresh-app-awareness feature enabled. + tr := config.NewTransaction(s.state) + tr.Set("core", "experimental.refresh-app-awareness", true) + tr.Commit() + + // With a snap state indicating a snap is already installed. + snapst := &snapstate.SnapState{ + Active: true, + Sequence: []*snap.SideInfo{ + {RealName: "pkg", SnapID: "pkg-id", Revision: snap.R(1)}, + }, + Current: snap.R(1), + SnapType: "app", + } + snapstate.Set(s.state, "pkg", snapst) + + // With a snap info indicating it has an application called "app" + snapstate.MockSnapReadInfo(func(name string, si *snap.SideInfo) (*snap.Info, error) { + if name != "pkg" { + return s.fakeBackend.ReadInfo(name, si) + } + info := &snap.Info{SuggestedName: name, SideInfo: *si, SnapType: snap.TypeApp} + info.Apps = map[string]*snap.AppInfo{ + "app": {Snap: info, Name: "app"}, + } + return info, nil + }) + + // With an app belonging to the snap that is apparently running. + restore := snapstate.MockPidsOfSnap(func(instanceName string) (map[string][]int, error) { + c.Assert(instanceName, Equals, "pkg") + return map[string][]int{ + "snap.pkg.app": {1234}, + }, nil + }) + defer restore() + + // Attempt to install revision 2 of the snap, with the IgnoreRunning flag set. + snapsup := &snapstate.SnapSetup{ + SideInfo: &snap.SideInfo{RealName: "pkg", SnapID: "pkg-id", Revision: snap.R(2)}, + Flags: snapstate.Flags{IgnoreRunning: true}, + } + + // And observe that we do so despite the running app. + _, err := snapstate.DoInstall(s.state, snapst, snapsup, 0, "", dummyInUseCheck) + c.Assert(err, IsNil) + + // The state confirms that the refresh operation was not postponed. + err = snapstate.Get(s.state, "pkg", snapst) + c.Assert(err, IsNil) + c.Check(snapst.RefreshInhibitedTime, IsNil) +} + func (s *snapmgrTestSuite) TestInstallDespiteBusySnap(c *C) { s.state.Lock() defer s.state.Unlock() diff -Nru snapd-2.47.1+20.10.1build1/overlord/snapstate/snapstate_remove_test.go snapd-2.48+21.04/overlord/snapstate/snapstate_remove_test.go --- snapd-2.47.1+20.10.1build1/overlord/snapstate/snapstate_remove_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48+21.04/overlord/snapstate/snapstate_remove_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -0,0 +1,1587 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * 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 + * 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 snapstate_test + +import ( + "fmt" + "os" + "path/filepath" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/overlord/configstate/config" + "github.com/snapcore/snapd/overlord/snapstate" + "github.com/snapcore/snapd/overlord/state" + "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/testutil" +) + +func (s *snapmgrTestSuite) TestRemoveTasks(c *C) { + s.state.Lock() + defer s.state.Unlock() + + snapstate.Set(s.state, "foo", &snapstate.SnapState{ + Active: true, + Sequence: []*snap.SideInfo{ + {RealName: "foo", Revision: snap.R(11)}, + }, + Current: snap.R(11), + SnapType: "app", + }) + + ts, err := snapstate.Remove(s.state, "foo", snap.R(0), nil) + c.Assert(err, IsNil) + + c.Assert(s.state.TaskCount(), Equals, len(ts.Tasks())) + verifyRemoveTasks(c, ts) +} + +func (s *snapmgrTestSuite) TestRemoveTasksAutoSnapshotDisabled(c *C) { + snapstate.AutomaticSnapshot = func(st *state.State, instanceName string) (ts *state.TaskSet, err error) { + return nil, snapstate.ErrNothingToDo + } + + s.state.Lock() + defer s.state.Unlock() + + snapstate.Set(s.state, "foo", &snapstate.SnapState{ + Active: true, + Sequence: []*snap.SideInfo{ + {RealName: "foo", Revision: snap.R(11)}, + }, + Current: snap.R(11), + SnapType: "app", + }) + + ts, err := snapstate.Remove(s.state, "foo", snap.R(0), nil) + c.Assert(err, IsNil) + + c.Assert(taskKinds(ts.Tasks()), DeepEquals, []string{ + "stop-snap-services", + "run-hook[remove]", + "auto-disconnect", + "remove-aliases", + "unlink-snap", + "remove-profiles", + "clear-snap", + "discard-snap", + }) +} + +func (s *snapmgrTestSuite) TestRemoveTasksAutoSnapshotDisabledByPurgeFlag(c *C) { + s.state.Lock() + defer s.state.Unlock() + + snapstate.Set(s.state, "foo", &snapstate.SnapState{ + Active: true, + Sequence: []*snap.SideInfo{ + {RealName: "foo", Revision: snap.R(11)}, + }, + Current: snap.R(11), + SnapType: "app", + }) + + ts, err := snapstate.Remove(s.state, "foo", snap.R(0), &snapstate.RemoveFlags{Purge: true}) + c.Assert(err, IsNil) + + c.Assert(taskKinds(ts.Tasks()), DeepEquals, []string{ + "stop-snap-services", + "run-hook[remove]", + "auto-disconnect", + "remove-aliases", + "unlink-snap", + "remove-profiles", + "clear-snap", + "discard-snap", + }) +} + +func (s *snapmgrTestSuite) TestRemoveHookNotExecutedIfNotLastRevison(c *C) { + s.state.Lock() + defer s.state.Unlock() + + snapstate.Set(s.state, "foo", &snapstate.SnapState{ + Active: true, + Sequence: []*snap.SideInfo{ + {RealName: "foo", Revision: snap.R(11)}, + {RealName: "foo", Revision: snap.R(12)}, + }, + Current: snap.R(12), + }) + + ts, err := snapstate.Remove(s.state, "foo", snap.R(11), nil) + c.Assert(err, IsNil) + + runHooks := tasksWithKind(ts, "run-hook") + // no 'remove' hook task + c.Assert(runHooks, HasLen, 0) +} + +func (s *snapmgrTestSuite) TestRemoveConflict(c *C) { + s.state.Lock() + defer s.state.Unlock() + + snapstate.Set(s.state, "some-snap", &snapstate.SnapState{ + Active: true, + Sequence: []*snap.SideInfo{{RealName: "some-snap", Revision: snap.R(11)}}, + Current: snap.R(11), + }) + + ts, err := snapstate.Remove(s.state, "some-snap", snap.R(0), nil) + c.Assert(err, IsNil) + // need a change to make the tasks visible + s.state.NewChange("remove", "...").AddAll(ts) + + _, err = snapstate.Remove(s.state, "some-snap", snap.R(0), nil) + c.Assert(err, ErrorMatches, `snap "some-snap" has "remove" change in progress`) +} + +func (s *snapmgrTestSuite) testRemoveDiskSpaceCheck(c *C, featureFlag, automaticSnapshot bool) error { + s.state.Lock() + defer s.state.Unlock() + + restore := snapstate.MockOsutilCheckFreeSpace(func(string, uint64) error { + // osutil.CheckFreeSpace shouldn't be hit if either featureFlag + // or automaticSnapshot is false. If both are true then we return disk + // space error which should result in snapstate.InsufficientSpaceError + // on remove(). + return &osutil.NotEnoughDiskSpaceError{} + }) + defer restore() + + var automaticSnapshotCalled bool + snapstate.AutomaticSnapshot = func(st *state.State, instanceName string) (ts *state.TaskSet, err error) { + automaticSnapshotCalled = true + if automaticSnapshot { + t := s.state.NewTask("foo", "") + ts = state.NewTaskSet(t) + return ts, nil + } + // ErrNothingToDo is returned if automatic snapshots are disabled + return nil, snapstate.ErrNothingToDo + } + + tr := config.NewTransaction(s.state) + tr.Set("core", "experimental.check-disk-space-remove", featureFlag) + tr.Commit() + + snapstate.Set(s.state, "some-snap", &snapstate.SnapState{ + Active: true, + Sequence: []*snap.SideInfo{{RealName: "some-snap", Revision: snap.R(11)}}, + Current: snap.R(11), + SnapType: "app", + }) + + _, err := snapstate.Remove(s.state, "some-snap", snap.R(0), nil) + c.Assert(automaticSnapshotCalled, Equals, true) + return err +} + +func (s *snapmgrTestSuite) TestRemoveDiskSpaceCheckDoesNothingWhenNoSnapshot(c *C) { + featureFlag := true + snapshot := false + err := s.testRemoveDiskSpaceCheck(c, featureFlag, snapshot) + c.Assert(err, IsNil) +} + +func (s *snapmgrTestSuite) TestRemoveDiskSpaceCheckDisabledByFeatureFlag(c *C) { + featureFlag := false + snapshot := true + err := s.testRemoveDiskSpaceCheck(c, featureFlag, snapshot) + c.Assert(err, IsNil) +} + +func (s *snapmgrTestSuite) TestRemoveDiskSpaceForSnapshotError(c *C) { + featureFlag := true + snapshot := true + // both the snapshot and disk check feature are enabled, so we should hit + // the disk check (which fails). + err := s.testRemoveDiskSpaceCheck(c, featureFlag, snapshot) + c.Assert(err, NotNil) + + diskSpaceErr := err.(*snapstate.InsufficientSpaceError) + c.Assert(diskSpaceErr, ErrorMatches, `cannot create automatic snapshot when removing last revision of the snap: insufficient space.*`) + c.Check(diskSpaceErr.Path, Equals, filepath.Join(dirs.GlobalRootDir, "/var/lib/snapd")) + c.Check(diskSpaceErr.Snaps, DeepEquals, []string{"some-snap"}) + c.Check(diskSpaceErr.ChangeKind, Equals, "remove") +} + +func (s *snapmgrTestSuite) TestRemoveRunThrough(c *C) { + c.Assert(snapstate.KeepAuxStoreInfo("some-snap-id", nil), IsNil) + c.Check(snapstate.AuxStoreInfoFilename("some-snap-id"), testutil.FilePresent) + si := snap.SideInfo{ + SnapID: "some-snap-id", + RealName: "some-snap", + Revision: snap.R(7), + } + + s.state.Lock() + defer s.state.Unlock() + + snapstate.Set(s.state, "some-snap", &snapstate.SnapState{ + Active: true, + Sequence: []*snap.SideInfo{&si}, + Current: si.Revision, + SnapType: "app", + }) + + chg := s.state.NewChange("remove", "remove a snap") + ts, err := snapstate.Remove(s.state, "some-snap", snap.R(0), nil) + c.Assert(err, IsNil) + chg.AddAll(ts) + + s.state.Unlock() + defer s.se.Stop() + s.settle(c) + s.state.Lock() + + expected := fakeOps{ + { + op: "auto-disconnect:Doing", + name: "some-snap", + revno: snap.R(7), + }, + { + op: "remove-snap-aliases", + name: "some-snap", + }, + { + op: "unlink-snap", + path: filepath.Join(dirs.SnapMountDir, "some-snap/7"), + }, + { + op: "remove-profiles:Doing", + name: "some-snap", + revno: snap.R(7), + }, + { + op: "remove-snap-data", + path: filepath.Join(dirs.SnapMountDir, "some-snap/7"), + }, + { + op: "remove-snap-common-data", + path: filepath.Join(dirs.SnapMountDir, "some-snap/7"), + }, + { + op: "remove-snap-data-dir", + name: "some-snap", + path: filepath.Join(dirs.SnapDataDir, "some-snap"), + }, + { + op: "remove-snap-files", + path: filepath.Join(dirs.SnapMountDir, "some-snap/7"), + stype: "app", + }, + { + op: "discard-namespace", + name: "some-snap", + }, + { + op: "remove-inhibit-lock", + name: "some-snap", + }, + { + op: "remove-snap-dir", + name: "some-snap", + path: filepath.Join(dirs.SnapMountDir, "some-snap"), + }, + } + // start with an easier-to-read error if this fails: + c.Check(len(s.fakeBackend.ops), Equals, len(expected)) + c.Assert(s.fakeBackend.ops.Ops(), DeepEquals, expected.Ops()) + c.Check(s.fakeBackend.ops, DeepEquals, expected) + + // verify snapSetup info + tasks := ts.Tasks() + for _, t := range tasks { + if t.Kind() == "run-hook" { + continue + } + if t.Kind() == "save-snapshot" { + continue + } + snapsup, err := snapstate.TaskSnapSetup(t) + c.Assert(err, IsNil) + + var expSnapSetup *snapstate.SnapSetup + switch t.Kind() { + case "discard-conns": + expSnapSetup = &snapstate.SnapSetup{ + SideInfo: &snap.SideInfo{ + RealName: "some-snap", + }, + } + case "clear-snap", "discard-snap": + expSnapSetup = &snapstate.SnapSetup{ + SideInfo: &snap.SideInfo{ + RealName: "some-snap", + SnapID: "some-snap-id", + Revision: snap.R(7), + }, + } + default: + expSnapSetup = &snapstate.SnapSetup{ + SideInfo: &snap.SideInfo{ + RealName: "some-snap", + SnapID: "some-snap-id", + Revision: snap.R(7), + }, + Type: snap.TypeApp, + PlugsOnly: true, + } + + } + + c.Check(snapsup, DeepEquals, expSnapSetup, Commentf(t.Kind())) + } + + // verify snaps in the system state + var snapst snapstate.SnapState + err = snapstate.Get(s.state, "some-snap", &snapst) + c.Assert(err, Equals, state.ErrNoState) + c.Check(snapstate.AuxStoreInfoFilename("some-snap-id"), testutil.FileAbsent) + +} + +func (s *snapmgrTestSuite) TestParallelInstanceRemoveRunThrough(c *C) { + si := snap.SideInfo{ + RealName: "some-snap", + Revision: snap.R(7), + } + + s.state.Lock() + defer s.state.Unlock() + + // pretend we have both a regular snap and a parallel instance + snapstate.Set(s.state, "some-snap_instance", &snapstate.SnapState{ + Active: true, + Sequence: []*snap.SideInfo{&si}, + Current: si.Revision, + SnapType: "app", + InstanceKey: "instance", + }) + snapstate.Set(s.state, "some-snap", &snapstate.SnapState{ + Active: true, + Sequence: []*snap.SideInfo{&si}, + Current: si.Revision, + SnapType: "app", + }) + + chg := s.state.NewChange("remove", "remove a snap") + ts, err := snapstate.Remove(s.state, "some-snap_instance", snap.R(0), nil) + c.Assert(err, IsNil) + chg.AddAll(ts) + + s.state.Unlock() + s.settle(c) + s.state.Lock() + + expected := fakeOps{ + { + op: "auto-disconnect:Doing", + name: "some-snap_instance", + revno: snap.R(7), + }, + { + op: "remove-snap-aliases", + name: "some-snap_instance", + }, + { + op: "unlink-snap", + path: filepath.Join(dirs.SnapMountDir, "some-snap_instance/7"), + }, + { + op: "remove-profiles:Doing", + name: "some-snap_instance", + revno: snap.R(7), + }, + { + op: "remove-snap-data", + path: filepath.Join(dirs.SnapMountDir, "some-snap_instance/7"), + }, + { + op: "remove-snap-common-data", + path: filepath.Join(dirs.SnapMountDir, "some-snap_instance/7"), + }, + { + op: "remove-snap-data-dir", + name: "some-snap_instance", + path: filepath.Join(dirs.SnapDataDir, "some-snap"), + otherInstances: true, + }, + { + op: "remove-snap-files", + path: filepath.Join(dirs.SnapMountDir, "some-snap_instance/7"), + stype: "app", + }, + { + op: "discard-namespace", + name: "some-snap_instance", + }, + { + op: "remove-inhibit-lock", + name: "some-snap_instance", + }, + { + op: "remove-snap-dir", + name: "some-snap_instance", + path: filepath.Join(dirs.SnapMountDir, "some-snap"), + otherInstances: true, + }, + } + // start with an easier-to-read error if this fails: + c.Check(len(s.fakeBackend.ops), Equals, len(expected)) + c.Assert(s.fakeBackend.ops.Ops(), DeepEquals, expected.Ops()) + c.Check(s.fakeBackend.ops, DeepEquals, expected) + + // verify snapSetup info + tasks := ts.Tasks() + for _, t := range tasks { + if t.Kind() == "run-hook" { + continue + } + if t.Kind() == "save-snapshot" { + continue + } + snapsup, err := snapstate.TaskSnapSetup(t) + c.Assert(err, IsNil) + + var expSnapSetup *snapstate.SnapSetup + switch t.Kind() { + case "discard-conns": + expSnapSetup = &snapstate.SnapSetup{ + SideInfo: &snap.SideInfo{ + RealName: "some-snap", + }, + InstanceKey: "instance", + } + case "clear-snap", "discard-snap": + expSnapSetup = &snapstate.SnapSetup{ + SideInfo: &snap.SideInfo{ + RealName: "some-snap", + Revision: snap.R(7), + }, + InstanceKey: "instance", + } + default: + expSnapSetup = &snapstate.SnapSetup{ + SideInfo: &snap.SideInfo{ + RealName: "some-snap", + Revision: snap.R(7), + }, + Type: snap.TypeApp, + PlugsOnly: true, + InstanceKey: "instance", + } + + } + + c.Check(snapsup, DeepEquals, expSnapSetup, Commentf(t.Kind())) + } + + // verify snaps in the system state + var snapst snapstate.SnapState + err = snapstate.Get(s.state, "some-snap_instance", &snapst) + c.Assert(err, Equals, state.ErrNoState) + + // the non-instance snap is still there + err = snapstate.Get(s.state, "some-snap", &snapst) + c.Assert(err, IsNil) +} + +func (s *snapmgrTestSuite) TestParallelInstanceRemoveRunThroughOtherInstances(c *C) { + si := snap.SideInfo{ + RealName: "some-snap", + Revision: snap.R(7), + } + + s.state.Lock() + defer s.state.Unlock() + + // pretend we have both a regular snap and a parallel instance + snapstate.Set(s.state, "some-snap_instance", &snapstate.SnapState{ + Active: true, + Sequence: []*snap.SideInfo{&si}, + Current: si.Revision, + SnapType: "app", + InstanceKey: "instance", + }) + snapstate.Set(s.state, "some-snap_other", &snapstate.SnapState{ + Active: true, + Sequence: []*snap.SideInfo{&si}, + Current: si.Revision, + SnapType: "app", + InstanceKey: "other", + }) + + chg := s.state.NewChange("remove", "remove a snap") + ts, err := snapstate.Remove(s.state, "some-snap_instance", snap.R(0), nil) + c.Assert(err, IsNil) + chg.AddAll(ts) + + s.state.Unlock() + s.settle(c) + s.state.Lock() + + expected := fakeOps{ + { + op: "auto-disconnect:Doing", + name: "some-snap_instance", + revno: snap.R(7), + }, + { + op: "remove-snap-aliases", + name: "some-snap_instance", + }, + { + op: "unlink-snap", + path: filepath.Join(dirs.SnapMountDir, "some-snap_instance/7"), + }, + { + op: "remove-profiles:Doing", + name: "some-snap_instance", + revno: snap.R(7), + }, + { + op: "remove-snap-data", + path: filepath.Join(dirs.SnapMountDir, "some-snap_instance/7"), + }, + { + op: "remove-snap-common-data", + path: filepath.Join(dirs.SnapMountDir, "some-snap_instance/7"), + }, + { + op: "remove-snap-data-dir", + name: "some-snap_instance", + path: filepath.Join(dirs.SnapDataDir, "some-snap"), + otherInstances: true, + }, + { + op: "remove-snap-files", + path: filepath.Join(dirs.SnapMountDir, "some-snap_instance/7"), + stype: "app", + }, + { + op: "discard-namespace", + name: "some-snap_instance", + }, + { + op: "remove-inhibit-lock", + name: "some-snap_instance", + }, + { + op: "remove-snap-dir", + name: "some-snap_instance", + path: filepath.Join(dirs.SnapMountDir, "some-snap"), + otherInstances: true, + }, + } + // start with an easier-to-read error if this fails: + c.Check(len(s.fakeBackend.ops), Equals, len(expected)) + c.Assert(s.fakeBackend.ops.Ops(), DeepEquals, expected.Ops()) + c.Check(s.fakeBackend.ops, DeepEquals, expected) + + // verify snaps in the system state + var snapst snapstate.SnapState + err = snapstate.Get(s.state, "some-snap_instance", &snapst) + c.Assert(err, Equals, state.ErrNoState) + + // the other instance is still there + err = snapstate.Get(s.state, "some-snap_other", &snapst) + c.Assert(err, IsNil) +} + +func (s *snapmgrTestSuite) TestRemoveWithManyRevisionsRunThrough(c *C) { + si3 := snap.SideInfo{ + SnapID: "some-snap-id", + RealName: "some-snap", + Revision: snap.R(3), + } + + si5 := snap.SideInfo{ + SnapID: "some-snap-id", + RealName: "some-snap", + Revision: snap.R(5), + } + + si7 := snap.SideInfo{ + SnapID: "some-snap-id", + RealName: "some-snap", + Revision: snap.R(7), + } + + s.state.Lock() + defer s.state.Unlock() + + snapstate.Set(s.state, "some-snap", &snapstate.SnapState{ + Active: true, + Sequence: []*snap.SideInfo{&si5, &si3, &si7}, + Current: si7.Revision, + SnapType: "app", + }) + + chg := s.state.NewChange("remove", "remove a snap") + ts, err := snapstate.Remove(s.state, "some-snap", snap.R(0), nil) + c.Assert(err, IsNil) + chg.AddAll(ts) + + s.state.Unlock() + defer s.se.Stop() + s.settle(c) + s.state.Lock() + + expected := fakeOps{ + { + op: "auto-disconnect:Doing", + name: "some-snap", + revno: snap.R(7), + }, + { + op: "remove-snap-aliases", + name: "some-snap", + }, + { + op: "unlink-snap", + path: filepath.Join(dirs.SnapMountDir, "some-snap/7"), + }, + { + op: "remove-profiles:Doing", + name: "some-snap", + revno: snap.R(7), + }, + { + op: "remove-snap-data", + path: filepath.Join(dirs.SnapMountDir, "some-snap/3"), + }, + { + op: "remove-snap-files", + path: filepath.Join(dirs.SnapMountDir, "some-snap/3"), + stype: "app", + }, + { + op: "remove-snap-data", + path: filepath.Join(dirs.SnapMountDir, "some-snap/5"), + }, + { + op: "remove-snap-files", + path: filepath.Join(dirs.SnapMountDir, "some-snap/5"), + stype: "app", + }, + { + op: "remove-snap-data", + path: filepath.Join(dirs.SnapMountDir, "some-snap/7"), + }, + { + op: "remove-snap-common-data", + path: filepath.Join(dirs.SnapMountDir, "some-snap/7"), + }, + { + op: "remove-snap-data-dir", + name: "some-snap", + path: filepath.Join(dirs.SnapDataDir, "some-snap"), + }, + { + op: "remove-snap-files", + path: filepath.Join(dirs.SnapMountDir, "some-snap/7"), + stype: "app", + }, + { + op: "discard-namespace", + name: "some-snap", + }, + { + op: "remove-inhibit-lock", + name: "some-snap", + }, + { + op: "remove-snap-dir", + name: "some-snap", + path: filepath.Join(dirs.SnapMountDir, "some-snap"), + }, + } + // start with an easier-to-read error if this fails: + c.Assert(s.fakeBackend.ops.Ops(), DeepEquals, expected.Ops()) + c.Assert(s.fakeBackend.ops, DeepEquals, expected) + + // verify snapSetup info + tasks := ts.Tasks() + revnos := []snap.Revision{{N: 3}, {N: 5}, {N: 7}} + whichRevno := 0 + for _, t := range tasks { + if t.Kind() == "run-hook" { + continue + } + if t.Kind() == "save-snapshot" { + continue + } + snapsup, err := snapstate.TaskSnapSetup(t) + c.Assert(err, IsNil) + + var expSnapSetup *snapstate.SnapSetup + switch t.Kind() { + case "discard-conns": + expSnapSetup = &snapstate.SnapSetup{ + SideInfo: &snap.SideInfo{ + SnapID: "some-snap-id", + RealName: "some-snap", + }, + } + case "clear-snap", "discard-snap": + expSnapSetup = &snapstate.SnapSetup{ + SideInfo: &snap.SideInfo{ + SnapID: "some-snap-id", + RealName: "some-snap", + Revision: revnos[whichRevno], + }, + } + default: + expSnapSetup = &snapstate.SnapSetup{ + SideInfo: &snap.SideInfo{ + SnapID: "some-snap-id", + RealName: "some-snap", + Revision: snap.R(7), + }, + Type: snap.TypeApp, + PlugsOnly: true, + } + + } + + c.Check(snapsup, DeepEquals, expSnapSetup, Commentf(t.Kind())) + + if t.Kind() == "discard-snap" { + whichRevno++ + } + } + + // verify snaps in the system state + var snapst snapstate.SnapState + err = snapstate.Get(s.state, "some-snap", &snapst) + c.Assert(err, Equals, state.ErrNoState) +} + +func (s *snapmgrTestSuite) TestRemoveOneRevisionRunThrough(c *C) { + si3 := snap.SideInfo{ + RealName: "some-snap", + Revision: snap.R(3), + } + + si5 := snap.SideInfo{ + RealName: "some-snap", + Revision: snap.R(5), + } + + si7 := snap.SideInfo{ + RealName: "some-snap", + Revision: snap.R(7), + } + + s.state.Lock() + defer s.state.Unlock() + + snapstate.Set(s.state, "some-snap", &snapstate.SnapState{ + Active: true, + Sequence: []*snap.SideInfo{&si5, &si3, &si7}, + Current: si7.Revision, + SnapType: "app", + }) + + chg := s.state.NewChange("remove", "remove a snap") + ts, err := snapstate.Remove(s.state, "some-snap", snap.R(3), nil) + c.Assert(err, IsNil) + chg.AddAll(ts) + + s.state.Unlock() + defer s.se.Stop() + s.settle(c) + s.state.Lock() + + c.Check(len(s.fakeBackend.ops), Equals, 2) + expected := fakeOps{ + { + op: "remove-snap-data", + path: filepath.Join(dirs.SnapMountDir, "some-snap/3"), + }, + { + op: "remove-snap-files", + path: filepath.Join(dirs.SnapMountDir, "some-snap/3"), + stype: "app", + }, + } + // start with an easier-to-read error if this fails: + c.Assert(s.fakeBackend.ops.Ops(), DeepEquals, expected.Ops()) + c.Assert(s.fakeBackend.ops, DeepEquals, expected) + + // verify snapSetup info + tasks := ts.Tasks() + for _, t := range tasks { + if t.Kind() == "save-snapshot" { + continue + } + snapsup, err := snapstate.TaskSnapSetup(t) + c.Assert(err, IsNil) + + expSnapSetup := &snapstate.SnapSetup{ + SideInfo: &snap.SideInfo{ + RealName: "some-snap", + Revision: snap.R(3), + }, + } + + c.Check(snapsup, DeepEquals, expSnapSetup, Commentf(t.Kind())) + } + + // verify snaps in the system state + var snapst snapstate.SnapState + err = snapstate.Get(s.state, "some-snap", &snapst) + c.Assert(err, IsNil) + c.Check(snapst.Sequence, HasLen, 2) +} + +func (s *snapmgrTestSuite) TestRemoveLastRevisionRunThrough(c *C) { + si := snap.SideInfo{ + RealName: "some-snap", + Revision: snap.R(2), + } + + s.state.Lock() + defer s.state.Unlock() + + snapstate.Set(s.state, "some-snap", &snapstate.SnapState{ + Active: false, + Sequence: []*snap.SideInfo{&si}, + Current: si.Revision, + SnapType: "app", + }) + + chg := s.state.NewChange("remove", "remove a snap") + ts, err := snapstate.Remove(s.state, "some-snap", snap.R(2), nil) + c.Assert(err, IsNil) + chg.AddAll(ts) + + s.state.Unlock() + defer s.se.Stop() + s.settle(c) + s.state.Lock() + + c.Check(len(s.fakeBackend.ops), Equals, 8) + expected := fakeOps{ + { + op: "auto-disconnect:Doing", + name: "some-snap", + revno: snap.R(2), + }, + { + op: "remove-snap-data", + path: filepath.Join(dirs.SnapMountDir, "some-snap/2"), + }, + { + op: "remove-snap-common-data", + path: filepath.Join(dirs.SnapMountDir, "some-snap/2"), + }, + { + op: "remove-snap-data-dir", + name: "some-snap", + path: filepath.Join(dirs.SnapDataDir, "some-snap"), + }, + { + op: "remove-snap-files", + path: filepath.Join(dirs.SnapMountDir, "some-snap/2"), + stype: "app", + }, + { + op: "discard-namespace", + name: "some-snap", + }, + { + op: "remove-inhibit-lock", + name: "some-snap", + }, + { + op: "remove-snap-dir", + name: "some-snap", + path: filepath.Join(dirs.SnapMountDir, "some-snap"), + }, + } + // start with an easier-to-read error if this fails: + c.Assert(s.fakeBackend.ops.Ops(), DeepEquals, expected.Ops()) + c.Assert(s.fakeBackend.ops, DeepEquals, expected) + + // verify snapSetup info + tasks := ts.Tasks() + for _, t := range tasks { + if t.Kind() == "run-hook" { + continue + } + if t.Kind() == "save-snapshot" { + continue + } + snapsup, err := snapstate.TaskSnapSetup(t) + c.Assert(err, IsNil) + + expSnapSetup := &snapstate.SnapSetup{ + SideInfo: &snap.SideInfo{ + RealName: "some-snap", + }, + } + if t.Kind() != "discard-conns" { + expSnapSetup.SideInfo.Revision = snap.R(2) + } + if t.Kind() == "auto-disconnect" { + expSnapSetup.PlugsOnly = true + expSnapSetup.Type = "app" + } + + c.Check(snapsup, DeepEquals, expSnapSetup, Commentf(t.Kind())) + } + + // verify snaps in the system state + var snapst snapstate.SnapState + err = snapstate.Get(s.state, "some-snap", &snapst) + c.Assert(err, Equals, state.ErrNoState) +} + +func (s *snapmgrTestSuite) TestRemoveCurrentActiveRevisionRefused(c *C) { + si := snap.SideInfo{ + RealName: "some-snap", + Revision: snap.R(2), + } + + s.state.Lock() + defer s.state.Unlock() + + snapstate.Set(s.state, "some-snap", &snapstate.SnapState{ + Active: true, + Sequence: []*snap.SideInfo{&si}, + Current: si.Revision, + SnapType: "app", + }) + + _, err := snapstate.Remove(s.state, "some-snap", snap.R(2), nil) + + c.Check(err, ErrorMatches, `cannot remove active revision 2 of snap "some-snap"`) +} + +func (s *snapmgrTestSuite) TestRemoveCurrentRevisionOfSeveralRefused(c *C) { + si := snap.SideInfo{ + RealName: "some-snap", + Revision: snap.R(2), + } + + s.state.Lock() + defer s.state.Unlock() + + snapstate.Set(s.state, "some-snap", &snapstate.SnapState{ + Active: true, + Sequence: []*snap.SideInfo{&si, &si}, + Current: si.Revision, + SnapType: "app", + }) + + _, err := snapstate.Remove(s.state, "some-snap", snap.R(2), nil) + c.Assert(err, NotNil) + c.Check(err.Error(), Equals, `cannot remove active revision 2 of snap "some-snap" (revert first?)`) +} + +func (s *snapmgrTestSuite) TestRemoveMissingRevisionRefused(c *C) { + si := snap.SideInfo{ + RealName: "some-snap", + Revision: snap.R(2), + } + + s.state.Lock() + defer s.state.Unlock() + + snapstate.Set(s.state, "some-snap", &snapstate.SnapState{ + Active: true, + Sequence: []*snap.SideInfo{&si}, + Current: si.Revision, + SnapType: "app", + }) + + _, err := snapstate.Remove(s.state, "some-snap", snap.R(1), nil) + + c.Check(err, ErrorMatches, `revision 1 of snap "some-snap" is not installed`) +} + +func (s *snapmgrTestSuite) TestRemoveRefused(c *C) { + si := snap.SideInfo{ + RealName: "brand-gadget", + Revision: snap.R(7), + } + + s.state.Lock() + defer s.state.Unlock() + + snapstate.Set(s.state, "brand-gadget", &snapstate.SnapState{ + Active: true, + Sequence: []*snap.SideInfo{&si}, + Current: si.Revision, + SnapType: "gadget", + }) + + _, err := snapstate.Remove(s.state, "brand-gadget", snap.R(0), nil) + + c.Check(err, ErrorMatches, `snap "brand-gadget" is not removable: snap is used by the model`) +} + +func (s *snapmgrTestSuite) TestRemoveRefusedLastRevision(c *C) { + si := snap.SideInfo{ + RealName: "brand-gadget", + Revision: snap.R(7), + } + + s.state.Lock() + defer s.state.Unlock() + + snapstate.Set(s.state, "brand-gadget", &snapstate.SnapState{ + Active: false, + Sequence: []*snap.SideInfo{&si}, + Current: si.Revision, + SnapType: "gadget", + }) + + _, err := snapstate.Remove(s.state, "brand-gadget", snap.R(7), nil) + + c.Check(err, ErrorMatches, `snap "brand-gadget" is not removable: snap is used by the model`) +} + +func (s *snapmgrTestSuite) TestRemoveDeletesConfigOnLastRevision(c *C) { + si := snap.SideInfo{ + RealName: "some-snap", + Revision: snap.R(7), + } + + s.state.Lock() + defer s.state.Unlock() + + snapstate.Set(s.state, "some-snap", &snapstate.SnapState{ + Active: true, + Sequence: []*snap.SideInfo{&si}, + Current: si.Revision, + SnapType: "app", + }) + + snapstate.Set(s.state, "another-snap", &snapstate.SnapState{ + Active: true, + Sequence: []*snap.SideInfo{&si}, + Current: si.Revision, + SnapType: "app", + }) + + tr := config.NewTransaction(s.state) + tr.Set("some-snap", "foo", "bar") + tr.Commit() + + // a config for some other snap to verify its not accidentally destroyed + tr = config.NewTransaction(s.state) + tr.Set("another-snap", "bar", "baz") + tr.Commit() + + var res string + tr = config.NewTransaction(s.state) + c.Assert(tr.Get("some-snap", "foo", &res), IsNil) + c.Assert(tr.Get("another-snap", "bar", &res), IsNil) + + chg := s.state.NewChange("remove", "remove a snap") + ts, err := snapstate.Remove(s.state, "some-snap", snap.R(0), nil) + c.Assert(err, IsNil) + chg.AddAll(ts) + + s.state.Unlock() + defer s.se.Stop() + s.settle(c) + s.state.Lock() + + // verify snaps in the system state + var snapst snapstate.SnapState + err = snapstate.Get(s.state, "some-snap", &snapst) + c.Assert(err, Equals, state.ErrNoState) + + tr = config.NewTransaction(s.state) + err = tr.Get("some-snap", "foo", &res) + c.Assert(err, NotNil) + c.Assert(err, ErrorMatches, `snap "some-snap" has no "foo" configuration option`) + + // and another snap has its config intact + c.Assert(tr.Get("another-snap", "bar", &res), IsNil) + c.Assert(res, Equals, "baz") +} + +func (s *snapmgrTestSuite) TestRemoveDoesntDeleteConfigIfNotLastRevision(c *C) { + si1 := snap.SideInfo{ + RealName: "some-snap", + Revision: snap.R(7), + } + si2 := snap.SideInfo{ + RealName: "some-snap", + Revision: snap.R(8), + } + + s.state.Lock() + defer s.state.Unlock() + + snapstate.Set(s.state, "some-snap", &snapstate.SnapState{ + Active: true, + Sequence: []*snap.SideInfo{&si1, &si2}, + Current: si2.Revision, + SnapType: "app", + }) + + tr := config.NewTransaction(s.state) + tr.Set("some-snap", "foo", "bar") + tr.Commit() + + var res string + tr = config.NewTransaction(s.state) + c.Assert(tr.Get("some-snap", "foo", &res), IsNil) + + chg := s.state.NewChange("remove", "remove a snap") + ts, err := snapstate.Remove(s.state, "some-snap", si1.Revision, nil) + c.Assert(err, IsNil) + chg.AddAll(ts) + + s.state.Unlock() + defer s.se.Stop() + s.settle(c) + s.state.Lock() + + // verify snaps in the system state + var snapst snapstate.SnapState + err = snapstate.Get(s.state, "some-snap", &snapst) + c.Assert(err, IsNil) + + tr = config.NewTransaction(s.state) + c.Assert(tr.Get("some-snap", "foo", &res), IsNil) + c.Assert(res, Equals, "bar") +} + +func (s *snapmgrTestSuite) TestRemoveMany(c *C) { + s.state.Lock() + defer s.state.Unlock() + + snapstate.Set(s.state, "one", &snapstate.SnapState{ + Active: true, + Sequence: []*snap.SideInfo{ + {RealName: "one", SnapID: "one-id", Revision: snap.R(1)}, + }, + Current: snap.R(1), + }) + snapstate.Set(s.state, "two", &snapstate.SnapState{ + Active: true, + Sequence: []*snap.SideInfo{ + {RealName: "two", SnapID: "two-id", Revision: snap.R(1)}, + }, + Current: snap.R(1), + }) + + removed, tts, err := snapstate.RemoveMany(s.state, []string{"one", "two"}) + c.Assert(err, IsNil) + c.Assert(tts, HasLen, 2) + c.Check(removed, DeepEquals, []string{"one", "two"}) + + c.Assert(s.state.TaskCount(), Equals, 8*2) + for i, ts := range tts { + c.Assert(taskKinds(ts.Tasks()), DeepEquals, []string{ + "stop-snap-services", + "run-hook[remove]", + "auto-disconnect", + "remove-aliases", + "unlink-snap", + "remove-profiles", + "clear-snap", + "discard-snap", + }) + verifyStopReason(c, ts, "remove") + // check that tasksets are in separate lanes + for _, t := range ts.Tasks() { + c.Assert(t.Lanes(), DeepEquals, []int{i + 1}) + } + + } +} + +func (s *snapmgrTestSuite) testRemoveManyDiskSpaceCheck(c *C, featureFlag, automaticSnapshot, freeSpaceCheckFail bool) error { + s.state.Lock() + defer s.state.Unlock() + + var checkFreeSpaceCall, snapshotSizeCall int + + // restored by TearDownTest + snapstate.EstimateSnapshotSize = func(st *state.State, instanceName string, users []string) (uint64, error) { + snapshotSizeCall++ + // expect two snapshot size estimations + switch instanceName { + case "one": + return 10, nil + case "two": + return 20, nil + default: + c.Fatalf("unexpected snap: %s", instanceName) + } + return 1, nil + } + + restore := snapstate.MockOsutilCheckFreeSpace(func(path string, required uint64) error { + checkFreeSpaceCall++ + // required size is the sum of snapshot sizes of test snaps + c.Check(required, Equals, snapstate.SafetyMarginDiskSpace(30)) + if freeSpaceCheckFail { + return &osutil.NotEnoughDiskSpaceError{} + } + return nil + }) + defer restore() + + var automaticSnapshotCalled bool + snapstate.AutomaticSnapshot = func(st *state.State, instanceName string) (ts *state.TaskSet, err error) { + automaticSnapshotCalled = true + if automaticSnapshot { + t := s.state.NewTask("foo", "") + ts = state.NewTaskSet(t) + return ts, nil + } + // ErrNothingToDo is returned if automatic snapshots are disabled + return nil, snapstate.ErrNothingToDo + } + + tr := config.NewTransaction(s.state) + tr.Set("core", "experimental.check-disk-space-remove", featureFlag) + tr.Commit() + + snapstate.Set(s.state, "one", &snapstate.SnapState{ + Active: true, + SnapType: "app", + Sequence: []*snap.SideInfo{ + {RealName: "one", SnapID: "one-id", Revision: snap.R(1)}, + }, + Current: snap.R(1), + }) + snapstate.Set(s.state, "two", &snapstate.SnapState{ + Active: true, + SnapType: "app", + Sequence: []*snap.SideInfo{ + {RealName: "two", SnapID: "two-id", Revision: snap.R(1)}, + }, + Current: snap.R(1), + }) + + _, _, err := snapstate.RemoveMany(s.state, []string{"one", "two"}) + if featureFlag && automaticSnapshot { + c.Check(snapshotSizeCall, Equals, 2) + c.Check(checkFreeSpaceCall, Equals, 1) + } else { + c.Check(checkFreeSpaceCall, Equals, 0) + c.Check(snapshotSizeCall, Equals, 0) + } + c.Check(automaticSnapshotCalled, Equals, true) + + return err +} + +func (s *snapmgrTestSuite) TestRemoveManyDiskSpaceError(c *C) { + featureFlag := true + automaticSnapshot := true + freeSpaceCheckFail := true + err := s.testRemoveManyDiskSpaceCheck(c, featureFlag, automaticSnapshot, freeSpaceCheckFail) + + diskSpaceErr := err.(*snapstate.InsufficientSpaceError) + c.Check(diskSpaceErr.Path, Equals, filepath.Join(dirs.GlobalRootDir, "/var/lib/snapd")) + c.Check(diskSpaceErr.Snaps, DeepEquals, []string{"one", "two"}) + c.Check(diskSpaceErr.ChangeKind, Equals, "remove") +} + +func (s *snapmgrTestSuite) TestRemoveManyDiskSpaceCheckDisabled(c *C) { + featureFlag := false + automaticSnapshot := true + freeSpaceCheckFail := true + err := s.testRemoveManyDiskSpaceCheck(c, featureFlag, automaticSnapshot, freeSpaceCheckFail) + c.Assert(err, IsNil) +} + +func (s *snapmgrTestSuite) TestRemoveManyDiskSpaceSnapshotDisabled(c *C) { + featureFlag := true + automaticSnapshot := false + freeSpaceCheckFail := true + err := s.testRemoveManyDiskSpaceCheck(c, featureFlag, automaticSnapshot, freeSpaceCheckFail) + c.Assert(err, IsNil) +} + +func (s *snapmgrTestSuite) TestRemoveManyDiskSpaceCheckPasses(c *C) { + featureFlag := true + automaticSnapshot := true + freeSpaceCheckFail := false + err := s.testRemoveManyDiskSpaceCheck(c, featureFlag, automaticSnapshot, freeSpaceCheckFail) + c.Check(err, IsNil) +} + +type snapdBackend struct { + fakeSnappyBackend +} + +func (f *snapdBackend) RemoveSnapData(info *snap.Info) error { + dir := snap.DataDir(info.SnapName(), info.Revision) + if err := os.Remove(dir); err != nil { + return fmt.Errorf("unexpected error: %v", err) + } + return f.fakeSnappyBackend.RemoveSnapData(info) +} + +func (f *snapdBackend) RemoveSnapCommonData(info *snap.Info) error { + dir := snap.CommonDataDir(info.SnapName()) + if err := os.Remove(dir); err != nil { + return fmt.Errorf("unexpected error: %v", err) + } + return f.fakeSnappyBackend.RemoveSnapCommonData(info) +} + +func isUndone(c *C, tasks []*state.Task, kind string, numExpected int) { + var count int + for _, t := range tasks { + if t.Kind() == kind { + c.Assert(t.Status(), Equals, state.UndoneStatus) + count++ + } + } + c.Assert(count, Equals, numExpected) +} + +func injectError(c *C, chg *state.Change, beforeTaskKind string, snapRev snap.Revision) { + var found bool + for _, t := range chg.Tasks() { + if t.Kind() != beforeTaskKind { + continue + } + sup, err := snapstate.TaskSnapSetup(t) + c.Assert(err, IsNil) + if sup.Revision() != snapRev { + continue + } + prev := t.WaitTasks()[0] + terr := chg.State().NewTask("error-trigger", "provoking undo") + t.WaitFor(terr) + terr.WaitFor(prev) + chg.AddTask(terr) + found = true + break + } + c.Assert(found, Equals, true) +} + +func makeTestSnaps(c *C, st *state.State) { + si1 := snap.SideInfo{ + SnapID: "some-snap-id", + RealName: "some-snap", + Revision: snap.R(1), + } + + si2 := snap.SideInfo{ + SnapID: "some-snap-id", + RealName: "some-snap", + Revision: snap.R(2), + } + + snapstate.Set(st, "some-snap", &snapstate.SnapState{ + Active: true, + Sequence: []*snap.SideInfo{&si1, &si2}, + Current: si1.Revision, + SnapType: "app", + }) + + c.Assert(os.MkdirAll(snap.DataDir("some-snap", si1.Revision), 0755), IsNil) + c.Assert(os.MkdirAll(snap.DataDir("some-snap", si2.Revision), 0755), IsNil) + c.Assert(os.MkdirAll(snap.CommonDataDir("some-snap"), 0755), IsNil) +} + +func (s *snapmgrTestSuite) TestRemoveManyUndoRestoresCurrent(c *C) { + b := &snapdBackend{} + snapstate.SetSnapManagerBackend(s.snapmgr, b) + AddForeignTaskHandlers(s.o.TaskRunner(), b) + + s.state.Lock() + defer s.state.Unlock() + makeTestSnaps(c, s.state) + + chg := s.state.NewChange("remove", "remove a snap") + ts, err := snapstate.Remove(s.state, "some-snap", snap.R(0), nil) + c.Assert(err, IsNil) + chg.AddAll(ts) + + // inject an error before clear-snap of revision 1 (current), after + // discard-snap for revision 2, that means data and snap rev 1 + // are still present. + injectError(c, chg, "clear-snap", snap.Revision{N: 1}) + + s.state.Unlock() + defer s.se.Stop() + s.settle(c) + s.state.Lock() + + c.Assert(chg.Status(), Equals, state.ErrorStatus) + isUndone(c, chg.Tasks(), "unlink-snap", 1) + + var snapst snapstate.SnapState + c.Assert(snapstate.Get(s.state, "some-snap", &snapst), IsNil) + c.Check(snapst.Active, Equals, true) + c.Check(snapst.Current, Equals, snap.Revision{N: 1}) + c.Assert(snapst.Sequence, HasLen, 1) + c.Check(snapst.Sequence[0].Revision, Equals, snap.Revision{N: 1}) + + expected := fakeOps{ + { + op: "auto-disconnect:Doing", + name: "some-snap", + revno: snap.R(1), + }, + { + op: "remove-snap-aliases", + name: "some-snap", + }, + { + op: "unlink-snap", + path: filepath.Join(dirs.SnapMountDir, "some-snap/1"), + }, + { + op: "remove-profiles:Doing", + name: "some-snap", + revno: snap.R(1), + }, + { + op: "remove-snap-data", + path: filepath.Join(dirs.SnapMountDir, "some-snap/2"), + }, + { + op: "remove-snap-files", + path: filepath.Join(dirs.SnapMountDir, "some-snap/2"), + stype: "app", + }, + { + op: "remove-profiles:Undoing", + name: "some-snap", + revno: snap.R(1), + }, + { + op: "link-snap", + path: filepath.Join(dirs.SnapMountDir, "some-snap/1"), + }, + { + op: "update-aliases", + }, + } + // start with an easier-to-read error if this fails: + c.Check(len(b.ops), Equals, len(expected)) + c.Assert(b.ops.Ops(), DeepEquals, expected.Ops()) + c.Check(b.ops, DeepEquals, expected) +} + +func (s *snapmgrTestSuite) TestRemoveManyUndoLeavesInactiveSnapAfterDataIsLost(c *C) { + b := &snapdBackend{} + snapstate.SetSnapManagerBackend(s.snapmgr, b) + AddForeignTaskHandlers(s.o.TaskRunner(), b) + + s.state.Lock() + defer s.state.Unlock() + makeTestSnaps(c, s.state) + + chg := s.state.NewChange("remove", "remove a snap") + ts, err := snapstate.Remove(s.state, "some-snap", snap.R(0), nil) + c.Assert(err, IsNil) + chg.AddAll(ts) + + // inject an error after removing data of both revisions (which includes + // current rev 1), before discarding the snap completely. + injectError(c, chg, "discard-snap", snap.Revision{N: 1}) + + s.state.Unlock() + defer s.se.Stop() + s.settle(c) + s.state.Lock() + + c.Assert(chg.Status(), Equals, state.ErrorStatus) + isUndone(c, chg.Tasks(), "unlink-snap", 1) + + var snapst snapstate.SnapState + c.Assert(snapstate.Get(s.state, "some-snap", &snapst), IsNil) + + // revision 1 is still present but not active, since the error happened + // after its data was removed. + c.Check(snapst.Active, Equals, false) + c.Check(snapst.Current, Equals, snap.Revision{N: 1}) + c.Assert(snapst.Sequence, HasLen, 1) + c.Check(snapst.Sequence[0].Revision, Equals, snap.Revision{N: 1}) + + expected := fakeOps{ + { + op: "auto-disconnect:Doing", + name: "some-snap", + revno: snap.R(1), + }, + { + op: "remove-snap-aliases", + name: "some-snap", + }, + { + op: "unlink-snap", + path: filepath.Join(dirs.SnapMountDir, "some-snap/1"), + }, + { + op: "remove-profiles:Doing", + name: "some-snap", + revno: snap.R(1), + }, + { + op: "remove-snap-data", + path: filepath.Join(dirs.SnapMountDir, "some-snap/2"), + }, + { + op: "remove-snap-files", + path: filepath.Join(dirs.SnapMountDir, "some-snap/2"), + stype: "app", + }, + { + op: "remove-snap-data", + path: filepath.Join(dirs.SnapMountDir, "some-snap/1"), + }, + { + op: "remove-snap-common-data", + path: filepath.Join(dirs.SnapMountDir, "some-snap/1"), + }, + { + op: "remove-snap-data-dir", + name: "some-snap", + path: filepath.Join(dirs.SnapDataDir, "some-snap"), + }, + { + op: "remove-profiles:Undoing", + name: "some-snap", + revno: snap.R(1), + }, + { + op: "update-aliases", + }, + } + + // start with an easier-to-read error if this fails: + c.Check(len(b.ops), Equals, len(expected)) + c.Assert(b.ops.Ops(), DeepEquals, expected.Ops()) + c.Check(b.ops, DeepEquals, expected) +} diff -Nru snapd-2.47.1+20.10.1build1/overlord/snapstate/snapstate_test.go snapd-2.48+21.04/overlord/snapstate/snapstate_test.go --- snapd-2.47.1+20.10.1build1/overlord/snapstate/snapstate_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/overlord/snapstate/snapstate_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -42,7 +42,6 @@ "github.com/snapcore/snapd/gadget" "github.com/snapcore/snapd/interfaces" "github.com/snapcore/snapd/logger" - "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/overlord" "github.com/snapcore/snapd/overlord/auth" "github.com/snapcore/snapd/overlord/configstate/config" @@ -959,196 +958,6 @@ c.Check(snapsup.PlugsOnly, Equals, false) } -func (s *snapmgrTestSuite) TestRemoveTasks(c *C) { - s.state.Lock() - defer s.state.Unlock() - - snapstate.Set(s.state, "foo", &snapstate.SnapState{ - Active: true, - Sequence: []*snap.SideInfo{ - {RealName: "foo", Revision: snap.R(11)}, - }, - Current: snap.R(11), - SnapType: "app", - }) - - ts, err := snapstate.Remove(s.state, "foo", snap.R(0), nil) - c.Assert(err, IsNil) - - c.Assert(s.state.TaskCount(), Equals, len(ts.Tasks())) - verifyRemoveTasks(c, ts) -} - -func (s *snapmgrTestSuite) TestRemoveTasksAutoSnapshotDisabled(c *C) { - snapstate.AutomaticSnapshot = func(st *state.State, instanceName string) (ts *state.TaskSet, err error) { - return nil, snapstate.ErrNothingToDo - } - - s.state.Lock() - defer s.state.Unlock() - - snapstate.Set(s.state, "foo", &snapstate.SnapState{ - Active: true, - Sequence: []*snap.SideInfo{ - {RealName: "foo", Revision: snap.R(11)}, - }, - Current: snap.R(11), - SnapType: "app", - }) - - ts, err := snapstate.Remove(s.state, "foo", snap.R(0), nil) - c.Assert(err, IsNil) - - c.Assert(taskKinds(ts.Tasks()), DeepEquals, []string{ - "stop-snap-services", - "run-hook[remove]", - "auto-disconnect", - "remove-aliases", - "unlink-snap", - "remove-profiles", - "clear-snap", - "discard-snap", - }) -} - -func (s *snapmgrTestSuite) TestRemoveTasksAutoSnapshotDisabledByPurgeFlag(c *C) { - s.state.Lock() - defer s.state.Unlock() - - snapstate.Set(s.state, "foo", &snapstate.SnapState{ - Active: true, - Sequence: []*snap.SideInfo{ - {RealName: "foo", Revision: snap.R(11)}, - }, - Current: snap.R(11), - SnapType: "app", - }) - - ts, err := snapstate.Remove(s.state, "foo", snap.R(0), &snapstate.RemoveFlags{Purge: true}) - c.Assert(err, IsNil) - - c.Assert(taskKinds(ts.Tasks()), DeepEquals, []string{ - "stop-snap-services", - "run-hook[remove]", - "auto-disconnect", - "remove-aliases", - "unlink-snap", - "remove-profiles", - "clear-snap", - "discard-snap", - }) -} - -func (s *snapmgrTestSuite) TestRemoveHookNotExecutedIfNotLastRevison(c *C) { - s.state.Lock() - defer s.state.Unlock() - - snapstate.Set(s.state, "foo", &snapstate.SnapState{ - Active: true, - Sequence: []*snap.SideInfo{ - {RealName: "foo", Revision: snap.R(11)}, - {RealName: "foo", Revision: snap.R(12)}, - }, - Current: snap.R(12), - }) - - ts, err := snapstate.Remove(s.state, "foo", snap.R(11), nil) - c.Assert(err, IsNil) - - runHooks := tasksWithKind(ts, "run-hook") - // no 'remove' hook task - c.Assert(runHooks, HasLen, 0) -} - -func (s *snapmgrTestSuite) TestRemoveConflict(c *C) { - s.state.Lock() - defer s.state.Unlock() - - snapstate.Set(s.state, "some-snap", &snapstate.SnapState{ - Active: true, - Sequence: []*snap.SideInfo{{RealName: "some-snap", Revision: snap.R(11)}}, - Current: snap.R(11), - }) - - ts, err := snapstate.Remove(s.state, "some-snap", snap.R(0), nil) - c.Assert(err, IsNil) - // need a change to make the tasks visible - s.state.NewChange("remove", "...").AddAll(ts) - - _, err = snapstate.Remove(s.state, "some-snap", snap.R(0), nil) - c.Assert(err, ErrorMatches, `snap "some-snap" has "remove" change in progress`) -} - -func (s *snapmgrTestSuite) testRemoveDiskSpaceCheck(c *C, featureFlag, automaticSnapshot bool) error { - s.state.Lock() - defer s.state.Unlock() - - restore := snapstate.MockOsutilCheckFreeSpace(func(string, uint64) error { - // osutil.CheckFreeSpace shouldn't be hit if either featureFlag - // or automaticSnapshot is false. If both are true then we return disk - // space error which should result in snapstate.InsufficientSpaceError - // on remove(). - return &osutil.NotEnoughDiskSpaceError{} - }) - defer restore() - - var automaticSnapshotCalled bool - snapstate.AutomaticSnapshot = func(st *state.State, instanceName string) (ts *state.TaskSet, err error) { - automaticSnapshotCalled = true - if automaticSnapshot { - t := s.state.NewTask("foo", "") - ts = state.NewTaskSet(t) - return ts, nil - } - // ErrNothingToDo is returned if automatic snapshots are disabled - return nil, snapstate.ErrNothingToDo - } - - tr := config.NewTransaction(s.state) - tr.Set("core", "experimental.check-disk-space-remove", featureFlag) - tr.Commit() - - snapstate.Set(s.state, "some-snap", &snapstate.SnapState{ - Active: true, - Sequence: []*snap.SideInfo{{RealName: "some-snap", Revision: snap.R(11)}}, - Current: snap.R(11), - SnapType: "app", - }) - - _, err := snapstate.Remove(s.state, "some-snap", snap.R(0), nil) - c.Assert(automaticSnapshotCalled, Equals, true) - return err -} - -func (s *snapmgrTestSuite) TestRemoveDiskSpaceCheckDoesNothingWhenNoSnapshot(c *C) { - featureFlag := true - snapshot := false - err := s.testRemoveDiskSpaceCheck(c, featureFlag, snapshot) - c.Assert(err, IsNil) -} - -func (s *snapmgrTestSuite) TestRemoveDiskSpaceCheckDisabledByFeatureFlag(c *C) { - featureFlag := false - snapshot := true - err := s.testRemoveDiskSpaceCheck(c, featureFlag, snapshot) - c.Assert(err, IsNil) -} - -func (s *snapmgrTestSuite) TestRemoveDiskSpaceForSnapshotError(c *C) { - featureFlag := true - snapshot := true - // both the snapshot and disk check feature are enabled, so we should hit - // the disk check (which fails). - err := s.testRemoveDiskSpaceCheck(c, featureFlag, snapshot) - c.Assert(err, NotNil) - - diskSpaceErr := err.(*snapstate.InsufficientSpaceError) - c.Assert(diskSpaceErr, ErrorMatches, `cannot create automatic snapshot when removing last revision of the snap: insufficient space.*`) - c.Check(diskSpaceErr.Path, Equals, filepath.Join(dirs.GlobalRootDir, "/var/lib/snapd")) - c.Check(diskSpaceErr.Snaps, DeepEquals, []string{"some-snap"}) - c.Check(diskSpaceErr.ChangeKind, Equals, "remove") -} - func (s *snapmgrTestSuite) TestDisableSnapDisabledServicesSaved(c *C) { s.state.Lock() defer s.state.Unlock() @@ -1325,1066 +1134,136 @@ c.Assert(snapst.LastActiveDisabledServices, HasLen, 0) } -func (s *snapmgrTestSuite) TestEnableSnapMissingDisabledServicesMergedAndSaved(c *C) { - s.state.Lock() - defer s.state.Unlock() - - prevCurrentlyDisabled := s.fakeBackend.servicesCurrentlyDisabled - s.fakeBackend.servicesCurrentlyDisabled = []string{"svc1", "svc2"} - - // reset the services to what they were before after the test is done - defer func() { - s.fakeBackend.servicesCurrentlyDisabled = prevCurrentlyDisabled - }() - - snapstate.Set(s.state, "services-snap", &snapstate.SnapState{ - Sequence: []*snap.SideInfo{ - {RealName: "services-snap", Revision: snap.R(11)}, - }, - Current: snap.R(11), - Active: true, - // keep this to make gofmt 1.10 happy - LastActiveDisabledServices: []string{"missing-svc3"}, - }) - - disableChg := s.state.NewChange("disable", "disable a snap") - disableTs, err := snapstate.Disable(s.state, "services-snap") - c.Assert(err, IsNil) - disableChg.AddAll(disableTs) - - s.state.Unlock() - defer s.se.Stop() - s.settle(c) - s.state.Lock() - - c.Assert(disableChg.Err(), IsNil) - c.Assert(disableChg.IsReady(), Equals, true) - - // get the snap state - var snapst snapstate.SnapState - err = snapstate.Get(s.state, "services-snap", &snapst) - c.Assert(err, IsNil) - - // make sure that the disabled services in this snap's state is what we - // provided - sort.Strings(snapst.LastActiveDisabledServices) - c.Assert(snapst.LastActiveDisabledServices, DeepEquals, []string{"missing-svc3", "svc1", "svc2"}) - - enableChg := s.state.NewChange("enable", "disable a snap") - enableTs, err := snapstate.Enable(s.state, "services-snap") - c.Assert(err, IsNil) - enableChg.AddAll(enableTs) - - s.state.Unlock() - defer s.se.Stop() - s.settle(c) - s.state.Lock() - - c.Assert(enableChg.Err(), IsNil) - c.Assert(enableChg.IsReady(), Equals, true) - - // get the snap state again - err = snapstate.Get(s.state, "services-snap", &snapst) - c.Assert(err, IsNil) - - // make sure that there is nothing in the last active disabled services list - // because we re-enabled the snap and there should be nothing we have to - // keep track of in the state anymore - c.Assert(snapst.LastActiveDisabledServices, DeepEquals, []string{"missing-svc3"}) -} - -func (s *snapmgrTestSuite) TestEnableSnapMissingDisabledServicesSaved(c *C) { - s.state.Lock() - defer s.state.Unlock() - - snapstate.Set(s.state, "services-snap", &snapstate.SnapState{ - Sequence: []*snap.SideInfo{ - {RealName: "services-snap", Revision: snap.R(11)}, - }, - Current: snap.R(11), - Active: true, - // keep this to make gofmt 1.10 happy - LastActiveDisabledServices: []string{"missing-svc3"}, - }) - - disableChg := s.state.NewChange("disable", "disable a snap") - disableTs, err := snapstate.Disable(s.state, "services-snap") - c.Assert(err, IsNil) - disableChg.AddAll(disableTs) - - s.state.Unlock() - defer s.se.Stop() - s.settle(c) - s.state.Lock() - - c.Assert(disableChg.Err(), IsNil) - c.Assert(disableChg.IsReady(), Equals, true) - - // get the snap state - var snapst snapstate.SnapState - err = snapstate.Get(s.state, "services-snap", &snapst) - c.Assert(err, IsNil) - - // make sure that the disabled services in this snap's state is what we - // provided - sort.Strings(snapst.LastActiveDisabledServices) - c.Assert(snapst.LastActiveDisabledServices, DeepEquals, []string{"missing-svc3"}) - - enableChg := s.state.NewChange("enable", "disable a snap") - enableTs, err := snapstate.Enable(s.state, "services-snap") - c.Assert(err, IsNil) - enableChg.AddAll(enableTs) - - s.state.Unlock() - defer s.se.Stop() - s.settle(c) - s.state.Lock() - - c.Assert(enableChg.Err(), IsNil) - c.Assert(enableChg.IsReady(), Equals, true) - - // get the snap state again - err = snapstate.Get(s.state, "services-snap", &snapst) - c.Assert(err, IsNil) - - // make sure that there is nothing in the last active disabled services list - // because we re-enabled the snap and there should be nothing we have to - // keep track of in the state anymore - c.Assert(snapst.LastActiveDisabledServices, DeepEquals, []string{"missing-svc3"}) -} - -func makeTestSnap(c *C, snapYamlContent string) (snapFilePath string) { - return snaptest.MakeTestSnapWithFiles(c, snapYamlContent, nil) -} - -func (s *snapmgrTestSuite) TestRemoveRunThrough(c *C) { - c.Assert(snapstate.KeepAuxStoreInfo("some-snap-id", nil), IsNil) - c.Check(snapstate.AuxStoreInfoFilename("some-snap-id"), testutil.FilePresent) - si := snap.SideInfo{ - SnapID: "some-snap-id", - RealName: "some-snap", - Revision: snap.R(7), - } - - s.state.Lock() - defer s.state.Unlock() - - snapstate.Set(s.state, "some-snap", &snapstate.SnapState{ - Active: true, - Sequence: []*snap.SideInfo{&si}, - Current: si.Revision, - SnapType: "app", - }) - - chg := s.state.NewChange("remove", "remove a snap") - ts, err := snapstate.Remove(s.state, "some-snap", snap.R(0), nil) - c.Assert(err, IsNil) - chg.AddAll(ts) - - s.state.Unlock() - defer s.se.Stop() - s.settle(c) - s.state.Lock() - - expected := fakeOps{ - { - op: "auto-disconnect:Doing", - name: "some-snap", - revno: snap.R(7), - }, - { - op: "remove-snap-aliases", - name: "some-snap", - }, - { - op: "unlink-snap", - path: filepath.Join(dirs.SnapMountDir, "some-snap/7"), - }, - { - op: "remove-profiles:Doing", - name: "some-snap", - revno: snap.R(7), - }, - { - op: "remove-snap-data", - path: filepath.Join(dirs.SnapMountDir, "some-snap/7"), - }, - { - op: "remove-snap-common-data", - path: filepath.Join(dirs.SnapMountDir, "some-snap/7"), - }, - { - op: "remove-snap-data-dir", - name: "some-snap", - path: filepath.Join(dirs.SnapDataDir, "some-snap"), - }, - { - op: "remove-snap-files", - path: filepath.Join(dirs.SnapMountDir, "some-snap/7"), - stype: "app", - }, - { - op: "discard-namespace", - name: "some-snap", - }, - { - op: "remove-snap-dir", - name: "some-snap", - path: filepath.Join(dirs.SnapMountDir, "some-snap"), - }, - } - // start with an easier-to-read error if this fails: - c.Check(len(s.fakeBackend.ops), Equals, len(expected)) - c.Assert(s.fakeBackend.ops.Ops(), DeepEquals, expected.Ops()) - c.Check(s.fakeBackend.ops, DeepEquals, expected) - - // verify snapSetup info - tasks := ts.Tasks() - for _, t := range tasks { - if t.Kind() == "run-hook" { - continue - } - if t.Kind() == "save-snapshot" { - continue - } - snapsup, err := snapstate.TaskSnapSetup(t) - c.Assert(err, IsNil) - - var expSnapSetup *snapstate.SnapSetup - switch t.Kind() { - case "discard-conns": - expSnapSetup = &snapstate.SnapSetup{ - SideInfo: &snap.SideInfo{ - RealName: "some-snap", - }, - } - case "clear-snap", "discard-snap": - expSnapSetup = &snapstate.SnapSetup{ - SideInfo: &snap.SideInfo{ - RealName: "some-snap", - SnapID: "some-snap-id", - Revision: snap.R(7), - }, - } - default: - expSnapSetup = &snapstate.SnapSetup{ - SideInfo: &snap.SideInfo{ - RealName: "some-snap", - SnapID: "some-snap-id", - Revision: snap.R(7), - }, - Type: snap.TypeApp, - PlugsOnly: true, - } - - } - - c.Check(snapsup, DeepEquals, expSnapSetup, Commentf(t.Kind())) - } - - // verify snaps in the system state - var snapst snapstate.SnapState - err = snapstate.Get(s.state, "some-snap", &snapst) - c.Assert(err, Equals, state.ErrNoState) - c.Check(snapstate.AuxStoreInfoFilename("some-snap-id"), testutil.FileAbsent) - -} - -func (s *snapmgrTestSuite) TestParallelInstanceRemoveRunThrough(c *C) { - si := snap.SideInfo{ - RealName: "some-snap", - Revision: snap.R(7), - } - - s.state.Lock() - defer s.state.Unlock() - - // pretend we have both a regular snap and a parallel instance - snapstate.Set(s.state, "some-snap_instance", &snapstate.SnapState{ - Active: true, - Sequence: []*snap.SideInfo{&si}, - Current: si.Revision, - SnapType: "app", - InstanceKey: "instance", - }) - snapstate.Set(s.state, "some-snap", &snapstate.SnapState{ - Active: true, - Sequence: []*snap.SideInfo{&si}, - Current: si.Revision, - SnapType: "app", - }) - - chg := s.state.NewChange("remove", "remove a snap") - ts, err := snapstate.Remove(s.state, "some-snap_instance", snap.R(0), nil) - c.Assert(err, IsNil) - chg.AddAll(ts) - - s.state.Unlock() - s.settle(c) - s.state.Lock() - - expected := fakeOps{ - { - op: "auto-disconnect:Doing", - name: "some-snap_instance", - revno: snap.R(7), - }, - { - op: "remove-snap-aliases", - name: "some-snap_instance", - }, - { - op: "unlink-snap", - path: filepath.Join(dirs.SnapMountDir, "some-snap_instance/7"), - }, - { - op: "remove-profiles:Doing", - name: "some-snap_instance", - revno: snap.R(7), - }, - { - op: "remove-snap-data", - path: filepath.Join(dirs.SnapMountDir, "some-snap_instance/7"), - }, - { - op: "remove-snap-common-data", - path: filepath.Join(dirs.SnapMountDir, "some-snap_instance/7"), - }, - { - op: "remove-snap-data-dir", - name: "some-snap_instance", - path: filepath.Join(dirs.SnapDataDir, "some-snap"), - otherInstances: true, - }, - { - op: "remove-snap-files", - path: filepath.Join(dirs.SnapMountDir, "some-snap_instance/7"), - stype: "app", - }, - { - op: "discard-namespace", - name: "some-snap_instance", - }, - { - op: "remove-snap-dir", - name: "some-snap_instance", - path: filepath.Join(dirs.SnapMountDir, "some-snap"), - otherInstances: true, - }, - } - // start with an easier-to-read error if this fails: - c.Check(len(s.fakeBackend.ops), Equals, len(expected)) - c.Assert(s.fakeBackend.ops.Ops(), DeepEquals, expected.Ops()) - c.Check(s.fakeBackend.ops, DeepEquals, expected) - - // verify snapSetup info - tasks := ts.Tasks() - for _, t := range tasks { - if t.Kind() == "run-hook" { - continue - } - if t.Kind() == "save-snapshot" { - continue - } - snapsup, err := snapstate.TaskSnapSetup(t) - c.Assert(err, IsNil) - - var expSnapSetup *snapstate.SnapSetup - switch t.Kind() { - case "discard-conns": - expSnapSetup = &snapstate.SnapSetup{ - SideInfo: &snap.SideInfo{ - RealName: "some-snap", - }, - InstanceKey: "instance", - } - case "clear-snap", "discard-snap": - expSnapSetup = &snapstate.SnapSetup{ - SideInfo: &snap.SideInfo{ - RealName: "some-snap", - Revision: snap.R(7), - }, - InstanceKey: "instance", - } - default: - expSnapSetup = &snapstate.SnapSetup{ - SideInfo: &snap.SideInfo{ - RealName: "some-snap", - Revision: snap.R(7), - }, - Type: snap.TypeApp, - PlugsOnly: true, - InstanceKey: "instance", - } - - } - - c.Check(snapsup, DeepEquals, expSnapSetup, Commentf(t.Kind())) - } - - // verify snaps in the system state - var snapst snapstate.SnapState - err = snapstate.Get(s.state, "some-snap_instance", &snapst) - c.Assert(err, Equals, state.ErrNoState) - - // the non-instance snap is still there - err = snapstate.Get(s.state, "some-snap", &snapst) - c.Assert(err, IsNil) -} - -func (s *snapmgrTestSuite) TestParallelInstanceRemoveRunThroughOtherInstances(c *C) { - si := snap.SideInfo{ - RealName: "some-snap", - Revision: snap.R(7), - } - - s.state.Lock() - defer s.state.Unlock() - - // pretend we have both a regular snap and a parallel instance - snapstate.Set(s.state, "some-snap_instance", &snapstate.SnapState{ - Active: true, - Sequence: []*snap.SideInfo{&si}, - Current: si.Revision, - SnapType: "app", - InstanceKey: "instance", - }) - snapstate.Set(s.state, "some-snap_other", &snapstate.SnapState{ - Active: true, - Sequence: []*snap.SideInfo{&si}, - Current: si.Revision, - SnapType: "app", - InstanceKey: "other", - }) - - chg := s.state.NewChange("remove", "remove a snap") - ts, err := snapstate.Remove(s.state, "some-snap_instance", snap.R(0), nil) - c.Assert(err, IsNil) - chg.AddAll(ts) - - s.state.Unlock() - s.settle(c) - s.state.Lock() - - expected := fakeOps{ - { - op: "auto-disconnect:Doing", - name: "some-snap_instance", - revno: snap.R(7), - }, - { - op: "remove-snap-aliases", - name: "some-snap_instance", - }, - { - op: "unlink-snap", - path: filepath.Join(dirs.SnapMountDir, "some-snap_instance/7"), - }, - { - op: "remove-profiles:Doing", - name: "some-snap_instance", - revno: snap.R(7), - }, - { - op: "remove-snap-data", - path: filepath.Join(dirs.SnapMountDir, "some-snap_instance/7"), - }, - { - op: "remove-snap-common-data", - path: filepath.Join(dirs.SnapMountDir, "some-snap_instance/7"), - }, - { - op: "remove-snap-data-dir", - name: "some-snap_instance", - path: filepath.Join(dirs.SnapDataDir, "some-snap"), - otherInstances: true, - }, - { - op: "remove-snap-files", - path: filepath.Join(dirs.SnapMountDir, "some-snap_instance/7"), - stype: "app", - }, - { - op: "discard-namespace", - name: "some-snap_instance", - }, - { - op: "remove-snap-dir", - name: "some-snap_instance", - path: filepath.Join(dirs.SnapMountDir, "some-snap"), - otherInstances: true, - }, - } - // start with an easier-to-read error if this fails: - c.Check(len(s.fakeBackend.ops), Equals, len(expected)) - c.Assert(s.fakeBackend.ops.Ops(), DeepEquals, expected.Ops()) - c.Check(s.fakeBackend.ops, DeepEquals, expected) - - // verify snaps in the system state - var snapst snapstate.SnapState - err = snapstate.Get(s.state, "some-snap_instance", &snapst) - c.Assert(err, Equals, state.ErrNoState) - - // the other instance is still there - err = snapstate.Get(s.state, "some-snap_other", &snapst) - c.Assert(err, IsNil) -} - -func (s *snapmgrTestSuite) TestRemoveWithManyRevisionsRunThrough(c *C) { - si3 := snap.SideInfo{ - SnapID: "some-snap-id", - RealName: "some-snap", - Revision: snap.R(3), - } - - si5 := snap.SideInfo{ - SnapID: "some-snap-id", - RealName: "some-snap", - Revision: snap.R(5), - } - - si7 := snap.SideInfo{ - SnapID: "some-snap-id", - RealName: "some-snap", - Revision: snap.R(7), - } - - s.state.Lock() - defer s.state.Unlock() - - snapstate.Set(s.state, "some-snap", &snapstate.SnapState{ - Active: true, - Sequence: []*snap.SideInfo{&si5, &si3, &si7}, - Current: si7.Revision, - SnapType: "app", - }) - - chg := s.state.NewChange("remove", "remove a snap") - ts, err := snapstate.Remove(s.state, "some-snap", snap.R(0), nil) - c.Assert(err, IsNil) - chg.AddAll(ts) - - s.state.Unlock() - defer s.se.Stop() - s.settle(c) - s.state.Lock() - - expected := fakeOps{ - { - op: "auto-disconnect:Doing", - name: "some-snap", - revno: snap.R(7), - }, - { - op: "remove-snap-aliases", - name: "some-snap", - }, - { - op: "unlink-snap", - path: filepath.Join(dirs.SnapMountDir, "some-snap/7"), - }, - { - op: "remove-profiles:Doing", - name: "some-snap", - revno: snap.R(7), - }, - { - op: "remove-snap-data", - path: filepath.Join(dirs.SnapMountDir, "some-snap/7"), - }, - { - op: "remove-snap-files", - path: filepath.Join(dirs.SnapMountDir, "some-snap/7"), - stype: "app", - }, - { - op: "remove-snap-data", - path: filepath.Join(dirs.SnapMountDir, "some-snap/3"), - }, - { - op: "remove-snap-files", - path: filepath.Join(dirs.SnapMountDir, "some-snap/3"), - stype: "app", - }, - { - op: "remove-snap-data", - path: filepath.Join(dirs.SnapMountDir, "some-snap/5"), - }, - { - op: "remove-snap-common-data", - path: filepath.Join(dirs.SnapMountDir, "some-snap/5"), - }, - { - op: "remove-snap-data-dir", - name: "some-snap", - path: filepath.Join(dirs.SnapDataDir, "some-snap"), - }, - { - op: "remove-snap-files", - path: filepath.Join(dirs.SnapMountDir, "some-snap/5"), - stype: "app", - }, - { - op: "discard-namespace", - name: "some-snap", - }, - { - op: "remove-snap-dir", - name: "some-snap", - path: filepath.Join(dirs.SnapMountDir, "some-snap"), - }, - } - // start with an easier-to-read error if this fails: - c.Assert(s.fakeBackend.ops.Ops(), DeepEquals, expected.Ops()) - c.Assert(s.fakeBackend.ops, DeepEquals, expected) - - // verify snapSetup info - tasks := ts.Tasks() - revnos := []snap.Revision{{N: 7}, {N: 3}, {N: 5}} - whichRevno := 0 - for _, t := range tasks { - if t.Kind() == "run-hook" { - continue - } - if t.Kind() == "save-snapshot" { - continue - } - snapsup, err := snapstate.TaskSnapSetup(t) - c.Assert(err, IsNil) - - var expSnapSetup *snapstate.SnapSetup - switch t.Kind() { - case "discard-conns": - expSnapSetup = &snapstate.SnapSetup{ - SideInfo: &snap.SideInfo{ - SnapID: "some-snap-id", - RealName: "some-snap", - }, - } - case "clear-snap", "discard-snap": - expSnapSetup = &snapstate.SnapSetup{ - SideInfo: &snap.SideInfo{ - SnapID: "some-snap-id", - RealName: "some-snap", - Revision: revnos[whichRevno], - }, - } - default: - expSnapSetup = &snapstate.SnapSetup{ - SideInfo: &snap.SideInfo{ - SnapID: "some-snap-id", - RealName: "some-snap", - Revision: snap.R(7), - }, - Type: snap.TypeApp, - PlugsOnly: true, - } - - } - - c.Check(snapsup, DeepEquals, expSnapSetup, Commentf(t.Kind())) - - if t.Kind() == "discard-snap" { - whichRevno++ - } - } - - // verify snaps in the system state - var snapst snapstate.SnapState - err = snapstate.Get(s.state, "some-snap", &snapst) - c.Assert(err, Equals, state.ErrNoState) -} - -func (s *snapmgrTestSuite) TestRemoveOneRevisionRunThrough(c *C) { - si3 := snap.SideInfo{ - RealName: "some-snap", - Revision: snap.R(3), - } - - si5 := snap.SideInfo{ - RealName: "some-snap", - Revision: snap.R(5), - } - - si7 := snap.SideInfo{ - RealName: "some-snap", - Revision: snap.R(7), - } - - s.state.Lock() - defer s.state.Unlock() - - snapstate.Set(s.state, "some-snap", &snapstate.SnapState{ - Active: true, - Sequence: []*snap.SideInfo{&si5, &si3, &si7}, - Current: si7.Revision, - SnapType: "app", - }) - - chg := s.state.NewChange("remove", "remove a snap") - ts, err := snapstate.Remove(s.state, "some-snap", snap.R(3), nil) - c.Assert(err, IsNil) - chg.AddAll(ts) - - s.state.Unlock() - defer s.se.Stop() - s.settle(c) - s.state.Lock() - - c.Check(len(s.fakeBackend.ops), Equals, 2) - expected := fakeOps{ - { - op: "remove-snap-data", - path: filepath.Join(dirs.SnapMountDir, "some-snap/3"), - }, - { - op: "remove-snap-files", - path: filepath.Join(dirs.SnapMountDir, "some-snap/3"), - stype: "app", - }, - } - // start with an easier-to-read error if this fails: - c.Assert(s.fakeBackend.ops.Ops(), DeepEquals, expected.Ops()) - c.Assert(s.fakeBackend.ops, DeepEquals, expected) - - // verify snapSetup info - tasks := ts.Tasks() - for _, t := range tasks { - if t.Kind() == "save-snapshot" { - continue - } - snapsup, err := snapstate.TaskSnapSetup(t) - c.Assert(err, IsNil) - - expSnapSetup := &snapstate.SnapSetup{ - SideInfo: &snap.SideInfo{ - RealName: "some-snap", - Revision: snap.R(3), - }, - } - - c.Check(snapsup, DeepEquals, expSnapSetup, Commentf(t.Kind())) - } - - // verify snaps in the system state - var snapst snapstate.SnapState - err = snapstate.Get(s.state, "some-snap", &snapst) - c.Assert(err, IsNil) - c.Check(snapst.Sequence, HasLen, 2) -} - -func (s *snapmgrTestSuite) TestRemoveLastRevisionRunThrough(c *C) { - si := snap.SideInfo{ - RealName: "some-snap", - Revision: snap.R(2), - } - - s.state.Lock() - defer s.state.Unlock() - - snapstate.Set(s.state, "some-snap", &snapstate.SnapState{ - Active: false, - Sequence: []*snap.SideInfo{&si}, - Current: si.Revision, - SnapType: "app", - }) - - chg := s.state.NewChange("remove", "remove a snap") - ts, err := snapstate.Remove(s.state, "some-snap", snap.R(2), nil) - c.Assert(err, IsNil) - chg.AddAll(ts) - - s.state.Unlock() - defer s.se.Stop() - s.settle(c) - s.state.Lock() - - c.Check(len(s.fakeBackend.ops), Equals, 7) - expected := fakeOps{ - { - op: "auto-disconnect:Doing", - name: "some-snap", - revno: snap.R(2), - }, - { - op: "remove-snap-data", - path: filepath.Join(dirs.SnapMountDir, "some-snap/2"), - }, - { - op: "remove-snap-common-data", - path: filepath.Join(dirs.SnapMountDir, "some-snap/2"), - }, - { - op: "remove-snap-data-dir", - name: "some-snap", - path: filepath.Join(dirs.SnapDataDir, "some-snap"), - }, - { - op: "remove-snap-files", - path: filepath.Join(dirs.SnapMountDir, "some-snap/2"), - stype: "app", - }, - { - op: "discard-namespace", - name: "some-snap", - }, - { - op: "remove-snap-dir", - name: "some-snap", - path: filepath.Join(dirs.SnapMountDir, "some-snap"), - }, - } - // start with an easier-to-read error if this fails: - c.Assert(s.fakeBackend.ops.Ops(), DeepEquals, expected.Ops()) - c.Assert(s.fakeBackend.ops, DeepEquals, expected) - - // verify snapSetup info - tasks := ts.Tasks() - for _, t := range tasks { - if t.Kind() == "run-hook" { - continue - } - if t.Kind() == "save-snapshot" { - continue - } - snapsup, err := snapstate.TaskSnapSetup(t) - c.Assert(err, IsNil) - - expSnapSetup := &snapstate.SnapSetup{ - SideInfo: &snap.SideInfo{ - RealName: "some-snap", - }, - } - if t.Kind() != "discard-conns" { - expSnapSetup.SideInfo.Revision = snap.R(2) - } - if t.Kind() == "auto-disconnect" { - expSnapSetup.PlugsOnly = true - expSnapSetup.Type = "app" - } - - c.Check(snapsup, DeepEquals, expSnapSetup, Commentf(t.Kind())) - } - - // verify snaps in the system state - var snapst snapstate.SnapState - err = snapstate.Get(s.state, "some-snap", &snapst) - c.Assert(err, Equals, state.ErrNoState) -} - -func (s *snapmgrTestSuite) TestRemoveCurrentActiveRevisionRefused(c *C) { - si := snap.SideInfo{ - RealName: "some-snap", - Revision: snap.R(2), - } - - s.state.Lock() - defer s.state.Unlock() - - snapstate.Set(s.state, "some-snap", &snapstate.SnapState{ - Active: true, - Sequence: []*snap.SideInfo{&si}, - Current: si.Revision, - SnapType: "app", - }) - - _, err := snapstate.Remove(s.state, "some-snap", snap.R(2), nil) - - c.Check(err, ErrorMatches, `cannot remove active revision 2 of snap "some-snap"`) -} - -func (s *snapmgrTestSuite) TestRemoveCurrentRevisionOfSeveralRefused(c *C) { - si := snap.SideInfo{ - RealName: "some-snap", - Revision: snap.R(2), - } - - s.state.Lock() - defer s.state.Unlock() - - snapstate.Set(s.state, "some-snap", &snapstate.SnapState{ - Active: true, - Sequence: []*snap.SideInfo{&si, &si}, - Current: si.Revision, - SnapType: "app", - }) - - _, err := snapstate.Remove(s.state, "some-snap", snap.R(2), nil) - c.Assert(err, NotNil) - c.Check(err.Error(), Equals, `cannot remove active revision 2 of snap "some-snap" (revert first?)`) -} - -func (s *snapmgrTestSuite) TestRemoveMissingRevisionRefused(c *C) { - si := snap.SideInfo{ - RealName: "some-snap", - Revision: snap.R(2), - } - - s.state.Lock() - defer s.state.Unlock() - - snapstate.Set(s.state, "some-snap", &snapstate.SnapState{ - Active: true, - Sequence: []*snap.SideInfo{&si}, - Current: si.Revision, - SnapType: "app", - }) - - _, err := snapstate.Remove(s.state, "some-snap", snap.R(1), nil) - - c.Check(err, ErrorMatches, `revision 1 of snap "some-snap" is not installed`) -} - -func (s *snapmgrTestSuite) TestRemoveRefused(c *C) { - si := snap.SideInfo{ - RealName: "brand-gadget", - Revision: snap.R(7), - } - - s.state.Lock() - defer s.state.Unlock() - - snapstate.Set(s.state, "brand-gadget", &snapstate.SnapState{ - Active: true, - Sequence: []*snap.SideInfo{&si}, - Current: si.Revision, - SnapType: "gadget", - }) - - _, err := snapstate.Remove(s.state, "brand-gadget", snap.R(0), nil) - - c.Check(err, ErrorMatches, `snap "brand-gadget" is not removable: snap is used by the model`) -} - -func (s *snapmgrTestSuite) TestRemoveRefusedLastRevision(c *C) { - si := snap.SideInfo{ - RealName: "brand-gadget", - Revision: snap.R(7), - } - - s.state.Lock() - defer s.state.Unlock() - - snapstate.Set(s.state, "brand-gadget", &snapstate.SnapState{ - Active: false, - Sequence: []*snap.SideInfo{&si}, - Current: si.Revision, - SnapType: "gadget", - }) - - _, err := snapstate.Remove(s.state, "brand-gadget", snap.R(7), nil) - - c.Check(err, ErrorMatches, `snap "brand-gadget" is not removable: snap is used by the model`) -} - -func (s *snapmgrTestSuite) TestRemoveDeletesConfigOnLastRevision(c *C) { - si := snap.SideInfo{ - RealName: "some-snap", - Revision: snap.R(7), - } - +func (s *snapmgrTestSuite) TestEnableSnapMissingDisabledServicesMergedAndSaved(c *C) { s.state.Lock() defer s.state.Unlock() - snapstate.Set(s.state, "some-snap", &snapstate.SnapState{ - Active: true, - Sequence: []*snap.SideInfo{&si}, - Current: si.Revision, - SnapType: "app", - }) + prevCurrentlyDisabled := s.fakeBackend.servicesCurrentlyDisabled + s.fakeBackend.servicesCurrentlyDisabled = []string{"svc1", "svc2"} - snapstate.Set(s.state, "another-snap", &snapstate.SnapState{ - Active: true, - Sequence: []*snap.SideInfo{&si}, - Current: si.Revision, - SnapType: "app", + // reset the services to what they were before after the test is done + defer func() { + s.fakeBackend.servicesCurrentlyDisabled = prevCurrentlyDisabled + }() + + snapstate.Set(s.state, "services-snap", &snapstate.SnapState{ + Sequence: []*snap.SideInfo{ + {RealName: "services-snap", Revision: snap.R(11)}, + }, + Current: snap.R(11), + Active: true, + // keep this to make gofmt 1.10 happy + LastActiveDisabledServices: []string{"missing-svc3"}, }) - tr := config.NewTransaction(s.state) - tr.Set("some-snap", "foo", "bar") - tr.Commit() + disableChg := s.state.NewChange("disable", "disable a snap") + disableTs, err := snapstate.Disable(s.state, "services-snap") + c.Assert(err, IsNil) + disableChg.AddAll(disableTs) - // a config for some other snap to verify its not accidentally destroyed - tr = config.NewTransaction(s.state) - tr.Set("another-snap", "bar", "baz") - tr.Commit() + s.state.Unlock() + defer s.se.Stop() + s.settle(c) + s.state.Lock() - var res string - tr = config.NewTransaction(s.state) - c.Assert(tr.Get("some-snap", "foo", &res), IsNil) - c.Assert(tr.Get("another-snap", "bar", &res), IsNil) + c.Assert(disableChg.Err(), IsNil) + c.Assert(disableChg.IsReady(), Equals, true) - chg := s.state.NewChange("remove", "remove a snap") - ts, err := snapstate.Remove(s.state, "some-snap", snap.R(0), nil) + // get the snap state + var snapst snapstate.SnapState + err = snapstate.Get(s.state, "services-snap", &snapst) c.Assert(err, IsNil) - chg.AddAll(ts) + + // make sure that the disabled services in this snap's state is what we + // provided + sort.Strings(snapst.LastActiveDisabledServices) + c.Assert(snapst.LastActiveDisabledServices, DeepEquals, []string{"missing-svc3", "svc1", "svc2"}) + + enableChg := s.state.NewChange("enable", "disable a snap") + enableTs, err := snapstate.Enable(s.state, "services-snap") + c.Assert(err, IsNil) + enableChg.AddAll(enableTs) s.state.Unlock() defer s.se.Stop() s.settle(c) s.state.Lock() - // verify snaps in the system state - var snapst snapstate.SnapState - err = snapstate.Get(s.state, "some-snap", &snapst) - c.Assert(err, Equals, state.ErrNoState) + c.Assert(enableChg.Err(), IsNil) + c.Assert(enableChg.IsReady(), Equals, true) - tr = config.NewTransaction(s.state) - err = tr.Get("some-snap", "foo", &res) - c.Assert(err, NotNil) - c.Assert(err, ErrorMatches, `snap "some-snap" has no "foo" configuration option`) + // get the snap state again + err = snapstate.Get(s.state, "services-snap", &snapst) + c.Assert(err, IsNil) - // and another snap has its config intact - c.Assert(tr.Get("another-snap", "bar", &res), IsNil) - c.Assert(res, Equals, "baz") + // make sure that there is nothing in the last active disabled services list + // because we re-enabled the snap and there should be nothing we have to + // keep track of in the state anymore + c.Assert(snapst.LastActiveDisabledServices, DeepEquals, []string{"missing-svc3"}) } -func (s *snapmgrTestSuite) TestRemoveDoesntDeleteConfigIfNotLastRevision(c *C) { - si1 := snap.SideInfo{ - RealName: "some-snap", - Revision: snap.R(7), - } - si2 := snap.SideInfo{ - RealName: "some-snap", - Revision: snap.R(8), - } - +func (s *snapmgrTestSuite) TestEnableSnapMissingDisabledServicesSaved(c *C) { s.state.Lock() defer s.state.Unlock() - snapstate.Set(s.state, "some-snap", &snapstate.SnapState{ - Active: true, - Sequence: []*snap.SideInfo{&si1, &si2}, - Current: si2.Revision, - SnapType: "app", + snapstate.Set(s.state, "services-snap", &snapstate.SnapState{ + Sequence: []*snap.SideInfo{ + {RealName: "services-snap", Revision: snap.R(11)}, + }, + Current: snap.R(11), + Active: true, + // keep this to make gofmt 1.10 happy + LastActiveDisabledServices: []string{"missing-svc3"}, }) - tr := config.NewTransaction(s.state) - tr.Set("some-snap", "foo", "bar") - tr.Commit() + disableChg := s.state.NewChange("disable", "disable a snap") + disableTs, err := snapstate.Disable(s.state, "services-snap") + c.Assert(err, IsNil) + disableChg.AddAll(disableTs) - var res string - tr = config.NewTransaction(s.state) - c.Assert(tr.Get("some-snap", "foo", &res), IsNil) + s.state.Unlock() + defer s.se.Stop() + s.settle(c) + s.state.Lock() + + c.Assert(disableChg.Err(), IsNil) + c.Assert(disableChg.IsReady(), Equals, true) - chg := s.state.NewChange("remove", "remove a snap") - ts, err := snapstate.Remove(s.state, "some-snap", si1.Revision, nil) + // get the snap state + var snapst snapstate.SnapState + err = snapstate.Get(s.state, "services-snap", &snapst) c.Assert(err, IsNil) - chg.AddAll(ts) + + // make sure that the disabled services in this snap's state is what we + // provided + sort.Strings(snapst.LastActiveDisabledServices) + c.Assert(snapst.LastActiveDisabledServices, DeepEquals, []string{"missing-svc3"}) + + enableChg := s.state.NewChange("enable", "disable a snap") + enableTs, err := snapstate.Enable(s.state, "services-snap") + c.Assert(err, IsNil) + enableChg.AddAll(enableTs) s.state.Unlock() defer s.se.Stop() s.settle(c) s.state.Lock() - // verify snaps in the system state - var snapst snapstate.SnapState - err = snapstate.Get(s.state, "some-snap", &snapst) + c.Assert(enableChg.Err(), IsNil) + c.Assert(enableChg.IsReady(), Equals, true) + + // get the snap state again + err = snapstate.Get(s.state, "services-snap", &snapst) c.Assert(err, IsNil) - tr = config.NewTransaction(s.state) - c.Assert(tr.Get("some-snap", "foo", &res), IsNil) - c.Assert(res, Equals, "bar") + // make sure that there is nothing in the last active disabled services list + // because we re-enabled the snap and there should be nothing we have to + // keep track of in the state anymore + c.Assert(snapst.LastActiveDisabledServices, DeepEquals, []string{"missing-svc3"}) +} + +func makeTestSnap(c *C, snapYamlContent string) (snapFilePath string) { + return snaptest.MakeTestSnapWithFiles(c, snapYamlContent, nil) } func (s *snapmgrTestSuite) TestRevertRestoresConfigSnapshot(c *C) { @@ -4349,7 +3228,7 @@ c.Assert(l, HasLen, 1) } -func (s *snapmgrTestSuite) TestWaitRestartBasics(c *C) { +func (s *snapmgrTestSuite) TestFinishRestartBasics(c *C) { r := release.MockOnClassic(true) defer r() @@ -4364,15 +3243,77 @@ si := &snap.SideInfo{RealName: "some-app"} snaptest.MockSnap(c, "name: some-app\nversion: 1", si) snapsup := &snapstate.SnapSetup{SideInfo: si} - err := snapstate.WaitRestart(task, snapsup) + err := snapstate.FinishRestart(task, snapsup) c.Check(err, IsNil) // restarting ... we always wait state.MockRestarting(st, state.RestartDaemon) - err = snapstate.WaitRestart(task, snapsup) + err = snapstate.FinishRestart(task, snapsup) c.Check(err, FitsTypeOf, &state.Retry{}) } +func (s *snapmgrTestSuite) TestFinishRestartGeneratesSnapdWrappersOnCore(c *C) { + r := release.MockOnClassic(false) + defer r() + + var generateWrappersCalled bool + restore := snapstate.MockGenerateSnapdWrappers(func(snapInfo *snap.Info) error { + c.Assert(snapInfo.SnapName(), Equals, "snapd") + generateWrappersCalled = true + return nil + }) + defer restore() + + st := s.state + st.Lock() + defer st.Unlock() + + for i, tc := range []struct { + onClassic bool + expectedWrappersCall bool + snapName string + snapYaml string + }{ + { + onClassic: false, + snapName: "snapd", + snapYaml: `name: snapd +type: snapd +`, + expectedWrappersCall: true, + }, + { + onClassic: true, + snapName: "snapd", + snapYaml: `name: snapd +type: snapd +`, + expectedWrappersCall: false, + }, + { + onClassic: false, + snapName: "some-snap", + snapYaml: `name: some-snap`, + expectedWrappersCall: false, + }, + } { + generateWrappersCalled = false + release.MockOnClassic(tc.onClassic) + + task := st.NewTask("auto-connect", "...") + si := &snap.SideInfo{Revision: snap.R("x2"), RealName: tc.snapName} + snapInfo := snaptest.MockSnapCurrent(c, string(tc.snapYaml), si) + snapsup := &snapstate.SnapSetup{SideInfo: si, Type: snapInfo.SnapType} + + // restarting + state.MockRestarting(st, state.RestartUnset) + c.Assert(snapstate.FinishRestart(task, snapsup), IsNil) + c.Check(generateWrappersCalled, Equals, tc.expectedWrappersCall, Commentf("#%d: %v", i, tc)) + + c.Assert(os.RemoveAll(filepath.Join(snap.BaseDir(snapInfo.SnapName()), "current")), IsNil) + } +} + type snapmgrQuerySuite struct { st *state.State restore func() @@ -5269,165 +4210,6 @@ c.Assert(snapst.LocalRevision().Unset(), Equals, true) } -func (s *snapmgrTestSuite) TestRemoveMany(c *C) { - s.state.Lock() - defer s.state.Unlock() - - snapstate.Set(s.state, "one", &snapstate.SnapState{ - Active: true, - Sequence: []*snap.SideInfo{ - {RealName: "one", SnapID: "one-id", Revision: snap.R(1)}, - }, - Current: snap.R(1), - }) - snapstate.Set(s.state, "two", &snapstate.SnapState{ - Active: true, - Sequence: []*snap.SideInfo{ - {RealName: "two", SnapID: "two-id", Revision: snap.R(1)}, - }, - Current: snap.R(1), - }) - - removed, tts, err := snapstate.RemoveMany(s.state, []string{"one", "two"}) - c.Assert(err, IsNil) - c.Assert(tts, HasLen, 2) - c.Check(removed, DeepEquals, []string{"one", "two"}) - - c.Assert(s.state.TaskCount(), Equals, 8*2) - for i, ts := range tts { - c.Assert(taskKinds(ts.Tasks()), DeepEquals, []string{ - "stop-snap-services", - "run-hook[remove]", - "auto-disconnect", - "remove-aliases", - "unlink-snap", - "remove-profiles", - "clear-snap", - "discard-snap", - }) - verifyStopReason(c, ts, "remove") - // check that tasksets are in separate lanes - for _, t := range ts.Tasks() { - c.Assert(t.Lanes(), DeepEquals, []int{i + 1}) - } - - } -} - -func (s *snapmgrTestSuite) testRemoveManyDiskSpaceCheck(c *C, featureFlag, automaticSnapshot, freeSpaceCheckFail bool) error { - s.state.Lock() - defer s.state.Unlock() - - var checkFreeSpaceCall, snapshotSizeCall int - - // restored by TearDownTest - snapstate.EstimateSnapshotSize = func(st *state.State, instanceName string, users []string) (uint64, error) { - snapshotSizeCall++ - // expect two snapshot size estimations - switch instanceName { - case "one": - return 10, nil - case "two": - return 20, nil - default: - c.Fatalf("unexpected snap: %s", instanceName) - } - return 1, nil - } - - restore := snapstate.MockOsutilCheckFreeSpace(func(path string, required uint64) error { - checkFreeSpaceCall++ - // required size is the sum of snapshot sizes of test snaps - c.Check(required, Equals, snapstate.SafetyMarginDiskSpace(30)) - if freeSpaceCheckFail { - return &osutil.NotEnoughDiskSpaceError{} - } - return nil - }) - defer restore() - - var automaticSnapshotCalled bool - snapstate.AutomaticSnapshot = func(st *state.State, instanceName string) (ts *state.TaskSet, err error) { - automaticSnapshotCalled = true - if automaticSnapshot { - t := s.state.NewTask("foo", "") - ts = state.NewTaskSet(t) - return ts, nil - } - // ErrNothingToDo is returned if automatic snapshots are disabled - return nil, snapstate.ErrNothingToDo - } - - tr := config.NewTransaction(s.state) - tr.Set("core", "experimental.check-disk-space-remove", featureFlag) - tr.Commit() - - snapstate.Set(s.state, "one", &snapstate.SnapState{ - Active: true, - SnapType: "app", - Sequence: []*snap.SideInfo{ - {RealName: "one", SnapID: "one-id", Revision: snap.R(1)}, - }, - Current: snap.R(1), - }) - snapstate.Set(s.state, "two", &snapstate.SnapState{ - Active: true, - SnapType: "app", - Sequence: []*snap.SideInfo{ - {RealName: "two", SnapID: "two-id", Revision: snap.R(1)}, - }, - Current: snap.R(1), - }) - - _, _, err := snapstate.RemoveMany(s.state, []string{"one", "two"}) - if featureFlag && automaticSnapshot { - c.Check(snapshotSizeCall, Equals, 2) - c.Check(checkFreeSpaceCall, Equals, 1) - } else { - c.Check(checkFreeSpaceCall, Equals, 0) - c.Check(snapshotSizeCall, Equals, 0) - } - c.Check(automaticSnapshotCalled, Equals, true) - - return err -} - -func (s *snapmgrTestSuite) TestRemoveManyDiskSpaceError(c *C) { - featureFlag := true - automaticSnapshot := true - freeSpaceCheckFail := true - err := s.testRemoveManyDiskSpaceCheck(c, featureFlag, automaticSnapshot, freeSpaceCheckFail) - - diskSpaceErr := err.(*snapstate.InsufficientSpaceError) - c.Check(diskSpaceErr.Path, Equals, filepath.Join(dirs.GlobalRootDir, "/var/lib/snapd")) - c.Check(diskSpaceErr.Snaps, DeepEquals, []string{"one", "two"}) - c.Check(diskSpaceErr.ChangeKind, Equals, "remove") -} - -func (s *snapmgrTestSuite) TestRemoveManyDiskSpaceCheckDisabled(c *C) { - featureFlag := false - automaticSnapshot := true - freeSpaceCheckFail := true - err := s.testRemoveManyDiskSpaceCheck(c, featureFlag, automaticSnapshot, freeSpaceCheckFail) - c.Assert(err, IsNil) -} - -func (s *snapmgrTestSuite) TestRemoveManyDiskSpaceSnapshotDisabled(c *C) { - featureFlag := true - automaticSnapshot := false - freeSpaceCheckFail := true - err := s.testRemoveManyDiskSpaceCheck(c, featureFlag, automaticSnapshot, freeSpaceCheckFail) - c.Assert(err, IsNil) -} - -func (s *snapmgrTestSuite) TestRemoveManyDiskSpaceCheckPasses(c *C) { - featureFlag := true - automaticSnapshot := true - freeSpaceCheckFail := false - err := s.testRemoveManyDiskSpaceCheck(c, featureFlag, automaticSnapshot, freeSpaceCheckFail) - c.Check(err, IsNil) -} - func tasksWithKind(ts *state.TaskSet, kind string) []*state.Task { var tasks []*state.Task for _, task := range ts.Tasks() { @@ -5967,6 +4749,10 @@ name: "ubuntu-core", }, { + op: "remove-inhibit-lock", + name: "ubuntu-core", + }, + { op: "remove-snap-dir", name: "ubuntu-core", path: filepath.Join(dirs.SnapMountDir, "ubuntu-core"), @@ -6063,6 +4849,10 @@ name: "ubuntu-core", }, { + op: "remove-inhibit-lock", + name: "ubuntu-core", + }, + { op: "remove-snap-dir", name: "ubuntu-core", path: filepath.Join(dirs.SnapMountDir, "ubuntu-core"), @@ -7533,3 +6323,62 @@ RequireTypeBase: false, }) } + +func (s *snapmgrTestSuite) TestEnsureAutoRefreshesAreDelayed(c *C) { + s.state.Lock() + defer s.state.Unlock() + + t0 := time.Now() + // with no changes in flight still works and we set the auto-refresh time as + // at least one minute past the start of the test + chgs, err := s.snapmgr.EnsureAutoRefreshesAreDelayed(time.Minute) + c.Assert(err, IsNil) + c.Assert(chgs, HasLen, 0) + + var holdTime time.Time + tr := config.NewTransaction(s.state) + err = tr.Get("core", "refresh.hold", &holdTime) + c.Assert(err, IsNil) + // use After() == false in case holdTime is _exactly_ one minute later than + // t0, in which case both After() and Before() will be false + c.Assert(t0.Add(time.Minute).After(holdTime), Equals, false) + + // now make some auto-refresh changes to make sure we get those figured out + chg0 := s.state.NewChange("auto-refresh", "auto-refresh-the-things") + chg0.AddTask(s.state.NewTask("nop", "do nothing")) + + // make it in doing state + chg0.SetStatus(state.DoingStatus) + + // this one will be picked up too + chg1 := s.state.NewChange("auto-refresh", "auto-refresh-the-things") + chg1.AddTask(s.state.NewTask("nop", "do nothing")) + chg1.SetStatus(state.DoStatus) + + // this one won't, it's Done + chg2 := s.state.NewChange("auto-refresh", "auto-refresh-the-things") + chg2.AddTask(s.state.NewTask("nop", "do nothing")) + chg2.SetStatus(state.DoneStatus) + + // nor this one, it's Undone + chg3 := s.state.NewChange("auto-refresh", "auto-refresh-the-things") + chg3.AddTask(s.state.NewTask("nop", "do nothing")) + chg3.SetStatus(state.UndoneStatus) + + // now we get our change ID returned when calling EnsureAutoRefreshesAreDelayed + chgs, err = s.snapmgr.EnsureAutoRefreshesAreDelayed(time.Minute) + c.Assert(err, IsNil) + // more helpful error message if we first compare the change ID's + expids := []string{chg0.ID(), chg1.ID()} + sort.Strings(expids) + c.Assert(chgs, HasLen, len(expids)) + gotids := []string{chgs[0].ID(), chgs[1].ID()} + sort.Strings(gotids) + c.Assert(expids, DeepEquals, gotids) + + sort.SliceStable(chgs, func(i, j int) bool { + return chgs[i].ID() < chgs[j].ID() + }) + + c.Assert(chgs, DeepEquals, []*state.Change{chg0, chg1}) +} diff -Nru snapd-2.47.1+20.10.1build1/overlord/snapstate/snapstate_update_test.go snapd-2.48+21.04/overlord/snapstate/snapstate_update_test.go --- snapd-2.47.1+20.10.1build1/overlord/snapstate/snapstate_update_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/overlord/snapstate/snapstate_update_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -4329,6 +4329,106 @@ checkIsAutoRefresh(c, ts.Tasks(), false) } +func (s *snapmgrTestSuite) TestUpdateManyFailureDoesntUndoSnapdRefresh(c *C) { + s.state.Lock() + defer s.state.Unlock() + + r := snapstatetest.MockDeviceModel(ModelWithBase("core18")) + defer r() + + snapstate.Set(s.state, "some-snap", &snapstate.SnapState{ + Active: true, + Sequence: []*snap.SideInfo{ + {RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(1)}, + }, + Current: snap.R(1), + SnapType: "app", + TrackingChannel: "channel-for-base/stable", + }) + + snapstate.Set(s.state, "core18", &snapstate.SnapState{ + Active: true, + Sequence: []*snap.SideInfo{ + {RealName: "core18", SnapID: "core18-snap-id", Revision: snap.R(1)}, + }, + Current: snap.R(1), + SnapType: "base", + }) + + snapstate.Set(s.state, "some-base", &snapstate.SnapState{ + Active: true, + Sequence: []*snap.SideInfo{ + {RealName: "some-base", SnapID: "some-base-id", Revision: snap.R(1)}, + }, + Current: snap.R(1), + SnapType: "base", + }) + + snapstate.Set(s.state, "snapd", &snapstate.SnapState{ + Active: true, + Sequence: []*snap.SideInfo{ + {RealName: "snapd", SnapID: "snapd-snap-id", Revision: snap.R(1)}, + }, + Current: snap.R(1), + SnapType: "app", + }) + + updates, tts, err := snapstate.UpdateMany(context.Background(), s.state, []string{"some-snap", "some-base", "snapd"}, 0, nil) + c.Assert(err, IsNil) + c.Assert(tts, HasLen, 4) + c.Assert(updates, HasLen, 3) + + chg := s.state.NewChange("refresh", "...") + for _, ts := range tts { + chg.AddAll(ts) + } + + // refresh of some-snap fails on link-snap + s.fakeBackend.linkSnapFailTrigger = filepath.Join(dirs.SnapMountDir, "/some-snap/11") + + s.state.Unlock() + defer s.se.Stop() + s.settle(c) + s.state.Lock() + + c.Check(chg.Err(), ErrorMatches, ".*cannot perform the following tasks:\n- Make snap \"some-snap\" \\(11\\) available to the system.*") + c.Check(chg.IsReady(), Equals, true) + + var snapst snapstate.SnapState + + // failed snap remains at the old revision, snapd and some-base are refreshed. + c.Assert(snapstate.Get(s.state, "some-snap", &snapst), IsNil) + c.Check(snapst.Current, Equals, snap.Revision{N: 1}) + + c.Assert(snapstate.Get(s.state, "snapd", &snapst), IsNil) + c.Check(snapst.Current, Equals, snap.Revision{N: 11}) + + c.Assert(snapstate.Get(s.state, "some-base", &snapst), IsNil) + c.Check(snapst.Current, Equals, snap.Revision{N: 11}) + + var undoneDownloads, doneDownloads int + for _, ts := range tts { + for _, t := range ts.Tasks() { + if t.Kind() == "download-snap" { + sup, err := snapstate.TaskSnapSetup(t) + c.Assert(err, IsNil) + switch sup.SnapName() { + case "some-snap": + undoneDownloads++ + c.Check(t.Status(), Equals, state.UndoneStatus) + case "snapd", "some-base": + doneDownloads++ + c.Check(t.Status(), Equals, state.DoneStatus) + default: + c.Errorf("unexpected snap %s", sup.SnapName()) + } + } + } + } + c.Assert(undoneDownloads, Equals, 1) + c.Assert(doneDownloads, Equals, 2) +} + func (s *snapmgrTestSuite) TestUpdateManyDevModeConfinementFiltering(c *C) { s.state.Lock() defer s.state.Unlock() diff -Nru snapd-2.47.1+20.10.1build1/overlord/state/task.go snapd-2.48+21.04/overlord/state/task.go --- snapd-2.47.1+20.10.1build1/overlord/state/task.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/overlord/state/task.go 2020-11-19 16:51:02.000000000 +0000 @@ -509,7 +509,7 @@ } } -// AddTask adds the the task to the task set. +// AddTask adds the task to the task set. func (ts *TaskSet) AddTask(task *Task) { for _, t := range ts.tasks { if t == task { diff -Nru snapd-2.47.1+20.10.1build1/packaging/amzn-2/snapd.spec snapd-2.48+21.04/packaging/amzn-2/snapd.spec --- snapd-2.47.1+20.10.1build1/packaging/amzn-2/snapd.spec 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/packaging/amzn-2/snapd.spec 2020-11-19 16:51:02.000000000 +0000 @@ -97,7 +97,7 @@ %endif Name: snapd -Version: 2.47.1 +Version: 2.48 Release: 0%{?dist} Summary: A transactional software package manager License: GPLv3 @@ -915,6 +915,321 @@ %changelog +* Thu Nov 19 2020 Michael Vogt +- New upstream release 2.48 + - osutil: add KernelCommandLineKeyValue + - devicestate: implement boot.HasFDESetupHook + - boot/makebootable.go: set snapd_recovery_mode=install at image- + build time + - bootloader: use ForGadget when installing boot config + - interfaces/raw_usb: allow read access to /proc/tty/drivers + - boot: add scaffolding for "fde-setup" hook support for sealing + - tests: fix basic20 test on arm devices + - seed: make a shared seed system label validation helper + - snap: add new "fde-setup" hooktype + - cmd/snap-bootstrap, secboot, tests: misc cleanups, add spread test + - secboot,cmd/snap-bootstrap: fix degraded mode cases with better + device handling + - boot,dirs,c/snap-bootstrap: avoid InstallHost* at the cost of some + messiness + - tests/nested/manual/refresh-revert-fundamentals: temporarily + disable secure boot + - snap-bootstrap,secboot: call BlockPCRProtectionPolicies in all + boot modes + - many: address degraded recover mode feedback, cleanups + - tests: Use systemd-run on tests part2 + - tests: set the opensuse tumbleweed system as manual in spread.yaml + - secboot: call BlockPCRProtectionPolicies even if the TPM is + disabled + - vendor: update to current secboot + - cmd/snap-bootstrap,o/devicestate: use a secret to pair data and + save + - spread.yaml: increase number of workers on 20.10 + - snap: add new `snap recovery --show-keys` option + - tests: minor test tweaks suggested in the review of 9607 + - snapd-generator: set standard snapfuse options when generating + units for containers + - tests: enable lxd test on ubuntu-core-20 and 16.04-32 + - interfaces: share /tmp/.X11-unix/ from host or provider + - tests: enable main lxd test on 20.10 + - cmd/s-b/initramfs-mounts: refactor recover mode to implement + degraded mode + - gadget/install: add progress logging + - packaging: keep secboot/encrypt_dummy.go in debian + - interfaces/udev: use distro specific path to snap-device-helper + - o/devistate: fix chaining of tasks related to regular snaps when + preseeding + - gadget, overlord/devicestate: validate that system supports + encrypted data before install + - interfaces/fwupd: enforce the confined fwupd to align Ubuntu Core + ESP layout + - many: add /v2/system-recovery-keys API and client + - secboot, many: return UnlockMethod from Unlock* methods for future + usage + - many: mv keys to ubuntu-boot, move model file, rename keyring + prefix for secboot + - tests: using systemd-run instead of manually create a systemd unit + - part 1 + - secboot, cmd/snap-bootstrap: enable or disable activation with + recovery key + - secboot: refactor Unlock...IfEncrypted to take keyfile + check + disks first + - secboot: add LockTPMSealedKeys() to lock access to keys + independently + - gadget: correct sfdisk arguments + - bootloader/assets/grub: adjust fwsetup menuentry label + - tests: new boot state tool + - spread: use the official image for Ubuntu 20.10, no longer an + unstable system + - tests/lib/nested: enable snapd logging to console for core18 + - osutil/disks: re-implement partition searching for disk w/ non- + adjacent parts + - tests: using the nested-state tool in nested tests + - many: seal a fallback object to the recovery boot chain + - gadget, gadget/install: move helpers to install package, refactor + unit tests + - dirs: add "gentoo" to altDirDistros + - update-pot: include file locations in translation template, and + extract strings from desktop files + - gadget/many: drop usage of gpt attr 59 for indicating creation of + partitions + - gadget/quantity: tweak test name + - snap: fix failing unittest for quantity.FormatDuration() + - gadget/quantity: introduce a new package that captures quantities + - o/devicestate,a/sysdb: make a backup of the device serial to save + - tests: fix rare interaction of tests.session and specific tests + - features: enable classic-preserves-xdg-runtime-dir + - tests/nested/core20/save: check the bind mount and size bump + - o/devicetate,dirs: keep device keys in ubuntu-save/save for UC20 + - tests: rename hasHooks to hasInterfaceHooks in the ifacestate + tests + - o/devicestate: unit test tweaks + - boot: store the TPM{PolicyAuthKey,LockoutAuth}File in ubuntu-save + - testutil, cmd/snap/version: fix misc little errors + - overlord/devicestate: bind mount ubuntu-save under + /var/lib/snapd/save on startup + - gadget/internal: tune ext4 setting for smaller filesystems + - tests/nested/core20/save: a test that verifies ubuntu-save is + present and set up + - tests: update google sru backend to support groovy + - o/ifacestate: handle interface hooks when preseeding + - tests: re-enable the apt hooks test + - interfaces,snap: use correct type: {os,snapd} for test data + - secboot: set metadata and keyslots sizes when formatting LUKS2 + volumes + - tests: improve uc20-create-partitions-reinstall test + - client, daemon, cmd/snap: cleanups from #9489 + more unit tests + - cmd/snap-bootstrap: mount ubuntu-save during boot if present + - secboot: fix doc comment on helper for unlocking volume with key + - tests: add spread test for refreshing from an old snapd and core18 + - o/snapstate: generate snapd snap wrappers again after restart on + refresh + - secboot: version bump, unlock volume with key + - tests/snap-advise-command: re-enable test + - cmd/snap, snapmgr, tests: cleanups after #9418 + - interfaces: deny connected x11 plugs access to ICE + - daemon,client: write and read a maintenance.json file for when + snapd is shut down + - many: update to secboot v1 (part 1) + - osutil/disks/mockdisk: panic if same mountpoint shows up again + with diff opts + - tests/nested/core20/gadget,kernel-reseal: add sanity checks to the + reseal tests + - many: implement snap routine console-conf-start for synchronizing + auto-refreshes + - dirs, boot: add ubuntu-save directories and related locations + - usersession: fix typo in test name + - overlord/snapstate: refactor ihibitRefresh + - overlord/snapstate: stop warning about inhibited refreshes + - cmd/snap: do not hardcode snapshot age value + - overlord,usersession: initial notifications of pending refreshes + - tests: add a unit test for UpdateMany where a single snap fails + - o/snapstate/catalogrefresh.go: don't refresh catalog in install + mode uc20 + - tests: also check snapst.Current in undo-unlink tests + - tests: new nested tool + - o/snapstate: implement undo handler for unlink-snap + - tests: clean systems.sh helper and migrate last set of tests + - tests: moving the lib section from systems.sh helper to os.query + tool + - tests/uc20-create-partitions: don't check for grub.cfg + - packaging: make sure that static binaries are indeed static, fix + openSUSE + - many: have install return encryption keys for data and save, + improve tests + - overlord: add link participant for linkage transitions + - tests: lxd smoke test + - tests: add tests for fsck; cmd/s-b/initramfs-mounts: fsck ubuntu- + seed too + - tests: moving main suite from systems.sh to os.query tool + - tests: moving the core test suite from systems.sh to os.query tool + - cmd/snap-confine: mask host's apparmor config + - o/snapstate: move setting updated SnapState after error paths + - tests: add value to INSTANCE_KEY/regular + - spread, tests: tweaks for openSUSE + - cmd/snap-confine: update path to snap-device-helper in AppArmor + profile + - tests: new os.query tool + - overlord/snapshotstate/backend: specify tar format for snapshots + - tests/nested/manual/minimal-smoke: use 384MB of RAM for nested + UC20 + - client,daemon,snap: auto-import does not error on managed devices + - interfaces: PTP hardware clock interface + - tests: use tests.backup tool + - many: verify that unit tests work with nosecboot tag and without + secboot package + - wrappers: do not error out on read-only /etc/dbus-1/session.d + filesystem on core18 + - snapshots: import of a snapshot set + - tests: more output for sbuild test + - o/snapstate: re-order remove tasks for individual snap revisions + to remove current last + - boot: skip some unit tests when running as root + - o/assertstate: introduce + ValidationTrackingKey/ValidationSetTracking and basic methods + - many: allow ignoring running apps for specific request + - tests: allow the searching test to fail under load + - overlord/snapstate: inhibit startup while unlinked + - seed/seedwriter/writer.go: check DevModeConfinement for dangerous + features + - tests/main/sudo-env: snap bin is available on Fedora + - boot, overlord/devicestate: list trusted and managed assets + upfront + - gadget, gadget/install: support for ubuntu-save, create one during + install if needed + - spread-shellcheck: temporary workaround for deadlock, drop + unnecessary test + - snap: support different exit-code in the snap command + - logger: use strutil.KernelCommandLineSplit in + debugEnabledOnKernelCmdline + - logger: fix snapd.debug=1 parsing + - overlord: increase refresh postpone limit to 14 days + - spread-shellcheck: use single thread pool executor + - gadget/install,secboot: add debug messages + - spread-shellcheck: speed up spread-shellcheck even more + - spread-shellcheck: process paths from arguments in parallel + - tests: tweak error from tests.cleanup + - spread: remove workaround for openSUSE go issue + - o/configstate: create /etc/sysctl.d when applying early config + defaults + - tests: new tests.backup tool + - tests: add tests.cleanup pop sub-command + - tests: migration of the main suite to snaps-state tool part 6 + - tests: fix journal-state test + - cmd/snap-bootstrap/initramfs-mounts: split off new helper for misc + recover files + - cmd/snap-bootstrap/initramfs-mounts: also copy /etc/machine-id for + same IP addr + - packaging/{ubuntu,debian}: add liblzo2-dev as a dependency for + building snapd + - boot, gadget, bootloader: observer preserves managed bootloader + configs + - tests/nested/manual: add uc20 grade signed cloud-init test + - o/snapstate/autorefresh.go: eliminate race when launching + autorefresh + - daemon,snapshotstate: do not return "size" from Import() + - daemon: limit reading from snapshot import to Content-Length + - many: set/expect Content-Length header when importing snapshots + - github: switch from ::set-env command to environment file + - tests: migration of the main suite to snaps-state tool part 5 + - client: cleanup the Client.raw* and Client.do* method families + - tests: moving main suite to snaps-state tool part 4 + - client,daemon,snap: use constant for snapshot content-type + - many: fix typos and repeated "the" + - secboot: fix tpm connection leak when it's not enabled + - many: scaffolding for snapshots import API + - run-checks: run spread-shellcheck too + - interfaces: update network-manager interface to allow + ObjectManager access from unconfined clients + - tests: move core and regression suites to snaps-state tool + - tests: moving interfaces tests to snaps-state tool + - gadget: preserve files when indicated by content change observer + - tests: moving smoke test suite and some tests from main suite to + snaps-state tool + - o/snapshotstate: pass set id to backend.Open, update tests + - asserts/snapasserts: introduce ValidationSets + - o/snapshotstate: improve allocation of new set IDs + - boot: look at the gadget for run mode bootloader when making the + system bootable + - cmd/snap: allow snap help vs --all to diverge purposefully + - usersession/userd: separate bus name ownership from defining + interfaces + - o/snapshotstate: set snapshot set id from its filename + - o/snapstate: move remove-related tests to snapstate_remove_test.go + - desktop/notification: switch ExpireTimeout to time.Duration + - desktop/notification: add unit tests + - snap: snap help output refresh + - tests/nested/manual/preseed: include a system-usernames snap when + preseeding + - tests: fix sudo-env test + - tests: fix nested core20 shellcheck bug + - tests/lib: move to new directory when restoring PWD, cleanup + unpacked unpacked snap directories + - desktop/notification: add bindings for FDO notifications + - dbustest: fix stale comment references + - many: move ManagedAssetsBootloader into TrustedAssetsBootloader, + drop former + - snap-repair: add uc20 support + - tests: print all the serial logs for the nested test + - o/snapstate/check_snap_test.go: mock osutil.Find{U,G}id to avoid + bug in test + - cmd/snap/auto-import: stop importing system user assertions from + initramfs mnts + - osutil/group.go: treat all non-nil errs from user.Lookup{Group,} + as Unknown* + - asserts: deserialize grouping only once in Pool.AddBatch if needed + - gadget: allow content observer to have opinions about a change + - tests: new snaps-state command - part1 + - o/assertstate: support refreshing any number of snap-declarations + - boot: use test helpers + - tests/core/snap-debug-bootvars: also check snap_mode + - many/apparmor: adjust rules for reading profile/ execing new + profiles for new kernel + - tests/core/snap-debug-bootvars: spread test for snap debug boot- + vars + - tests/lib/nested.sh: more little tweaks + - tests/nested/manual/grade-signed-above-testkeys-boot: enable kvm + - cmd/s-b/initramfs-mounts: use ConfigureTargetSystem for install, + recover modes + - overlord: explicitly set refresh-app-awareness in tests + - kernel: remove "edition" from kernel.yaml and add "update" + - spread: drop vendor from the packed project archive + - boot: fix debug bootloader variables dump on UC20 systems + - wrappers, systemd: allow empty root dir and conditionally do not + pass --root to systemctl + - tests/nested/manual: add test for grades above signed booting with + testkeys + - tests/nested: misc robustness fixes + - o/assertstate,asserts: use bulk refresh to refresh snap- + declarations + - tests/lib/prepare.sh: stop patching the uc20 initrd since it has + been updated now + - tests/nested/manual/refresh-revert-fundamentals: re-enable test + - update-pot: ignore .go files inside .git when running xgettext-go + - tests: disable part of the lxd test completely on 16.04. + - o/snapshotstate: tweak comment regarding snapshot filename + - o/snapstate: improve snapshot iteration + - bootloader: lk cleanups + - tests: update to support nested kvm without reboots on UC20 + - tests/nested/manual/preseed: disable system-key check for 20.04 + image + - spread.yaml: add ubuntu-20.10-64 to qemu + - store: handle v2 error when fetching assertions + - gadget: resolve device mapper devices for fallback device lookup + - tests/nested/cloud-init-many: simplify tests and unify + helpers/seed inputs + - tests: copy /usr/lib/snapd/info to correct directory + - check-pr-title.py * : allow "*" in the first part of the title + - many: typos and small test tweak + - tests/main/lxd: disable cgroup combination for 16.04 that is + failing a lot + - tests: make nested signing helpers less confusing + - tests: misc nested changes + - tests/nested/manual/refresh-revert-fundamentals: disable + temporarily + - tests/lib/cla_check: default to Python 3, tweaks, formatting + - tests/lib/cl_check.py: use python3 compatible code + * Thu Oct 08 2020 Michael Vogt - New upstream release 2.47.1 - o/configstate: create /etc/sysctl.d when applying early config diff -Nru snapd-2.47.1+20.10.1build1/packaging/arch/PKGBUILD snapd-2.48+21.04/packaging/arch/PKGBUILD --- snapd-2.47.1+20.10.1build1/packaging/arch/PKGBUILD 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/packaging/arch/PKGBUILD 2020-11-19 16:51:02.000000000 +0000 @@ -11,7 +11,7 @@ depends=('squashfs-tools' 'libseccomp' 'libsystemd' 'apparmor') optdepends=('bash-completion: bash completion support' 'xdg-desktop-portal: desktop integration') -pkgver=2.47.1 +pkgver=2.48 pkgrel=1 arch=('x86_64' 'i686' 'armv7h' 'aarch64') url="https://github.com/snapcore/snapd" diff -Nru snapd-2.47.1+20.10.1build1/packaging/centos-7/snapd.spec snapd-2.48+21.04/packaging/centos-7/snapd.spec --- snapd-2.47.1+20.10.1build1/packaging/centos-7/snapd.spec 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/packaging/centos-7/snapd.spec 2020-11-19 16:51:02.000000000 +0000 @@ -97,7 +97,7 @@ %endif Name: snapd -Version: 2.47.1 +Version: 2.48 Release: 0%{?dist} Summary: A transactional software package manager License: GPLv3 @@ -915,6 +915,321 @@ %changelog +* Thu Nov 19 2020 Michael Vogt +- New upstream release 2.48 + - osutil: add KernelCommandLineKeyValue + - devicestate: implement boot.HasFDESetupHook + - boot/makebootable.go: set snapd_recovery_mode=install at image- + build time + - bootloader: use ForGadget when installing boot config + - interfaces/raw_usb: allow read access to /proc/tty/drivers + - boot: add scaffolding for "fde-setup" hook support for sealing + - tests: fix basic20 test on arm devices + - seed: make a shared seed system label validation helper + - snap: add new "fde-setup" hooktype + - cmd/snap-bootstrap, secboot, tests: misc cleanups, add spread test + - secboot,cmd/snap-bootstrap: fix degraded mode cases with better + device handling + - boot,dirs,c/snap-bootstrap: avoid InstallHost* at the cost of some + messiness + - tests/nested/manual/refresh-revert-fundamentals: temporarily + disable secure boot + - snap-bootstrap,secboot: call BlockPCRProtectionPolicies in all + boot modes + - many: address degraded recover mode feedback, cleanups + - tests: Use systemd-run on tests part2 + - tests: set the opensuse tumbleweed system as manual in spread.yaml + - secboot: call BlockPCRProtectionPolicies even if the TPM is + disabled + - vendor: update to current secboot + - cmd/snap-bootstrap,o/devicestate: use a secret to pair data and + save + - spread.yaml: increase number of workers on 20.10 + - snap: add new `snap recovery --show-keys` option + - tests: minor test tweaks suggested in the review of 9607 + - snapd-generator: set standard snapfuse options when generating + units for containers + - tests: enable lxd test on ubuntu-core-20 and 16.04-32 + - interfaces: share /tmp/.X11-unix/ from host or provider + - tests: enable main lxd test on 20.10 + - cmd/s-b/initramfs-mounts: refactor recover mode to implement + degraded mode + - gadget/install: add progress logging + - packaging: keep secboot/encrypt_dummy.go in debian + - interfaces/udev: use distro specific path to snap-device-helper + - o/devistate: fix chaining of tasks related to regular snaps when + preseeding + - gadget, overlord/devicestate: validate that system supports + encrypted data before install + - interfaces/fwupd: enforce the confined fwupd to align Ubuntu Core + ESP layout + - many: add /v2/system-recovery-keys API and client + - secboot, many: return UnlockMethod from Unlock* methods for future + usage + - many: mv keys to ubuntu-boot, move model file, rename keyring + prefix for secboot + - tests: using systemd-run instead of manually create a systemd unit + - part 1 + - secboot, cmd/snap-bootstrap: enable or disable activation with + recovery key + - secboot: refactor Unlock...IfEncrypted to take keyfile + check + disks first + - secboot: add LockTPMSealedKeys() to lock access to keys + independently + - gadget: correct sfdisk arguments + - bootloader/assets/grub: adjust fwsetup menuentry label + - tests: new boot state tool + - spread: use the official image for Ubuntu 20.10, no longer an + unstable system + - tests/lib/nested: enable snapd logging to console for core18 + - osutil/disks: re-implement partition searching for disk w/ non- + adjacent parts + - tests: using the nested-state tool in nested tests + - many: seal a fallback object to the recovery boot chain + - gadget, gadget/install: move helpers to install package, refactor + unit tests + - dirs: add "gentoo" to altDirDistros + - update-pot: include file locations in translation template, and + extract strings from desktop files + - gadget/many: drop usage of gpt attr 59 for indicating creation of + partitions + - gadget/quantity: tweak test name + - snap: fix failing unittest for quantity.FormatDuration() + - gadget/quantity: introduce a new package that captures quantities + - o/devicestate,a/sysdb: make a backup of the device serial to save + - tests: fix rare interaction of tests.session and specific tests + - features: enable classic-preserves-xdg-runtime-dir + - tests/nested/core20/save: check the bind mount and size bump + - o/devicetate,dirs: keep device keys in ubuntu-save/save for UC20 + - tests: rename hasHooks to hasInterfaceHooks in the ifacestate + tests + - o/devicestate: unit test tweaks + - boot: store the TPM{PolicyAuthKey,LockoutAuth}File in ubuntu-save + - testutil, cmd/snap/version: fix misc little errors + - overlord/devicestate: bind mount ubuntu-save under + /var/lib/snapd/save on startup + - gadget/internal: tune ext4 setting for smaller filesystems + - tests/nested/core20/save: a test that verifies ubuntu-save is + present and set up + - tests: update google sru backend to support groovy + - o/ifacestate: handle interface hooks when preseeding + - tests: re-enable the apt hooks test + - interfaces,snap: use correct type: {os,snapd} for test data + - secboot: set metadata and keyslots sizes when formatting LUKS2 + volumes + - tests: improve uc20-create-partitions-reinstall test + - client, daemon, cmd/snap: cleanups from #9489 + more unit tests + - cmd/snap-bootstrap: mount ubuntu-save during boot if present + - secboot: fix doc comment on helper for unlocking volume with key + - tests: add spread test for refreshing from an old snapd and core18 + - o/snapstate: generate snapd snap wrappers again after restart on + refresh + - secboot: version bump, unlock volume with key + - tests/snap-advise-command: re-enable test + - cmd/snap, snapmgr, tests: cleanups after #9418 + - interfaces: deny connected x11 plugs access to ICE + - daemon,client: write and read a maintenance.json file for when + snapd is shut down + - many: update to secboot v1 (part 1) + - osutil/disks/mockdisk: panic if same mountpoint shows up again + with diff opts + - tests/nested/core20/gadget,kernel-reseal: add sanity checks to the + reseal tests + - many: implement snap routine console-conf-start for synchronizing + auto-refreshes + - dirs, boot: add ubuntu-save directories and related locations + - usersession: fix typo in test name + - overlord/snapstate: refactor ihibitRefresh + - overlord/snapstate: stop warning about inhibited refreshes + - cmd/snap: do not hardcode snapshot age value + - overlord,usersession: initial notifications of pending refreshes + - tests: add a unit test for UpdateMany where a single snap fails + - o/snapstate/catalogrefresh.go: don't refresh catalog in install + mode uc20 + - tests: also check snapst.Current in undo-unlink tests + - tests: new nested tool + - o/snapstate: implement undo handler for unlink-snap + - tests: clean systems.sh helper and migrate last set of tests + - tests: moving the lib section from systems.sh helper to os.query + tool + - tests/uc20-create-partitions: don't check for grub.cfg + - packaging: make sure that static binaries are indeed static, fix + openSUSE + - many: have install return encryption keys for data and save, + improve tests + - overlord: add link participant for linkage transitions + - tests: lxd smoke test + - tests: add tests for fsck; cmd/s-b/initramfs-mounts: fsck ubuntu- + seed too + - tests: moving main suite from systems.sh to os.query tool + - tests: moving the core test suite from systems.sh to os.query tool + - cmd/snap-confine: mask host's apparmor config + - o/snapstate: move setting updated SnapState after error paths + - tests: add value to INSTANCE_KEY/regular + - spread, tests: tweaks for openSUSE + - cmd/snap-confine: update path to snap-device-helper in AppArmor + profile + - tests: new os.query tool + - overlord/snapshotstate/backend: specify tar format for snapshots + - tests/nested/manual/minimal-smoke: use 384MB of RAM for nested + UC20 + - client,daemon,snap: auto-import does not error on managed devices + - interfaces: PTP hardware clock interface + - tests: use tests.backup tool + - many: verify that unit tests work with nosecboot tag and without + secboot package + - wrappers: do not error out on read-only /etc/dbus-1/session.d + filesystem on core18 + - snapshots: import of a snapshot set + - tests: more output for sbuild test + - o/snapstate: re-order remove tasks for individual snap revisions + to remove current last + - boot: skip some unit tests when running as root + - o/assertstate: introduce + ValidationTrackingKey/ValidationSetTracking and basic methods + - many: allow ignoring running apps for specific request + - tests: allow the searching test to fail under load + - overlord/snapstate: inhibit startup while unlinked + - seed/seedwriter/writer.go: check DevModeConfinement for dangerous + features + - tests/main/sudo-env: snap bin is available on Fedora + - boot, overlord/devicestate: list trusted and managed assets + upfront + - gadget, gadget/install: support for ubuntu-save, create one during + install if needed + - spread-shellcheck: temporary workaround for deadlock, drop + unnecessary test + - snap: support different exit-code in the snap command + - logger: use strutil.KernelCommandLineSplit in + debugEnabledOnKernelCmdline + - logger: fix snapd.debug=1 parsing + - overlord: increase refresh postpone limit to 14 days + - spread-shellcheck: use single thread pool executor + - gadget/install,secboot: add debug messages + - spread-shellcheck: speed up spread-shellcheck even more + - spread-shellcheck: process paths from arguments in parallel + - tests: tweak error from tests.cleanup + - spread: remove workaround for openSUSE go issue + - o/configstate: create /etc/sysctl.d when applying early config + defaults + - tests: new tests.backup tool + - tests: add tests.cleanup pop sub-command + - tests: migration of the main suite to snaps-state tool part 6 + - tests: fix journal-state test + - cmd/snap-bootstrap/initramfs-mounts: split off new helper for misc + recover files + - cmd/snap-bootstrap/initramfs-mounts: also copy /etc/machine-id for + same IP addr + - packaging/{ubuntu,debian}: add liblzo2-dev as a dependency for + building snapd + - boot, gadget, bootloader: observer preserves managed bootloader + configs + - tests/nested/manual: add uc20 grade signed cloud-init test + - o/snapstate/autorefresh.go: eliminate race when launching + autorefresh + - daemon,snapshotstate: do not return "size" from Import() + - daemon: limit reading from snapshot import to Content-Length + - many: set/expect Content-Length header when importing snapshots + - github: switch from ::set-env command to environment file + - tests: migration of the main suite to snaps-state tool part 5 + - client: cleanup the Client.raw* and Client.do* method families + - tests: moving main suite to snaps-state tool part 4 + - client,daemon,snap: use constant for snapshot content-type + - many: fix typos and repeated "the" + - secboot: fix tpm connection leak when it's not enabled + - many: scaffolding for snapshots import API + - run-checks: run spread-shellcheck too + - interfaces: update network-manager interface to allow + ObjectManager access from unconfined clients + - tests: move core and regression suites to snaps-state tool + - tests: moving interfaces tests to snaps-state tool + - gadget: preserve files when indicated by content change observer + - tests: moving smoke test suite and some tests from main suite to + snaps-state tool + - o/snapshotstate: pass set id to backend.Open, update tests + - asserts/snapasserts: introduce ValidationSets + - o/snapshotstate: improve allocation of new set IDs + - boot: look at the gadget for run mode bootloader when making the + system bootable + - cmd/snap: allow snap help vs --all to diverge purposefully + - usersession/userd: separate bus name ownership from defining + interfaces + - o/snapshotstate: set snapshot set id from its filename + - o/snapstate: move remove-related tests to snapstate_remove_test.go + - desktop/notification: switch ExpireTimeout to time.Duration + - desktop/notification: add unit tests + - snap: snap help output refresh + - tests/nested/manual/preseed: include a system-usernames snap when + preseeding + - tests: fix sudo-env test + - tests: fix nested core20 shellcheck bug + - tests/lib: move to new directory when restoring PWD, cleanup + unpacked unpacked snap directories + - desktop/notification: add bindings for FDO notifications + - dbustest: fix stale comment references + - many: move ManagedAssetsBootloader into TrustedAssetsBootloader, + drop former + - snap-repair: add uc20 support + - tests: print all the serial logs for the nested test + - o/snapstate/check_snap_test.go: mock osutil.Find{U,G}id to avoid + bug in test + - cmd/snap/auto-import: stop importing system user assertions from + initramfs mnts + - osutil/group.go: treat all non-nil errs from user.Lookup{Group,} + as Unknown* + - asserts: deserialize grouping only once in Pool.AddBatch if needed + - gadget: allow content observer to have opinions about a change + - tests: new snaps-state command - part1 + - o/assertstate: support refreshing any number of snap-declarations + - boot: use test helpers + - tests/core/snap-debug-bootvars: also check snap_mode + - many/apparmor: adjust rules for reading profile/ execing new + profiles for new kernel + - tests/core/snap-debug-bootvars: spread test for snap debug boot- + vars + - tests/lib/nested.sh: more little tweaks + - tests/nested/manual/grade-signed-above-testkeys-boot: enable kvm + - cmd/s-b/initramfs-mounts: use ConfigureTargetSystem for install, + recover modes + - overlord: explicitly set refresh-app-awareness in tests + - kernel: remove "edition" from kernel.yaml and add "update" + - spread: drop vendor from the packed project archive + - boot: fix debug bootloader variables dump on UC20 systems + - wrappers, systemd: allow empty root dir and conditionally do not + pass --root to systemctl + - tests/nested/manual: add test for grades above signed booting with + testkeys + - tests/nested: misc robustness fixes + - o/assertstate,asserts: use bulk refresh to refresh snap- + declarations + - tests/lib/prepare.sh: stop patching the uc20 initrd since it has + been updated now + - tests/nested/manual/refresh-revert-fundamentals: re-enable test + - update-pot: ignore .go files inside .git when running xgettext-go + - tests: disable part of the lxd test completely on 16.04. + - o/snapshotstate: tweak comment regarding snapshot filename + - o/snapstate: improve snapshot iteration + - bootloader: lk cleanups + - tests: update to support nested kvm without reboots on UC20 + - tests/nested/manual/preseed: disable system-key check for 20.04 + image + - spread.yaml: add ubuntu-20.10-64 to qemu + - store: handle v2 error when fetching assertions + - gadget: resolve device mapper devices for fallback device lookup + - tests/nested/cloud-init-many: simplify tests and unify + helpers/seed inputs + - tests: copy /usr/lib/snapd/info to correct directory + - check-pr-title.py * : allow "*" in the first part of the title + - many: typos and small test tweak + - tests/main/lxd: disable cgroup combination for 16.04 that is + failing a lot + - tests: make nested signing helpers less confusing + - tests: misc nested changes + - tests/nested/manual/refresh-revert-fundamentals: disable + temporarily + - tests/lib/cla_check: default to Python 3, tweaks, formatting + - tests/lib/cl_check.py: use python3 compatible code + * Thu Oct 08 2020 Michael Vogt - New upstream release 2.47.1 - o/configstate: create /etc/sysctl.d when applying early config diff -Nru snapd-2.47.1+20.10.1build1/packaging/centos-8/snapd.spec snapd-2.48+21.04/packaging/centos-8/snapd.spec --- snapd-2.47.1+20.10.1build1/packaging/centos-8/snapd.spec 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/packaging/centos-8/snapd.spec 2020-11-19 16:51:02.000000000 +0000 @@ -97,7 +97,7 @@ %endif Name: snapd -Version: 2.47.1 +Version: 2.48 Release: 0%{?dist} Summary: A transactional software package manager License: GPLv3 @@ -915,6 +915,321 @@ %changelog +* Thu Nov 19 2020 Michael Vogt +- New upstream release 2.48 + - osutil: add KernelCommandLineKeyValue + - devicestate: implement boot.HasFDESetupHook + - boot/makebootable.go: set snapd_recovery_mode=install at image- + build time + - bootloader: use ForGadget when installing boot config + - interfaces/raw_usb: allow read access to /proc/tty/drivers + - boot: add scaffolding for "fde-setup" hook support for sealing + - tests: fix basic20 test on arm devices + - seed: make a shared seed system label validation helper + - snap: add new "fde-setup" hooktype + - cmd/snap-bootstrap, secboot, tests: misc cleanups, add spread test + - secboot,cmd/snap-bootstrap: fix degraded mode cases with better + device handling + - boot,dirs,c/snap-bootstrap: avoid InstallHost* at the cost of some + messiness + - tests/nested/manual/refresh-revert-fundamentals: temporarily + disable secure boot + - snap-bootstrap,secboot: call BlockPCRProtectionPolicies in all + boot modes + - many: address degraded recover mode feedback, cleanups + - tests: Use systemd-run on tests part2 + - tests: set the opensuse tumbleweed system as manual in spread.yaml + - secboot: call BlockPCRProtectionPolicies even if the TPM is + disabled + - vendor: update to current secboot + - cmd/snap-bootstrap,o/devicestate: use a secret to pair data and + save + - spread.yaml: increase number of workers on 20.10 + - snap: add new `snap recovery --show-keys` option + - tests: minor test tweaks suggested in the review of 9607 + - snapd-generator: set standard snapfuse options when generating + units for containers + - tests: enable lxd test on ubuntu-core-20 and 16.04-32 + - interfaces: share /tmp/.X11-unix/ from host or provider + - tests: enable main lxd test on 20.10 + - cmd/s-b/initramfs-mounts: refactor recover mode to implement + degraded mode + - gadget/install: add progress logging + - packaging: keep secboot/encrypt_dummy.go in debian + - interfaces/udev: use distro specific path to snap-device-helper + - o/devistate: fix chaining of tasks related to regular snaps when + preseeding + - gadget, overlord/devicestate: validate that system supports + encrypted data before install + - interfaces/fwupd: enforce the confined fwupd to align Ubuntu Core + ESP layout + - many: add /v2/system-recovery-keys API and client + - secboot, many: return UnlockMethod from Unlock* methods for future + usage + - many: mv keys to ubuntu-boot, move model file, rename keyring + prefix for secboot + - tests: using systemd-run instead of manually create a systemd unit + - part 1 + - secboot, cmd/snap-bootstrap: enable or disable activation with + recovery key + - secboot: refactor Unlock...IfEncrypted to take keyfile + check + disks first + - secboot: add LockTPMSealedKeys() to lock access to keys + independently + - gadget: correct sfdisk arguments + - bootloader/assets/grub: adjust fwsetup menuentry label + - tests: new boot state tool + - spread: use the official image for Ubuntu 20.10, no longer an + unstable system + - tests/lib/nested: enable snapd logging to console for core18 + - osutil/disks: re-implement partition searching for disk w/ non- + adjacent parts + - tests: using the nested-state tool in nested tests + - many: seal a fallback object to the recovery boot chain + - gadget, gadget/install: move helpers to install package, refactor + unit tests + - dirs: add "gentoo" to altDirDistros + - update-pot: include file locations in translation template, and + extract strings from desktop files + - gadget/many: drop usage of gpt attr 59 for indicating creation of + partitions + - gadget/quantity: tweak test name + - snap: fix failing unittest for quantity.FormatDuration() + - gadget/quantity: introduce a new package that captures quantities + - o/devicestate,a/sysdb: make a backup of the device serial to save + - tests: fix rare interaction of tests.session and specific tests + - features: enable classic-preserves-xdg-runtime-dir + - tests/nested/core20/save: check the bind mount and size bump + - o/devicetate,dirs: keep device keys in ubuntu-save/save for UC20 + - tests: rename hasHooks to hasInterfaceHooks in the ifacestate + tests + - o/devicestate: unit test tweaks + - boot: store the TPM{PolicyAuthKey,LockoutAuth}File in ubuntu-save + - testutil, cmd/snap/version: fix misc little errors + - overlord/devicestate: bind mount ubuntu-save under + /var/lib/snapd/save on startup + - gadget/internal: tune ext4 setting for smaller filesystems + - tests/nested/core20/save: a test that verifies ubuntu-save is + present and set up + - tests: update google sru backend to support groovy + - o/ifacestate: handle interface hooks when preseeding + - tests: re-enable the apt hooks test + - interfaces,snap: use correct type: {os,snapd} for test data + - secboot: set metadata and keyslots sizes when formatting LUKS2 + volumes + - tests: improve uc20-create-partitions-reinstall test + - client, daemon, cmd/snap: cleanups from #9489 + more unit tests + - cmd/snap-bootstrap: mount ubuntu-save during boot if present + - secboot: fix doc comment on helper for unlocking volume with key + - tests: add spread test for refreshing from an old snapd and core18 + - o/snapstate: generate snapd snap wrappers again after restart on + refresh + - secboot: version bump, unlock volume with key + - tests/snap-advise-command: re-enable test + - cmd/snap, snapmgr, tests: cleanups after #9418 + - interfaces: deny connected x11 plugs access to ICE + - daemon,client: write and read a maintenance.json file for when + snapd is shut down + - many: update to secboot v1 (part 1) + - osutil/disks/mockdisk: panic if same mountpoint shows up again + with diff opts + - tests/nested/core20/gadget,kernel-reseal: add sanity checks to the + reseal tests + - many: implement snap routine console-conf-start for synchronizing + auto-refreshes + - dirs, boot: add ubuntu-save directories and related locations + - usersession: fix typo in test name + - overlord/snapstate: refactor ihibitRefresh + - overlord/snapstate: stop warning about inhibited refreshes + - cmd/snap: do not hardcode snapshot age value + - overlord,usersession: initial notifications of pending refreshes + - tests: add a unit test for UpdateMany where a single snap fails + - o/snapstate/catalogrefresh.go: don't refresh catalog in install + mode uc20 + - tests: also check snapst.Current in undo-unlink tests + - tests: new nested tool + - o/snapstate: implement undo handler for unlink-snap + - tests: clean systems.sh helper and migrate last set of tests + - tests: moving the lib section from systems.sh helper to os.query + tool + - tests/uc20-create-partitions: don't check for grub.cfg + - packaging: make sure that static binaries are indeed static, fix + openSUSE + - many: have install return encryption keys for data and save, + improve tests + - overlord: add link participant for linkage transitions + - tests: lxd smoke test + - tests: add tests for fsck; cmd/s-b/initramfs-mounts: fsck ubuntu- + seed too + - tests: moving main suite from systems.sh to os.query tool + - tests: moving the core test suite from systems.sh to os.query tool + - cmd/snap-confine: mask host's apparmor config + - o/snapstate: move setting updated SnapState after error paths + - tests: add value to INSTANCE_KEY/regular + - spread, tests: tweaks for openSUSE + - cmd/snap-confine: update path to snap-device-helper in AppArmor + profile + - tests: new os.query tool + - overlord/snapshotstate/backend: specify tar format for snapshots + - tests/nested/manual/minimal-smoke: use 384MB of RAM for nested + UC20 + - client,daemon,snap: auto-import does not error on managed devices + - interfaces: PTP hardware clock interface + - tests: use tests.backup tool + - many: verify that unit tests work with nosecboot tag and without + secboot package + - wrappers: do not error out on read-only /etc/dbus-1/session.d + filesystem on core18 + - snapshots: import of a snapshot set + - tests: more output for sbuild test + - o/snapstate: re-order remove tasks for individual snap revisions + to remove current last + - boot: skip some unit tests when running as root + - o/assertstate: introduce + ValidationTrackingKey/ValidationSetTracking and basic methods + - many: allow ignoring running apps for specific request + - tests: allow the searching test to fail under load + - overlord/snapstate: inhibit startup while unlinked + - seed/seedwriter/writer.go: check DevModeConfinement for dangerous + features + - tests/main/sudo-env: snap bin is available on Fedora + - boot, overlord/devicestate: list trusted and managed assets + upfront + - gadget, gadget/install: support for ubuntu-save, create one during + install if needed + - spread-shellcheck: temporary workaround for deadlock, drop + unnecessary test + - snap: support different exit-code in the snap command + - logger: use strutil.KernelCommandLineSplit in + debugEnabledOnKernelCmdline + - logger: fix snapd.debug=1 parsing + - overlord: increase refresh postpone limit to 14 days + - spread-shellcheck: use single thread pool executor + - gadget/install,secboot: add debug messages + - spread-shellcheck: speed up spread-shellcheck even more + - spread-shellcheck: process paths from arguments in parallel + - tests: tweak error from tests.cleanup + - spread: remove workaround for openSUSE go issue + - o/configstate: create /etc/sysctl.d when applying early config + defaults + - tests: new tests.backup tool + - tests: add tests.cleanup pop sub-command + - tests: migration of the main suite to snaps-state tool part 6 + - tests: fix journal-state test + - cmd/snap-bootstrap/initramfs-mounts: split off new helper for misc + recover files + - cmd/snap-bootstrap/initramfs-mounts: also copy /etc/machine-id for + same IP addr + - packaging/{ubuntu,debian}: add liblzo2-dev as a dependency for + building snapd + - boot, gadget, bootloader: observer preserves managed bootloader + configs + - tests/nested/manual: add uc20 grade signed cloud-init test + - o/snapstate/autorefresh.go: eliminate race when launching + autorefresh + - daemon,snapshotstate: do not return "size" from Import() + - daemon: limit reading from snapshot import to Content-Length + - many: set/expect Content-Length header when importing snapshots + - github: switch from ::set-env command to environment file + - tests: migration of the main suite to snaps-state tool part 5 + - client: cleanup the Client.raw* and Client.do* method families + - tests: moving main suite to snaps-state tool part 4 + - client,daemon,snap: use constant for snapshot content-type + - many: fix typos and repeated "the" + - secboot: fix tpm connection leak when it's not enabled + - many: scaffolding for snapshots import API + - run-checks: run spread-shellcheck too + - interfaces: update network-manager interface to allow + ObjectManager access from unconfined clients + - tests: move core and regression suites to snaps-state tool + - tests: moving interfaces tests to snaps-state tool + - gadget: preserve files when indicated by content change observer + - tests: moving smoke test suite and some tests from main suite to + snaps-state tool + - o/snapshotstate: pass set id to backend.Open, update tests + - asserts/snapasserts: introduce ValidationSets + - o/snapshotstate: improve allocation of new set IDs + - boot: look at the gadget for run mode bootloader when making the + system bootable + - cmd/snap: allow snap help vs --all to diverge purposefully + - usersession/userd: separate bus name ownership from defining + interfaces + - o/snapshotstate: set snapshot set id from its filename + - o/snapstate: move remove-related tests to snapstate_remove_test.go + - desktop/notification: switch ExpireTimeout to time.Duration + - desktop/notification: add unit tests + - snap: snap help output refresh + - tests/nested/manual/preseed: include a system-usernames snap when + preseeding + - tests: fix sudo-env test + - tests: fix nested core20 shellcheck bug + - tests/lib: move to new directory when restoring PWD, cleanup + unpacked unpacked snap directories + - desktop/notification: add bindings for FDO notifications + - dbustest: fix stale comment references + - many: move ManagedAssetsBootloader into TrustedAssetsBootloader, + drop former + - snap-repair: add uc20 support + - tests: print all the serial logs for the nested test + - o/snapstate/check_snap_test.go: mock osutil.Find{U,G}id to avoid + bug in test + - cmd/snap/auto-import: stop importing system user assertions from + initramfs mnts + - osutil/group.go: treat all non-nil errs from user.Lookup{Group,} + as Unknown* + - asserts: deserialize grouping only once in Pool.AddBatch if needed + - gadget: allow content observer to have opinions about a change + - tests: new snaps-state command - part1 + - o/assertstate: support refreshing any number of snap-declarations + - boot: use test helpers + - tests/core/snap-debug-bootvars: also check snap_mode + - many/apparmor: adjust rules for reading profile/ execing new + profiles for new kernel + - tests/core/snap-debug-bootvars: spread test for snap debug boot- + vars + - tests/lib/nested.sh: more little tweaks + - tests/nested/manual/grade-signed-above-testkeys-boot: enable kvm + - cmd/s-b/initramfs-mounts: use ConfigureTargetSystem for install, + recover modes + - overlord: explicitly set refresh-app-awareness in tests + - kernel: remove "edition" from kernel.yaml and add "update" + - spread: drop vendor from the packed project archive + - boot: fix debug bootloader variables dump on UC20 systems + - wrappers, systemd: allow empty root dir and conditionally do not + pass --root to systemctl + - tests/nested/manual: add test for grades above signed booting with + testkeys + - tests/nested: misc robustness fixes + - o/assertstate,asserts: use bulk refresh to refresh snap- + declarations + - tests/lib/prepare.sh: stop patching the uc20 initrd since it has + been updated now + - tests/nested/manual/refresh-revert-fundamentals: re-enable test + - update-pot: ignore .go files inside .git when running xgettext-go + - tests: disable part of the lxd test completely on 16.04. + - o/snapshotstate: tweak comment regarding snapshot filename + - o/snapstate: improve snapshot iteration + - bootloader: lk cleanups + - tests: update to support nested kvm without reboots on UC20 + - tests/nested/manual/preseed: disable system-key check for 20.04 + image + - spread.yaml: add ubuntu-20.10-64 to qemu + - store: handle v2 error when fetching assertions + - gadget: resolve device mapper devices for fallback device lookup + - tests/nested/cloud-init-many: simplify tests and unify + helpers/seed inputs + - tests: copy /usr/lib/snapd/info to correct directory + - check-pr-title.py * : allow "*" in the first part of the title + - many: typos and small test tweak + - tests/main/lxd: disable cgroup combination for 16.04 that is + failing a lot + - tests: make nested signing helpers less confusing + - tests: misc nested changes + - tests/nested/manual/refresh-revert-fundamentals: disable + temporarily + - tests/lib/cla_check: default to Python 3, tweaks, formatting + - tests/lib/cl_check.py: use python3 compatible code + * Thu Oct 08 2020 Michael Vogt - New upstream release 2.47.1 - o/configstate: create /etc/sysctl.d when applying early config diff -Nru snapd-2.47.1+20.10.1build1/packaging/debian-sid/changelog snapd-2.48+21.04/packaging/debian-sid/changelog --- snapd-2.47.1+20.10.1build1/packaging/debian-sid/changelog 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/packaging/debian-sid/changelog 2020-11-19 16:51:02.000000000 +0000 @@ -1,3 +1,321 @@ +snapd (2.48-1) unstable; urgency=medium + + * New upstream release, LP: #1904098 + - osutil: add KernelCommandLineKeyValue + - devicestate: implement boot.HasFDESetupHook + - boot/makebootable.go: set snapd_recovery_mode=install at image- + build time + - bootloader: use ForGadget when installing boot config + - interfaces/raw_usb: allow read access to /proc/tty/drivers + - boot: add scaffolding for "fde-setup" hook support for sealing + - tests: fix basic20 test on arm devices + - seed: make a shared seed system label validation helper + - snap: add new "fde-setup" hooktype + - cmd/snap-bootstrap, secboot, tests: misc cleanups, add spread test + - secboot,cmd/snap-bootstrap: fix degraded mode cases with better + device handling + - boot,dirs,c/snap-bootstrap: avoid InstallHost* at the cost of some + messiness + - tests/nested/manual/refresh-revert-fundamentals: temporarily + disable secure boot + - snap-bootstrap,secboot: call BlockPCRProtectionPolicies in all + boot modes + - many: address degraded recover mode feedback, cleanups + - tests: Use systemd-run on tests part2 + - tests: set the opensuse tumbleweed system as manual in spread.yaml + - secboot: call BlockPCRProtectionPolicies even if the TPM is + disabled + - vendor: update to current secboot + - cmd/snap-bootstrap,o/devicestate: use a secret to pair data and + save + - spread.yaml: increase number of workers on 20.10 + - snap: add new `snap recovery --show-keys` option + - tests: minor test tweaks suggested in the review of 9607 + - snapd-generator: set standard snapfuse options when generating + units for containers + - tests: enable lxd test on ubuntu-core-20 and 16.04-32 + - interfaces: share /tmp/.X11-unix/ from host or provider + - tests: enable main lxd test on 20.10 + - cmd/s-b/initramfs-mounts: refactor recover mode to implement + degraded mode + - gadget/install: add progress logging + - packaging: keep secboot/encrypt_dummy.go in debian + - interfaces/udev: use distro specific path to snap-device-helper + - o/devistate: fix chaining of tasks related to regular snaps when + preseeding + - gadget, overlord/devicestate: validate that system supports + encrypted data before install + - interfaces/fwupd: enforce the confined fwupd to align Ubuntu Core + ESP layout + - many: add /v2/system-recovery-keys API and client + - secboot, many: return UnlockMethod from Unlock* methods for future + usage + - many: mv keys to ubuntu-boot, move model file, rename keyring + prefix for secboot + - tests: using systemd-run instead of manually create a systemd unit + - part 1 + - secboot, cmd/snap-bootstrap: enable or disable activation with + recovery key + - secboot: refactor Unlock...IfEncrypted to take keyfile + check + disks first + - secboot: add LockTPMSealedKeys() to lock access to keys + independently + - gadget: correct sfdisk arguments + - bootloader/assets/grub: adjust fwsetup menuentry label + - tests: new boot state tool + - spread: use the official image for Ubuntu 20.10, no longer an + unstable system + - tests/lib/nested: enable snapd logging to console for core18 + - osutil/disks: re-implement partition searching for disk w/ non- + adjacent parts + - tests: using the nested-state tool in nested tests + - many: seal a fallback object to the recovery boot chain + - gadget, gadget/install: move helpers to install package, refactor + unit tests + - dirs: add "gentoo" to altDirDistros + - update-pot: include file locations in translation template, and + extract strings from desktop files + - gadget/many: drop usage of gpt attr 59 for indicating creation of + partitions + - gadget/quantity: tweak test name + - snap: fix failing unittest for quantity.FormatDuration() + - gadget/quantity: introduce a new package that captures quantities + - o/devicestate,a/sysdb: make a backup of the device serial to save + - tests: fix rare interaction of tests.session and specific tests + - features: enable classic-preserves-xdg-runtime-dir + - tests/nested/core20/save: check the bind mount and size bump + - o/devicetate,dirs: keep device keys in ubuntu-save/save for UC20 + - tests: rename hasHooks to hasInterfaceHooks in the ifacestate + tests + - o/devicestate: unit test tweaks + - boot: store the TPM{PolicyAuthKey,LockoutAuth}File in ubuntu-save + - testutil, cmd/snap/version: fix misc little errors + - overlord/devicestate: bind mount ubuntu-save under + /var/lib/snapd/save on startup + - gadget/internal: tune ext4 setting for smaller filesystems + - tests/nested/core20/save: a test that verifies ubuntu-save is + present and set up + - tests: update google sru backend to support groovy + - o/ifacestate: handle interface hooks when preseeding + - tests: re-enable the apt hooks test + - interfaces,snap: use correct type: {os,snapd} for test data + - secboot: set metadata and keyslots sizes when formatting LUKS2 + volumes + - tests: improve uc20-create-partitions-reinstall test + - client, daemon, cmd/snap: cleanups from #9489 + more unit tests + - cmd/snap-bootstrap: mount ubuntu-save during boot if present + - secboot: fix doc comment on helper for unlocking volume with key + - tests: add spread test for refreshing from an old snapd and core18 + - o/snapstate: generate snapd snap wrappers again after restart on + refresh + - secboot: version bump, unlock volume with key + - tests/snap-advise-command: re-enable test + - cmd/snap, snapmgr, tests: cleanups after #9418 + - interfaces: deny connected x11 plugs access to ICE + - daemon,client: write and read a maintenance.json file for when + snapd is shut down + - many: update to secboot v1 (part 1) + - osutil/disks/mockdisk: panic if same mountpoint shows up again + with diff opts + - tests/nested/core20/gadget,kernel-reseal: add sanity checks to the + reseal tests + - many: implement snap routine console-conf-start for synchronizing + auto-refreshes + - dirs, boot: add ubuntu-save directories and related locations + - usersession: fix typo in test name + - overlord/snapstate: refactor ihibitRefresh + - overlord/snapstate: stop warning about inhibited refreshes + - cmd/snap: do not hardcode snapshot age value + - overlord,usersession: initial notifications of pending refreshes + - tests: add a unit test for UpdateMany where a single snap fails + - o/snapstate/catalogrefresh.go: don't refresh catalog in install + mode uc20 + - tests: also check snapst.Current in undo-unlink tests + - tests: new nested tool + - o/snapstate: implement undo handler for unlink-snap + - tests: clean systems.sh helper and migrate last set of tests + - tests: moving the lib section from systems.sh helper to os.query + tool + - tests/uc20-create-partitions: don't check for grub.cfg + - packaging: make sure that static binaries are indeed static, fix + openSUSE + - many: have install return encryption keys for data and save, + improve tests + - overlord: add link participant for linkage transitions + - tests: lxd smoke test + - tests: add tests for fsck; cmd/s-b/initramfs-mounts: fsck ubuntu- + seed too + - tests: moving main suite from systems.sh to os.query tool + - tests: moving the core test suite from systems.sh to os.query tool + - cmd/snap-confine: mask host's apparmor config + - o/snapstate: move setting updated SnapState after error paths + - tests: add value to INSTANCE_KEY/regular + - spread, tests: tweaks for openSUSE + - cmd/snap-confine: update path to snap-device-helper in AppArmor + profile + - tests: new os.query tool + - overlord/snapshotstate/backend: specify tar format for snapshots + - tests/nested/manual/minimal-smoke: use 384MB of RAM for nested + UC20 + - client,daemon,snap: auto-import does not error on managed devices + - interfaces: PTP hardware clock interface + - tests: use tests.backup tool + - many: verify that unit tests work with nosecboot tag and without + secboot package + - wrappers: do not error out on read-only /etc/dbus-1/session.d + filesystem on core18 + - snapshots: import of a snapshot set + - tests: more output for sbuild test + - o/snapstate: re-order remove tasks for individual snap revisions + to remove current last + - boot: skip some unit tests when running as root + - o/assertstate: introduce + ValidationTrackingKey/ValidationSetTracking and basic methods + - many: allow ignoring running apps for specific request + - tests: allow the searching test to fail under load + - overlord/snapstate: inhibit startup while unlinked + - seed/seedwriter/writer.go: check DevModeConfinement for dangerous + features + - tests/main/sudo-env: snap bin is available on Fedora + - boot, overlord/devicestate: list trusted and managed assets + upfront + - gadget, gadget/install: support for ubuntu-save, create one during + install if needed + - spread-shellcheck: temporary workaround for deadlock, drop + unnecessary test + - snap: support different exit-code in the snap command + - logger: use strutil.KernelCommandLineSplit in + debugEnabledOnKernelCmdline + - logger: fix snapd.debug=1 parsing + - overlord: increase refresh postpone limit to 14 days + - spread-shellcheck: use single thread pool executor + - gadget/install,secboot: add debug messages + - spread-shellcheck: speed up spread-shellcheck even more + - spread-shellcheck: process paths from arguments in parallel + - tests: tweak error from tests.cleanup + - spread: remove workaround for openSUSE go issue + - o/configstate: create /etc/sysctl.d when applying early config + defaults + - tests: new tests.backup tool + - tests: add tests.cleanup pop sub-command + - tests: migration of the main suite to snaps-state tool part 6 + - tests: fix journal-state test + - cmd/snap-bootstrap/initramfs-mounts: split off new helper for misc + recover files + - cmd/snap-bootstrap/initramfs-mounts: also copy /etc/machine-id for + same IP addr + - packaging/{ubuntu,debian}: add liblzo2-dev as a dependency for + building snapd + - boot, gadget, bootloader: observer preserves managed bootloader + configs + - tests/nested/manual: add uc20 grade signed cloud-init test + - o/snapstate/autorefresh.go: eliminate race when launching + autorefresh + - daemon,snapshotstate: do not return "size" from Import() + - daemon: limit reading from snapshot import to Content-Length + - many: set/expect Content-Length header when importing snapshots + - github: switch from ::set-env command to environment file + - tests: migration of the main suite to snaps-state tool part 5 + - client: cleanup the Client.raw* and Client.do* method families + - tests: moving main suite to snaps-state tool part 4 + - client,daemon,snap: use constant for snapshot content-type + - many: fix typos and repeated "the" + - secboot: fix tpm connection leak when it's not enabled + - many: scaffolding for snapshots import API + - run-checks: run spread-shellcheck too + - interfaces: update network-manager interface to allow + ObjectManager access from unconfined clients + - tests: move core and regression suites to snaps-state tool + - tests: moving interfaces tests to snaps-state tool + - gadget: preserve files when indicated by content change observer + - tests: moving smoke test suite and some tests from main suite to + snaps-state tool + - o/snapshotstate: pass set id to backend.Open, update tests + - asserts/snapasserts: introduce ValidationSets + - o/snapshotstate: improve allocation of new set IDs + - boot: look at the gadget for run mode bootloader when making the + system bootable + - cmd/snap: allow snap help vs --all to diverge purposefully + - usersession/userd: separate bus name ownership from defining + interfaces + - o/snapshotstate: set snapshot set id from its filename + - o/snapstate: move remove-related tests to snapstate_remove_test.go + - desktop/notification: switch ExpireTimeout to time.Duration + - desktop/notification: add unit tests + - snap: snap help output refresh + - tests/nested/manual/preseed: include a system-usernames snap when + preseeding + - tests: fix sudo-env test + - tests: fix nested core20 shellcheck bug + - tests/lib: move to new directory when restoring PWD, cleanup + unpacked unpacked snap directories + - desktop/notification: add bindings for FDO notifications + - dbustest: fix stale comment references + - many: move ManagedAssetsBootloader into TrustedAssetsBootloader, + drop former + - snap-repair: add uc20 support + - tests: print all the serial logs for the nested test + - o/snapstate/check_snap_test.go: mock osutil.Find{U,G}id to avoid + bug in test + - cmd/snap/auto-import: stop importing system user assertions from + initramfs mnts + - osutil/group.go: treat all non-nil errs from user.Lookup{Group,} + as Unknown* + - asserts: deserialize grouping only once in Pool.AddBatch if needed + - gadget: allow content observer to have opinions about a change + - tests: new snaps-state command - part1 + - o/assertstate: support refreshing any number of snap-declarations + - boot: use test helpers + - tests/core/snap-debug-bootvars: also check snap_mode + - many/apparmor: adjust rules for reading profile/ execing new + profiles for new kernel + - tests/core/snap-debug-bootvars: spread test for snap debug boot- + vars + - tests/lib/nested.sh: more little tweaks + - tests/nested/manual/grade-signed-above-testkeys-boot: enable kvm + - cmd/s-b/initramfs-mounts: use ConfigureTargetSystem for install, + recover modes + - overlord: explicitly set refresh-app-awareness in tests + - kernel: remove "edition" from kernel.yaml and add "update" + - spread: drop vendor from the packed project archive + - boot: fix debug bootloader variables dump on UC20 systems + - wrappers, systemd: allow empty root dir and conditionally do not + pass --root to systemctl + - tests/nested/manual: add test for grades above signed booting with + testkeys + - tests/nested: misc robustness fixes + - o/assertstate,asserts: use bulk refresh to refresh snap- + declarations + - tests/lib/prepare.sh: stop patching the uc20 initrd since it has + been updated now + - tests/nested/manual/refresh-revert-fundamentals: re-enable test + - update-pot: ignore .go files inside .git when running xgettext-go + - tests: disable part of the lxd test completely on 16.04. + - o/snapshotstate: tweak comment regarding snapshot filename + - o/snapstate: improve snapshot iteration + - bootloader: lk cleanups + - tests: update to support nested kvm without reboots on UC20 + - tests/nested/manual/preseed: disable system-key check for 20.04 + image + - spread.yaml: add ubuntu-20.10-64 to qemu + - store: handle v2 error when fetching assertions + - gadget: resolve device mapper devices for fallback device lookup + - tests/nested/cloud-init-many: simplify tests and unify + helpers/seed inputs + - tests: copy /usr/lib/snapd/info to correct directory + - check-pr-title.py * : allow "*" in the first part of the title + - many: typos and small test tweak + - tests/main/lxd: disable cgroup combination for 16.04 that is + failing a lot + - tests: make nested signing helpers less confusing + - tests: misc nested changes + - tests/nested/manual/refresh-revert-fundamentals: disable + temporarily + - tests/lib/cla_check: default to Python 3, tweaks, formatting + - tests/lib/cl_check.py: use python3 compatible code + + -- Michael Vogt Thu, 19 Nov 2020 17:51:02 +0100 + snapd (2.47.1-1) unstable; urgency=medium * New upstream release, LP: #1895929 diff -Nru snapd-2.47.1+20.10.1build1/packaging/debian-sid/rules snapd-2.48+21.04/packaging/debian-sid/rules --- snapd-2.47.1+20.10.1build1/packaging/debian-sid/rules 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/packaging/debian-sid/rules 2020-11-19 16:51:02.000000000 +0000 @@ -149,7 +149,7 @@ find _build/src/$(DH_GOPKG)/cmd/snap-bootstrap -name "*.go" | xargs rm -f find _build/src/$(DH_GOPKG)/gadget/install -name "*.go" | grep -vE '(params\.go|install_dummy\.go)'| xargs rm -f # XXX: once dh-golang understands go build tags this would not be needed - find _build/src/$(DH_GOPKG)/secboot/ -name "*.go" | grep -Ev '(encrypt\.go|secboot_dummy\.go|secboot\.go)' | xargs rm -f + find _build/src/$(DH_GOPKG)/secboot/ -name "*.go" | grep -Ev '(encrypt\.go|secboot_dummy\.go|secboot\.go|encrypt_dummy\.go)' | xargs rm -f # and build dh_auto_build -- $(BUILDFLAGS) -tags "$(TAGS)" $(GCCGOFLAGS) diff -Nru snapd-2.47.1+20.10.1build1/packaging/fedora/snapd.spec snapd-2.48+21.04/packaging/fedora/snapd.spec --- snapd-2.47.1+20.10.1build1/packaging/fedora/snapd.spec 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/packaging/fedora/snapd.spec 2020-11-19 16:51:02.000000000 +0000 @@ -97,7 +97,7 @@ %endif Name: snapd -Version: 2.47.1 +Version: 2.48 Release: 0%{?dist} Summary: A transactional software package manager License: GPLv3 @@ -915,6 +915,321 @@ %changelog +* Thu Nov 19 2020 Michael Vogt +- New upstream release 2.48 + - osutil: add KernelCommandLineKeyValue + - devicestate: implement boot.HasFDESetupHook + - boot/makebootable.go: set snapd_recovery_mode=install at image- + build time + - bootloader: use ForGadget when installing boot config + - interfaces/raw_usb: allow read access to /proc/tty/drivers + - boot: add scaffolding for "fde-setup" hook support for sealing + - tests: fix basic20 test on arm devices + - seed: make a shared seed system label validation helper + - snap: add new "fde-setup" hooktype + - cmd/snap-bootstrap, secboot, tests: misc cleanups, add spread test + - secboot,cmd/snap-bootstrap: fix degraded mode cases with better + device handling + - boot,dirs,c/snap-bootstrap: avoid InstallHost* at the cost of some + messiness + - tests/nested/manual/refresh-revert-fundamentals: temporarily + disable secure boot + - snap-bootstrap,secboot: call BlockPCRProtectionPolicies in all + boot modes + - many: address degraded recover mode feedback, cleanups + - tests: Use systemd-run on tests part2 + - tests: set the opensuse tumbleweed system as manual in spread.yaml + - secboot: call BlockPCRProtectionPolicies even if the TPM is + disabled + - vendor: update to current secboot + - cmd/snap-bootstrap,o/devicestate: use a secret to pair data and + save + - spread.yaml: increase number of workers on 20.10 + - snap: add new `snap recovery --show-keys` option + - tests: minor test tweaks suggested in the review of 9607 + - snapd-generator: set standard snapfuse options when generating + units for containers + - tests: enable lxd test on ubuntu-core-20 and 16.04-32 + - interfaces: share /tmp/.X11-unix/ from host or provider + - tests: enable main lxd test on 20.10 + - cmd/s-b/initramfs-mounts: refactor recover mode to implement + degraded mode + - gadget/install: add progress logging + - packaging: keep secboot/encrypt_dummy.go in debian + - interfaces/udev: use distro specific path to snap-device-helper + - o/devistate: fix chaining of tasks related to regular snaps when + preseeding + - gadget, overlord/devicestate: validate that system supports + encrypted data before install + - interfaces/fwupd: enforce the confined fwupd to align Ubuntu Core + ESP layout + - many: add /v2/system-recovery-keys API and client + - secboot, many: return UnlockMethod from Unlock* methods for future + usage + - many: mv keys to ubuntu-boot, move model file, rename keyring + prefix for secboot + - tests: using systemd-run instead of manually create a systemd unit + - part 1 + - secboot, cmd/snap-bootstrap: enable or disable activation with + recovery key + - secboot: refactor Unlock...IfEncrypted to take keyfile + check + disks first + - secboot: add LockTPMSealedKeys() to lock access to keys + independently + - gadget: correct sfdisk arguments + - bootloader/assets/grub: adjust fwsetup menuentry label + - tests: new boot state tool + - spread: use the official image for Ubuntu 20.10, no longer an + unstable system + - tests/lib/nested: enable snapd logging to console for core18 + - osutil/disks: re-implement partition searching for disk w/ non- + adjacent parts + - tests: using the nested-state tool in nested tests + - many: seal a fallback object to the recovery boot chain + - gadget, gadget/install: move helpers to install package, refactor + unit tests + - dirs: add "gentoo" to altDirDistros + - update-pot: include file locations in translation template, and + extract strings from desktop files + - gadget/many: drop usage of gpt attr 59 for indicating creation of + partitions + - gadget/quantity: tweak test name + - snap: fix failing unittest for quantity.FormatDuration() + - gadget/quantity: introduce a new package that captures quantities + - o/devicestate,a/sysdb: make a backup of the device serial to save + - tests: fix rare interaction of tests.session and specific tests + - features: enable classic-preserves-xdg-runtime-dir + - tests/nested/core20/save: check the bind mount and size bump + - o/devicetate,dirs: keep device keys in ubuntu-save/save for UC20 + - tests: rename hasHooks to hasInterfaceHooks in the ifacestate + tests + - o/devicestate: unit test tweaks + - boot: store the TPM{PolicyAuthKey,LockoutAuth}File in ubuntu-save + - testutil, cmd/snap/version: fix misc little errors + - overlord/devicestate: bind mount ubuntu-save under + /var/lib/snapd/save on startup + - gadget/internal: tune ext4 setting for smaller filesystems + - tests/nested/core20/save: a test that verifies ubuntu-save is + present and set up + - tests: update google sru backend to support groovy + - o/ifacestate: handle interface hooks when preseeding + - tests: re-enable the apt hooks test + - interfaces,snap: use correct type: {os,snapd} for test data + - secboot: set metadata and keyslots sizes when formatting LUKS2 + volumes + - tests: improve uc20-create-partitions-reinstall test + - client, daemon, cmd/snap: cleanups from #9489 + more unit tests + - cmd/snap-bootstrap: mount ubuntu-save during boot if present + - secboot: fix doc comment on helper for unlocking volume with key + - tests: add spread test for refreshing from an old snapd and core18 + - o/snapstate: generate snapd snap wrappers again after restart on + refresh + - secboot: version bump, unlock volume with key + - tests/snap-advise-command: re-enable test + - cmd/snap, snapmgr, tests: cleanups after #9418 + - interfaces: deny connected x11 plugs access to ICE + - daemon,client: write and read a maintenance.json file for when + snapd is shut down + - many: update to secboot v1 (part 1) + - osutil/disks/mockdisk: panic if same mountpoint shows up again + with diff opts + - tests/nested/core20/gadget,kernel-reseal: add sanity checks to the + reseal tests + - many: implement snap routine console-conf-start for synchronizing + auto-refreshes + - dirs, boot: add ubuntu-save directories and related locations + - usersession: fix typo in test name + - overlord/snapstate: refactor ihibitRefresh + - overlord/snapstate: stop warning about inhibited refreshes + - cmd/snap: do not hardcode snapshot age value + - overlord,usersession: initial notifications of pending refreshes + - tests: add a unit test for UpdateMany where a single snap fails + - o/snapstate/catalogrefresh.go: don't refresh catalog in install + mode uc20 + - tests: also check snapst.Current in undo-unlink tests + - tests: new nested tool + - o/snapstate: implement undo handler for unlink-snap + - tests: clean systems.sh helper and migrate last set of tests + - tests: moving the lib section from systems.sh helper to os.query + tool + - tests/uc20-create-partitions: don't check for grub.cfg + - packaging: make sure that static binaries are indeed static, fix + openSUSE + - many: have install return encryption keys for data and save, + improve tests + - overlord: add link participant for linkage transitions + - tests: lxd smoke test + - tests: add tests for fsck; cmd/s-b/initramfs-mounts: fsck ubuntu- + seed too + - tests: moving main suite from systems.sh to os.query tool + - tests: moving the core test suite from systems.sh to os.query tool + - cmd/snap-confine: mask host's apparmor config + - o/snapstate: move setting updated SnapState after error paths + - tests: add value to INSTANCE_KEY/regular + - spread, tests: tweaks for openSUSE + - cmd/snap-confine: update path to snap-device-helper in AppArmor + profile + - tests: new os.query tool + - overlord/snapshotstate/backend: specify tar format for snapshots + - tests/nested/manual/minimal-smoke: use 384MB of RAM for nested + UC20 + - client,daemon,snap: auto-import does not error on managed devices + - interfaces: PTP hardware clock interface + - tests: use tests.backup tool + - many: verify that unit tests work with nosecboot tag and without + secboot package + - wrappers: do not error out on read-only /etc/dbus-1/session.d + filesystem on core18 + - snapshots: import of a snapshot set + - tests: more output for sbuild test + - o/snapstate: re-order remove tasks for individual snap revisions + to remove current last + - boot: skip some unit tests when running as root + - o/assertstate: introduce + ValidationTrackingKey/ValidationSetTracking and basic methods + - many: allow ignoring running apps for specific request + - tests: allow the searching test to fail under load + - overlord/snapstate: inhibit startup while unlinked + - seed/seedwriter/writer.go: check DevModeConfinement for dangerous + features + - tests/main/sudo-env: snap bin is available on Fedora + - boot, overlord/devicestate: list trusted and managed assets + upfront + - gadget, gadget/install: support for ubuntu-save, create one during + install if needed + - spread-shellcheck: temporary workaround for deadlock, drop + unnecessary test + - snap: support different exit-code in the snap command + - logger: use strutil.KernelCommandLineSplit in + debugEnabledOnKernelCmdline + - logger: fix snapd.debug=1 parsing + - overlord: increase refresh postpone limit to 14 days + - spread-shellcheck: use single thread pool executor + - gadget/install,secboot: add debug messages + - spread-shellcheck: speed up spread-shellcheck even more + - spread-shellcheck: process paths from arguments in parallel + - tests: tweak error from tests.cleanup + - spread: remove workaround for openSUSE go issue + - o/configstate: create /etc/sysctl.d when applying early config + defaults + - tests: new tests.backup tool + - tests: add tests.cleanup pop sub-command + - tests: migration of the main suite to snaps-state tool part 6 + - tests: fix journal-state test + - cmd/snap-bootstrap/initramfs-mounts: split off new helper for misc + recover files + - cmd/snap-bootstrap/initramfs-mounts: also copy /etc/machine-id for + same IP addr + - packaging/{ubuntu,debian}: add liblzo2-dev as a dependency for + building snapd + - boot, gadget, bootloader: observer preserves managed bootloader + configs + - tests/nested/manual: add uc20 grade signed cloud-init test + - o/snapstate/autorefresh.go: eliminate race when launching + autorefresh + - daemon,snapshotstate: do not return "size" from Import() + - daemon: limit reading from snapshot import to Content-Length + - many: set/expect Content-Length header when importing snapshots + - github: switch from ::set-env command to environment file + - tests: migration of the main suite to snaps-state tool part 5 + - client: cleanup the Client.raw* and Client.do* method families + - tests: moving main suite to snaps-state tool part 4 + - client,daemon,snap: use constant for snapshot content-type + - many: fix typos and repeated "the" + - secboot: fix tpm connection leak when it's not enabled + - many: scaffolding for snapshots import API + - run-checks: run spread-shellcheck too + - interfaces: update network-manager interface to allow + ObjectManager access from unconfined clients + - tests: move core and regression suites to snaps-state tool + - tests: moving interfaces tests to snaps-state tool + - gadget: preserve files when indicated by content change observer + - tests: moving smoke test suite and some tests from main suite to + snaps-state tool + - o/snapshotstate: pass set id to backend.Open, update tests + - asserts/snapasserts: introduce ValidationSets + - o/snapshotstate: improve allocation of new set IDs + - boot: look at the gadget for run mode bootloader when making the + system bootable + - cmd/snap: allow snap help vs --all to diverge purposefully + - usersession/userd: separate bus name ownership from defining + interfaces + - o/snapshotstate: set snapshot set id from its filename + - o/snapstate: move remove-related tests to snapstate_remove_test.go + - desktop/notification: switch ExpireTimeout to time.Duration + - desktop/notification: add unit tests + - snap: snap help output refresh + - tests/nested/manual/preseed: include a system-usernames snap when + preseeding + - tests: fix sudo-env test + - tests: fix nested core20 shellcheck bug + - tests/lib: move to new directory when restoring PWD, cleanup + unpacked unpacked snap directories + - desktop/notification: add bindings for FDO notifications + - dbustest: fix stale comment references + - many: move ManagedAssetsBootloader into TrustedAssetsBootloader, + drop former + - snap-repair: add uc20 support + - tests: print all the serial logs for the nested test + - o/snapstate/check_snap_test.go: mock osutil.Find{U,G}id to avoid + bug in test + - cmd/snap/auto-import: stop importing system user assertions from + initramfs mnts + - osutil/group.go: treat all non-nil errs from user.Lookup{Group,} + as Unknown* + - asserts: deserialize grouping only once in Pool.AddBatch if needed + - gadget: allow content observer to have opinions about a change + - tests: new snaps-state command - part1 + - o/assertstate: support refreshing any number of snap-declarations + - boot: use test helpers + - tests/core/snap-debug-bootvars: also check snap_mode + - many/apparmor: adjust rules for reading profile/ execing new + profiles for new kernel + - tests/core/snap-debug-bootvars: spread test for snap debug boot- + vars + - tests/lib/nested.sh: more little tweaks + - tests/nested/manual/grade-signed-above-testkeys-boot: enable kvm + - cmd/s-b/initramfs-mounts: use ConfigureTargetSystem for install, + recover modes + - overlord: explicitly set refresh-app-awareness in tests + - kernel: remove "edition" from kernel.yaml and add "update" + - spread: drop vendor from the packed project archive + - boot: fix debug bootloader variables dump on UC20 systems + - wrappers, systemd: allow empty root dir and conditionally do not + pass --root to systemctl + - tests/nested/manual: add test for grades above signed booting with + testkeys + - tests/nested: misc robustness fixes + - o/assertstate,asserts: use bulk refresh to refresh snap- + declarations + - tests/lib/prepare.sh: stop patching the uc20 initrd since it has + been updated now + - tests/nested/manual/refresh-revert-fundamentals: re-enable test + - update-pot: ignore .go files inside .git when running xgettext-go + - tests: disable part of the lxd test completely on 16.04. + - o/snapshotstate: tweak comment regarding snapshot filename + - o/snapstate: improve snapshot iteration + - bootloader: lk cleanups + - tests: update to support nested kvm without reboots on UC20 + - tests/nested/manual/preseed: disable system-key check for 20.04 + image + - spread.yaml: add ubuntu-20.10-64 to qemu + - store: handle v2 error when fetching assertions + - gadget: resolve device mapper devices for fallback device lookup + - tests/nested/cloud-init-many: simplify tests and unify + helpers/seed inputs + - tests: copy /usr/lib/snapd/info to correct directory + - check-pr-title.py * : allow "*" in the first part of the title + - many: typos and small test tweak + - tests/main/lxd: disable cgroup combination for 16.04 that is + failing a lot + - tests: make nested signing helpers less confusing + - tests: misc nested changes + - tests/nested/manual/refresh-revert-fundamentals: disable + temporarily + - tests/lib/cla_check: default to Python 3, tweaks, formatting + - tests/lib/cl_check.py: use python3 compatible code + * Thu Oct 08 2020 Michael Vogt - New upstream release 2.47.1 - o/configstate: create /etc/sysctl.d when applying early config diff -Nru snapd-2.47.1+20.10.1build1/packaging/fedora-29/snapd.spec snapd-2.48+21.04/packaging/fedora-29/snapd.spec --- snapd-2.47.1+20.10.1build1/packaging/fedora-29/snapd.spec 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/packaging/fedora-29/snapd.spec 2020-11-19 16:51:02.000000000 +0000 @@ -97,7 +97,7 @@ %endif Name: snapd -Version: 2.47.1 +Version: 2.48 Release: 0%{?dist} Summary: A transactional software package manager License: GPLv3 @@ -915,6 +915,321 @@ %changelog +* Thu Nov 19 2020 Michael Vogt +- New upstream release 2.48 + - osutil: add KernelCommandLineKeyValue + - devicestate: implement boot.HasFDESetupHook + - boot/makebootable.go: set snapd_recovery_mode=install at image- + build time + - bootloader: use ForGadget when installing boot config + - interfaces/raw_usb: allow read access to /proc/tty/drivers + - boot: add scaffolding for "fde-setup" hook support for sealing + - tests: fix basic20 test on arm devices + - seed: make a shared seed system label validation helper + - snap: add new "fde-setup" hooktype + - cmd/snap-bootstrap, secboot, tests: misc cleanups, add spread test + - secboot,cmd/snap-bootstrap: fix degraded mode cases with better + device handling + - boot,dirs,c/snap-bootstrap: avoid InstallHost* at the cost of some + messiness + - tests/nested/manual/refresh-revert-fundamentals: temporarily + disable secure boot + - snap-bootstrap,secboot: call BlockPCRProtectionPolicies in all + boot modes + - many: address degraded recover mode feedback, cleanups + - tests: Use systemd-run on tests part2 + - tests: set the opensuse tumbleweed system as manual in spread.yaml + - secboot: call BlockPCRProtectionPolicies even if the TPM is + disabled + - vendor: update to current secboot + - cmd/snap-bootstrap,o/devicestate: use a secret to pair data and + save + - spread.yaml: increase number of workers on 20.10 + - snap: add new `snap recovery --show-keys` option + - tests: minor test tweaks suggested in the review of 9607 + - snapd-generator: set standard snapfuse options when generating + units for containers + - tests: enable lxd test on ubuntu-core-20 and 16.04-32 + - interfaces: share /tmp/.X11-unix/ from host or provider + - tests: enable main lxd test on 20.10 + - cmd/s-b/initramfs-mounts: refactor recover mode to implement + degraded mode + - gadget/install: add progress logging + - packaging: keep secboot/encrypt_dummy.go in debian + - interfaces/udev: use distro specific path to snap-device-helper + - o/devistate: fix chaining of tasks related to regular snaps when + preseeding + - gadget, overlord/devicestate: validate that system supports + encrypted data before install + - interfaces/fwupd: enforce the confined fwupd to align Ubuntu Core + ESP layout + - many: add /v2/system-recovery-keys API and client + - secboot, many: return UnlockMethod from Unlock* methods for future + usage + - many: mv keys to ubuntu-boot, move model file, rename keyring + prefix for secboot + - tests: using systemd-run instead of manually create a systemd unit + - part 1 + - secboot, cmd/snap-bootstrap: enable or disable activation with + recovery key + - secboot: refactor Unlock...IfEncrypted to take keyfile + check + disks first + - secboot: add LockTPMSealedKeys() to lock access to keys + independently + - gadget: correct sfdisk arguments + - bootloader/assets/grub: adjust fwsetup menuentry label + - tests: new boot state tool + - spread: use the official image for Ubuntu 20.10, no longer an + unstable system + - tests/lib/nested: enable snapd logging to console for core18 + - osutil/disks: re-implement partition searching for disk w/ non- + adjacent parts + - tests: using the nested-state tool in nested tests + - many: seal a fallback object to the recovery boot chain + - gadget, gadget/install: move helpers to install package, refactor + unit tests + - dirs: add "gentoo" to altDirDistros + - update-pot: include file locations in translation template, and + extract strings from desktop files + - gadget/many: drop usage of gpt attr 59 for indicating creation of + partitions + - gadget/quantity: tweak test name + - snap: fix failing unittest for quantity.FormatDuration() + - gadget/quantity: introduce a new package that captures quantities + - o/devicestate,a/sysdb: make a backup of the device serial to save + - tests: fix rare interaction of tests.session and specific tests + - features: enable classic-preserves-xdg-runtime-dir + - tests/nested/core20/save: check the bind mount and size bump + - o/devicetate,dirs: keep device keys in ubuntu-save/save for UC20 + - tests: rename hasHooks to hasInterfaceHooks in the ifacestate + tests + - o/devicestate: unit test tweaks + - boot: store the TPM{PolicyAuthKey,LockoutAuth}File in ubuntu-save + - testutil, cmd/snap/version: fix misc little errors + - overlord/devicestate: bind mount ubuntu-save under + /var/lib/snapd/save on startup + - gadget/internal: tune ext4 setting for smaller filesystems + - tests/nested/core20/save: a test that verifies ubuntu-save is + present and set up + - tests: update google sru backend to support groovy + - o/ifacestate: handle interface hooks when preseeding + - tests: re-enable the apt hooks test + - interfaces,snap: use correct type: {os,snapd} for test data + - secboot: set metadata and keyslots sizes when formatting LUKS2 + volumes + - tests: improve uc20-create-partitions-reinstall test + - client, daemon, cmd/snap: cleanups from #9489 + more unit tests + - cmd/snap-bootstrap: mount ubuntu-save during boot if present + - secboot: fix doc comment on helper for unlocking volume with key + - tests: add spread test for refreshing from an old snapd and core18 + - o/snapstate: generate snapd snap wrappers again after restart on + refresh + - secboot: version bump, unlock volume with key + - tests/snap-advise-command: re-enable test + - cmd/snap, snapmgr, tests: cleanups after #9418 + - interfaces: deny connected x11 plugs access to ICE + - daemon,client: write and read a maintenance.json file for when + snapd is shut down + - many: update to secboot v1 (part 1) + - osutil/disks/mockdisk: panic if same mountpoint shows up again + with diff opts + - tests/nested/core20/gadget,kernel-reseal: add sanity checks to the + reseal tests + - many: implement snap routine console-conf-start for synchronizing + auto-refreshes + - dirs, boot: add ubuntu-save directories and related locations + - usersession: fix typo in test name + - overlord/snapstate: refactor ihibitRefresh + - overlord/snapstate: stop warning about inhibited refreshes + - cmd/snap: do not hardcode snapshot age value + - overlord,usersession: initial notifications of pending refreshes + - tests: add a unit test for UpdateMany where a single snap fails + - o/snapstate/catalogrefresh.go: don't refresh catalog in install + mode uc20 + - tests: also check snapst.Current in undo-unlink tests + - tests: new nested tool + - o/snapstate: implement undo handler for unlink-snap + - tests: clean systems.sh helper and migrate last set of tests + - tests: moving the lib section from systems.sh helper to os.query + tool + - tests/uc20-create-partitions: don't check for grub.cfg + - packaging: make sure that static binaries are indeed static, fix + openSUSE + - many: have install return encryption keys for data and save, + improve tests + - overlord: add link participant for linkage transitions + - tests: lxd smoke test + - tests: add tests for fsck; cmd/s-b/initramfs-mounts: fsck ubuntu- + seed too + - tests: moving main suite from systems.sh to os.query tool + - tests: moving the core test suite from systems.sh to os.query tool + - cmd/snap-confine: mask host's apparmor config + - o/snapstate: move setting updated SnapState after error paths + - tests: add value to INSTANCE_KEY/regular + - spread, tests: tweaks for openSUSE + - cmd/snap-confine: update path to snap-device-helper in AppArmor + profile + - tests: new os.query tool + - overlord/snapshotstate/backend: specify tar format for snapshots + - tests/nested/manual/minimal-smoke: use 384MB of RAM for nested + UC20 + - client,daemon,snap: auto-import does not error on managed devices + - interfaces: PTP hardware clock interface + - tests: use tests.backup tool + - many: verify that unit tests work with nosecboot tag and without + secboot package + - wrappers: do not error out on read-only /etc/dbus-1/session.d + filesystem on core18 + - snapshots: import of a snapshot set + - tests: more output for sbuild test + - o/snapstate: re-order remove tasks for individual snap revisions + to remove current last + - boot: skip some unit tests when running as root + - o/assertstate: introduce + ValidationTrackingKey/ValidationSetTracking and basic methods + - many: allow ignoring running apps for specific request + - tests: allow the searching test to fail under load + - overlord/snapstate: inhibit startup while unlinked + - seed/seedwriter/writer.go: check DevModeConfinement for dangerous + features + - tests/main/sudo-env: snap bin is available on Fedora + - boot, overlord/devicestate: list trusted and managed assets + upfront + - gadget, gadget/install: support for ubuntu-save, create one during + install if needed + - spread-shellcheck: temporary workaround for deadlock, drop + unnecessary test + - snap: support different exit-code in the snap command + - logger: use strutil.KernelCommandLineSplit in + debugEnabledOnKernelCmdline + - logger: fix snapd.debug=1 parsing + - overlord: increase refresh postpone limit to 14 days + - spread-shellcheck: use single thread pool executor + - gadget/install,secboot: add debug messages + - spread-shellcheck: speed up spread-shellcheck even more + - spread-shellcheck: process paths from arguments in parallel + - tests: tweak error from tests.cleanup + - spread: remove workaround for openSUSE go issue + - o/configstate: create /etc/sysctl.d when applying early config + defaults + - tests: new tests.backup tool + - tests: add tests.cleanup pop sub-command + - tests: migration of the main suite to snaps-state tool part 6 + - tests: fix journal-state test + - cmd/snap-bootstrap/initramfs-mounts: split off new helper for misc + recover files + - cmd/snap-bootstrap/initramfs-mounts: also copy /etc/machine-id for + same IP addr + - packaging/{ubuntu,debian}: add liblzo2-dev as a dependency for + building snapd + - boot, gadget, bootloader: observer preserves managed bootloader + configs + - tests/nested/manual: add uc20 grade signed cloud-init test + - o/snapstate/autorefresh.go: eliminate race when launching + autorefresh + - daemon,snapshotstate: do not return "size" from Import() + - daemon: limit reading from snapshot import to Content-Length + - many: set/expect Content-Length header when importing snapshots + - github: switch from ::set-env command to environment file + - tests: migration of the main suite to snaps-state tool part 5 + - client: cleanup the Client.raw* and Client.do* method families + - tests: moving main suite to snaps-state tool part 4 + - client,daemon,snap: use constant for snapshot content-type + - many: fix typos and repeated "the" + - secboot: fix tpm connection leak when it's not enabled + - many: scaffolding for snapshots import API + - run-checks: run spread-shellcheck too + - interfaces: update network-manager interface to allow + ObjectManager access from unconfined clients + - tests: move core and regression suites to snaps-state tool + - tests: moving interfaces tests to snaps-state tool + - gadget: preserve files when indicated by content change observer + - tests: moving smoke test suite and some tests from main suite to + snaps-state tool + - o/snapshotstate: pass set id to backend.Open, update tests + - asserts/snapasserts: introduce ValidationSets + - o/snapshotstate: improve allocation of new set IDs + - boot: look at the gadget for run mode bootloader when making the + system bootable + - cmd/snap: allow snap help vs --all to diverge purposefully + - usersession/userd: separate bus name ownership from defining + interfaces + - o/snapshotstate: set snapshot set id from its filename + - o/snapstate: move remove-related tests to snapstate_remove_test.go + - desktop/notification: switch ExpireTimeout to time.Duration + - desktop/notification: add unit tests + - snap: snap help output refresh + - tests/nested/manual/preseed: include a system-usernames snap when + preseeding + - tests: fix sudo-env test + - tests: fix nested core20 shellcheck bug + - tests/lib: move to new directory when restoring PWD, cleanup + unpacked unpacked snap directories + - desktop/notification: add bindings for FDO notifications + - dbustest: fix stale comment references + - many: move ManagedAssetsBootloader into TrustedAssetsBootloader, + drop former + - snap-repair: add uc20 support + - tests: print all the serial logs for the nested test + - o/snapstate/check_snap_test.go: mock osutil.Find{U,G}id to avoid + bug in test + - cmd/snap/auto-import: stop importing system user assertions from + initramfs mnts + - osutil/group.go: treat all non-nil errs from user.Lookup{Group,} + as Unknown* + - asserts: deserialize grouping only once in Pool.AddBatch if needed + - gadget: allow content observer to have opinions about a change + - tests: new snaps-state command - part1 + - o/assertstate: support refreshing any number of snap-declarations + - boot: use test helpers + - tests/core/snap-debug-bootvars: also check snap_mode + - many/apparmor: adjust rules for reading profile/ execing new + profiles for new kernel + - tests/core/snap-debug-bootvars: spread test for snap debug boot- + vars + - tests/lib/nested.sh: more little tweaks + - tests/nested/manual/grade-signed-above-testkeys-boot: enable kvm + - cmd/s-b/initramfs-mounts: use ConfigureTargetSystem for install, + recover modes + - overlord: explicitly set refresh-app-awareness in tests + - kernel: remove "edition" from kernel.yaml and add "update" + - spread: drop vendor from the packed project archive + - boot: fix debug bootloader variables dump on UC20 systems + - wrappers, systemd: allow empty root dir and conditionally do not + pass --root to systemctl + - tests/nested/manual: add test for grades above signed booting with + testkeys + - tests/nested: misc robustness fixes + - o/assertstate,asserts: use bulk refresh to refresh snap- + declarations + - tests/lib/prepare.sh: stop patching the uc20 initrd since it has + been updated now + - tests/nested/manual/refresh-revert-fundamentals: re-enable test + - update-pot: ignore .go files inside .git when running xgettext-go + - tests: disable part of the lxd test completely on 16.04. + - o/snapshotstate: tweak comment regarding snapshot filename + - o/snapstate: improve snapshot iteration + - bootloader: lk cleanups + - tests: update to support nested kvm without reboots on UC20 + - tests/nested/manual/preseed: disable system-key check for 20.04 + image + - spread.yaml: add ubuntu-20.10-64 to qemu + - store: handle v2 error when fetching assertions + - gadget: resolve device mapper devices for fallback device lookup + - tests/nested/cloud-init-many: simplify tests and unify + helpers/seed inputs + - tests: copy /usr/lib/snapd/info to correct directory + - check-pr-title.py * : allow "*" in the first part of the title + - many: typos and small test tweak + - tests/main/lxd: disable cgroup combination for 16.04 that is + failing a lot + - tests: make nested signing helpers less confusing + - tests: misc nested changes + - tests/nested/manual/refresh-revert-fundamentals: disable + temporarily + - tests/lib/cla_check: default to Python 3, tweaks, formatting + - tests/lib/cl_check.py: use python3 compatible code + * Thu Oct 08 2020 Michael Vogt - New upstream release 2.47.1 - o/configstate: create /etc/sysctl.d when applying early config diff -Nru snapd-2.47.1+20.10.1build1/packaging/fedora-30/snapd.spec snapd-2.48+21.04/packaging/fedora-30/snapd.spec --- snapd-2.47.1+20.10.1build1/packaging/fedora-30/snapd.spec 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/packaging/fedora-30/snapd.spec 2020-11-19 16:51:02.000000000 +0000 @@ -97,7 +97,7 @@ %endif Name: snapd -Version: 2.47.1 +Version: 2.48 Release: 0%{?dist} Summary: A transactional software package manager License: GPLv3 @@ -915,6 +915,321 @@ %changelog +* Thu Nov 19 2020 Michael Vogt +- New upstream release 2.48 + - osutil: add KernelCommandLineKeyValue + - devicestate: implement boot.HasFDESetupHook + - boot/makebootable.go: set snapd_recovery_mode=install at image- + build time + - bootloader: use ForGadget when installing boot config + - interfaces/raw_usb: allow read access to /proc/tty/drivers + - boot: add scaffolding for "fde-setup" hook support for sealing + - tests: fix basic20 test on arm devices + - seed: make a shared seed system label validation helper + - snap: add new "fde-setup" hooktype + - cmd/snap-bootstrap, secboot, tests: misc cleanups, add spread test + - secboot,cmd/snap-bootstrap: fix degraded mode cases with better + device handling + - boot,dirs,c/snap-bootstrap: avoid InstallHost* at the cost of some + messiness + - tests/nested/manual/refresh-revert-fundamentals: temporarily + disable secure boot + - snap-bootstrap,secboot: call BlockPCRProtectionPolicies in all + boot modes + - many: address degraded recover mode feedback, cleanups + - tests: Use systemd-run on tests part2 + - tests: set the opensuse tumbleweed system as manual in spread.yaml + - secboot: call BlockPCRProtectionPolicies even if the TPM is + disabled + - vendor: update to current secboot + - cmd/snap-bootstrap,o/devicestate: use a secret to pair data and + save + - spread.yaml: increase number of workers on 20.10 + - snap: add new `snap recovery --show-keys` option + - tests: minor test tweaks suggested in the review of 9607 + - snapd-generator: set standard snapfuse options when generating + units for containers + - tests: enable lxd test on ubuntu-core-20 and 16.04-32 + - interfaces: share /tmp/.X11-unix/ from host or provider + - tests: enable main lxd test on 20.10 + - cmd/s-b/initramfs-mounts: refactor recover mode to implement + degraded mode + - gadget/install: add progress logging + - packaging: keep secboot/encrypt_dummy.go in debian + - interfaces/udev: use distro specific path to snap-device-helper + - o/devistate: fix chaining of tasks related to regular snaps when + preseeding + - gadget, overlord/devicestate: validate that system supports + encrypted data before install + - interfaces/fwupd: enforce the confined fwupd to align Ubuntu Core + ESP layout + - many: add /v2/system-recovery-keys API and client + - secboot, many: return UnlockMethod from Unlock* methods for future + usage + - many: mv keys to ubuntu-boot, move model file, rename keyring + prefix for secboot + - tests: using systemd-run instead of manually create a systemd unit + - part 1 + - secboot, cmd/snap-bootstrap: enable or disable activation with + recovery key + - secboot: refactor Unlock...IfEncrypted to take keyfile + check + disks first + - secboot: add LockTPMSealedKeys() to lock access to keys + independently + - gadget: correct sfdisk arguments + - bootloader/assets/grub: adjust fwsetup menuentry label + - tests: new boot state tool + - spread: use the official image for Ubuntu 20.10, no longer an + unstable system + - tests/lib/nested: enable snapd logging to console for core18 + - osutil/disks: re-implement partition searching for disk w/ non- + adjacent parts + - tests: using the nested-state tool in nested tests + - many: seal a fallback object to the recovery boot chain + - gadget, gadget/install: move helpers to install package, refactor + unit tests + - dirs: add "gentoo" to altDirDistros + - update-pot: include file locations in translation template, and + extract strings from desktop files + - gadget/many: drop usage of gpt attr 59 for indicating creation of + partitions + - gadget/quantity: tweak test name + - snap: fix failing unittest for quantity.FormatDuration() + - gadget/quantity: introduce a new package that captures quantities + - o/devicestate,a/sysdb: make a backup of the device serial to save + - tests: fix rare interaction of tests.session and specific tests + - features: enable classic-preserves-xdg-runtime-dir + - tests/nested/core20/save: check the bind mount and size bump + - o/devicetate,dirs: keep device keys in ubuntu-save/save for UC20 + - tests: rename hasHooks to hasInterfaceHooks in the ifacestate + tests + - o/devicestate: unit test tweaks + - boot: store the TPM{PolicyAuthKey,LockoutAuth}File in ubuntu-save + - testutil, cmd/snap/version: fix misc little errors + - overlord/devicestate: bind mount ubuntu-save under + /var/lib/snapd/save on startup + - gadget/internal: tune ext4 setting for smaller filesystems + - tests/nested/core20/save: a test that verifies ubuntu-save is + present and set up + - tests: update google sru backend to support groovy + - o/ifacestate: handle interface hooks when preseeding + - tests: re-enable the apt hooks test + - interfaces,snap: use correct type: {os,snapd} for test data + - secboot: set metadata and keyslots sizes when formatting LUKS2 + volumes + - tests: improve uc20-create-partitions-reinstall test + - client, daemon, cmd/snap: cleanups from #9489 + more unit tests + - cmd/snap-bootstrap: mount ubuntu-save during boot if present + - secboot: fix doc comment on helper for unlocking volume with key + - tests: add spread test for refreshing from an old snapd and core18 + - o/snapstate: generate snapd snap wrappers again after restart on + refresh + - secboot: version bump, unlock volume with key + - tests/snap-advise-command: re-enable test + - cmd/snap, snapmgr, tests: cleanups after #9418 + - interfaces: deny connected x11 plugs access to ICE + - daemon,client: write and read a maintenance.json file for when + snapd is shut down + - many: update to secboot v1 (part 1) + - osutil/disks/mockdisk: panic if same mountpoint shows up again + with diff opts + - tests/nested/core20/gadget,kernel-reseal: add sanity checks to the + reseal tests + - many: implement snap routine console-conf-start for synchronizing + auto-refreshes + - dirs, boot: add ubuntu-save directories and related locations + - usersession: fix typo in test name + - overlord/snapstate: refactor ihibitRefresh + - overlord/snapstate: stop warning about inhibited refreshes + - cmd/snap: do not hardcode snapshot age value + - overlord,usersession: initial notifications of pending refreshes + - tests: add a unit test for UpdateMany where a single snap fails + - o/snapstate/catalogrefresh.go: don't refresh catalog in install + mode uc20 + - tests: also check snapst.Current in undo-unlink tests + - tests: new nested tool + - o/snapstate: implement undo handler for unlink-snap + - tests: clean systems.sh helper and migrate last set of tests + - tests: moving the lib section from systems.sh helper to os.query + tool + - tests/uc20-create-partitions: don't check for grub.cfg + - packaging: make sure that static binaries are indeed static, fix + openSUSE + - many: have install return encryption keys for data and save, + improve tests + - overlord: add link participant for linkage transitions + - tests: lxd smoke test + - tests: add tests for fsck; cmd/s-b/initramfs-mounts: fsck ubuntu- + seed too + - tests: moving main suite from systems.sh to os.query tool + - tests: moving the core test suite from systems.sh to os.query tool + - cmd/snap-confine: mask host's apparmor config + - o/snapstate: move setting updated SnapState after error paths + - tests: add value to INSTANCE_KEY/regular + - spread, tests: tweaks for openSUSE + - cmd/snap-confine: update path to snap-device-helper in AppArmor + profile + - tests: new os.query tool + - overlord/snapshotstate/backend: specify tar format for snapshots + - tests/nested/manual/minimal-smoke: use 384MB of RAM for nested + UC20 + - client,daemon,snap: auto-import does not error on managed devices + - interfaces: PTP hardware clock interface + - tests: use tests.backup tool + - many: verify that unit tests work with nosecboot tag and without + secboot package + - wrappers: do not error out on read-only /etc/dbus-1/session.d + filesystem on core18 + - snapshots: import of a snapshot set + - tests: more output for sbuild test + - o/snapstate: re-order remove tasks for individual snap revisions + to remove current last + - boot: skip some unit tests when running as root + - o/assertstate: introduce + ValidationTrackingKey/ValidationSetTracking and basic methods + - many: allow ignoring running apps for specific request + - tests: allow the searching test to fail under load + - overlord/snapstate: inhibit startup while unlinked + - seed/seedwriter/writer.go: check DevModeConfinement for dangerous + features + - tests/main/sudo-env: snap bin is available on Fedora + - boot, overlord/devicestate: list trusted and managed assets + upfront + - gadget, gadget/install: support for ubuntu-save, create one during + install if needed + - spread-shellcheck: temporary workaround for deadlock, drop + unnecessary test + - snap: support different exit-code in the snap command + - logger: use strutil.KernelCommandLineSplit in + debugEnabledOnKernelCmdline + - logger: fix snapd.debug=1 parsing + - overlord: increase refresh postpone limit to 14 days + - spread-shellcheck: use single thread pool executor + - gadget/install,secboot: add debug messages + - spread-shellcheck: speed up spread-shellcheck even more + - spread-shellcheck: process paths from arguments in parallel + - tests: tweak error from tests.cleanup + - spread: remove workaround for openSUSE go issue + - o/configstate: create /etc/sysctl.d when applying early config + defaults + - tests: new tests.backup tool + - tests: add tests.cleanup pop sub-command + - tests: migration of the main suite to snaps-state tool part 6 + - tests: fix journal-state test + - cmd/snap-bootstrap/initramfs-mounts: split off new helper for misc + recover files + - cmd/snap-bootstrap/initramfs-mounts: also copy /etc/machine-id for + same IP addr + - packaging/{ubuntu,debian}: add liblzo2-dev as a dependency for + building snapd + - boot, gadget, bootloader: observer preserves managed bootloader + configs + - tests/nested/manual: add uc20 grade signed cloud-init test + - o/snapstate/autorefresh.go: eliminate race when launching + autorefresh + - daemon,snapshotstate: do not return "size" from Import() + - daemon: limit reading from snapshot import to Content-Length + - many: set/expect Content-Length header when importing snapshots + - github: switch from ::set-env command to environment file + - tests: migration of the main suite to snaps-state tool part 5 + - client: cleanup the Client.raw* and Client.do* method families + - tests: moving main suite to snaps-state tool part 4 + - client,daemon,snap: use constant for snapshot content-type + - many: fix typos and repeated "the" + - secboot: fix tpm connection leak when it's not enabled + - many: scaffolding for snapshots import API + - run-checks: run spread-shellcheck too + - interfaces: update network-manager interface to allow + ObjectManager access from unconfined clients + - tests: move core and regression suites to snaps-state tool + - tests: moving interfaces tests to snaps-state tool + - gadget: preserve files when indicated by content change observer + - tests: moving smoke test suite and some tests from main suite to + snaps-state tool + - o/snapshotstate: pass set id to backend.Open, update tests + - asserts/snapasserts: introduce ValidationSets + - o/snapshotstate: improve allocation of new set IDs + - boot: look at the gadget for run mode bootloader when making the + system bootable + - cmd/snap: allow snap help vs --all to diverge purposefully + - usersession/userd: separate bus name ownership from defining + interfaces + - o/snapshotstate: set snapshot set id from its filename + - o/snapstate: move remove-related tests to snapstate_remove_test.go + - desktop/notification: switch ExpireTimeout to time.Duration + - desktop/notification: add unit tests + - snap: snap help output refresh + - tests/nested/manual/preseed: include a system-usernames snap when + preseeding + - tests: fix sudo-env test + - tests: fix nested core20 shellcheck bug + - tests/lib: move to new directory when restoring PWD, cleanup + unpacked unpacked snap directories + - desktop/notification: add bindings for FDO notifications + - dbustest: fix stale comment references + - many: move ManagedAssetsBootloader into TrustedAssetsBootloader, + drop former + - snap-repair: add uc20 support + - tests: print all the serial logs for the nested test + - o/snapstate/check_snap_test.go: mock osutil.Find{U,G}id to avoid + bug in test + - cmd/snap/auto-import: stop importing system user assertions from + initramfs mnts + - osutil/group.go: treat all non-nil errs from user.Lookup{Group,} + as Unknown* + - asserts: deserialize grouping only once in Pool.AddBatch if needed + - gadget: allow content observer to have opinions about a change + - tests: new snaps-state command - part1 + - o/assertstate: support refreshing any number of snap-declarations + - boot: use test helpers + - tests/core/snap-debug-bootvars: also check snap_mode + - many/apparmor: adjust rules for reading profile/ execing new + profiles for new kernel + - tests/core/snap-debug-bootvars: spread test for snap debug boot- + vars + - tests/lib/nested.sh: more little tweaks + - tests/nested/manual/grade-signed-above-testkeys-boot: enable kvm + - cmd/s-b/initramfs-mounts: use ConfigureTargetSystem for install, + recover modes + - overlord: explicitly set refresh-app-awareness in tests + - kernel: remove "edition" from kernel.yaml and add "update" + - spread: drop vendor from the packed project archive + - boot: fix debug bootloader variables dump on UC20 systems + - wrappers, systemd: allow empty root dir and conditionally do not + pass --root to systemctl + - tests/nested/manual: add test for grades above signed booting with + testkeys + - tests/nested: misc robustness fixes + - o/assertstate,asserts: use bulk refresh to refresh snap- + declarations + - tests/lib/prepare.sh: stop patching the uc20 initrd since it has + been updated now + - tests/nested/manual/refresh-revert-fundamentals: re-enable test + - update-pot: ignore .go files inside .git when running xgettext-go + - tests: disable part of the lxd test completely on 16.04. + - o/snapshotstate: tweak comment regarding snapshot filename + - o/snapstate: improve snapshot iteration + - bootloader: lk cleanups + - tests: update to support nested kvm without reboots on UC20 + - tests/nested/manual/preseed: disable system-key check for 20.04 + image + - spread.yaml: add ubuntu-20.10-64 to qemu + - store: handle v2 error when fetching assertions + - gadget: resolve device mapper devices for fallback device lookup + - tests/nested/cloud-init-many: simplify tests and unify + helpers/seed inputs + - tests: copy /usr/lib/snapd/info to correct directory + - check-pr-title.py * : allow "*" in the first part of the title + - many: typos and small test tweak + - tests/main/lxd: disable cgroup combination for 16.04 that is + failing a lot + - tests: make nested signing helpers less confusing + - tests: misc nested changes + - tests/nested/manual/refresh-revert-fundamentals: disable + temporarily + - tests/lib/cla_check: default to Python 3, tweaks, formatting + - tests/lib/cl_check.py: use python3 compatible code + * Thu Oct 08 2020 Michael Vogt - New upstream release 2.47.1 - o/configstate: create /etc/sysctl.d when applying early config diff -Nru snapd-2.47.1+20.10.1build1/packaging/fedora-31/snapd.spec snapd-2.48+21.04/packaging/fedora-31/snapd.spec --- snapd-2.47.1+20.10.1build1/packaging/fedora-31/snapd.spec 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/packaging/fedora-31/snapd.spec 2020-11-19 16:51:02.000000000 +0000 @@ -97,7 +97,7 @@ %endif Name: snapd -Version: 2.47.1 +Version: 2.48 Release: 0%{?dist} Summary: A transactional software package manager License: GPLv3 @@ -915,6 +915,321 @@ %changelog +* Thu Nov 19 2020 Michael Vogt +- New upstream release 2.48 + - osutil: add KernelCommandLineKeyValue + - devicestate: implement boot.HasFDESetupHook + - boot/makebootable.go: set snapd_recovery_mode=install at image- + build time + - bootloader: use ForGadget when installing boot config + - interfaces/raw_usb: allow read access to /proc/tty/drivers + - boot: add scaffolding for "fde-setup" hook support for sealing + - tests: fix basic20 test on arm devices + - seed: make a shared seed system label validation helper + - snap: add new "fde-setup" hooktype + - cmd/snap-bootstrap, secboot, tests: misc cleanups, add spread test + - secboot,cmd/snap-bootstrap: fix degraded mode cases with better + device handling + - boot,dirs,c/snap-bootstrap: avoid InstallHost* at the cost of some + messiness + - tests/nested/manual/refresh-revert-fundamentals: temporarily + disable secure boot + - snap-bootstrap,secboot: call BlockPCRProtectionPolicies in all + boot modes + - many: address degraded recover mode feedback, cleanups + - tests: Use systemd-run on tests part2 + - tests: set the opensuse tumbleweed system as manual in spread.yaml + - secboot: call BlockPCRProtectionPolicies even if the TPM is + disabled + - vendor: update to current secboot + - cmd/snap-bootstrap,o/devicestate: use a secret to pair data and + save + - spread.yaml: increase number of workers on 20.10 + - snap: add new `snap recovery --show-keys` option + - tests: minor test tweaks suggested in the review of 9607 + - snapd-generator: set standard snapfuse options when generating + units for containers + - tests: enable lxd test on ubuntu-core-20 and 16.04-32 + - interfaces: share /tmp/.X11-unix/ from host or provider + - tests: enable main lxd test on 20.10 + - cmd/s-b/initramfs-mounts: refactor recover mode to implement + degraded mode + - gadget/install: add progress logging + - packaging: keep secboot/encrypt_dummy.go in debian + - interfaces/udev: use distro specific path to snap-device-helper + - o/devistate: fix chaining of tasks related to regular snaps when + preseeding + - gadget, overlord/devicestate: validate that system supports + encrypted data before install + - interfaces/fwupd: enforce the confined fwupd to align Ubuntu Core + ESP layout + - many: add /v2/system-recovery-keys API and client + - secboot, many: return UnlockMethod from Unlock* methods for future + usage + - many: mv keys to ubuntu-boot, move model file, rename keyring + prefix for secboot + - tests: using systemd-run instead of manually create a systemd unit + - part 1 + - secboot, cmd/snap-bootstrap: enable or disable activation with + recovery key + - secboot: refactor Unlock...IfEncrypted to take keyfile + check + disks first + - secboot: add LockTPMSealedKeys() to lock access to keys + independently + - gadget: correct sfdisk arguments + - bootloader/assets/grub: adjust fwsetup menuentry label + - tests: new boot state tool + - spread: use the official image for Ubuntu 20.10, no longer an + unstable system + - tests/lib/nested: enable snapd logging to console for core18 + - osutil/disks: re-implement partition searching for disk w/ non- + adjacent parts + - tests: using the nested-state tool in nested tests + - many: seal a fallback object to the recovery boot chain + - gadget, gadget/install: move helpers to install package, refactor + unit tests + - dirs: add "gentoo" to altDirDistros + - update-pot: include file locations in translation template, and + extract strings from desktop files + - gadget/many: drop usage of gpt attr 59 for indicating creation of + partitions + - gadget/quantity: tweak test name + - snap: fix failing unittest for quantity.FormatDuration() + - gadget/quantity: introduce a new package that captures quantities + - o/devicestate,a/sysdb: make a backup of the device serial to save + - tests: fix rare interaction of tests.session and specific tests + - features: enable classic-preserves-xdg-runtime-dir + - tests/nested/core20/save: check the bind mount and size bump + - o/devicetate,dirs: keep device keys in ubuntu-save/save for UC20 + - tests: rename hasHooks to hasInterfaceHooks in the ifacestate + tests + - o/devicestate: unit test tweaks + - boot: store the TPM{PolicyAuthKey,LockoutAuth}File in ubuntu-save + - testutil, cmd/snap/version: fix misc little errors + - overlord/devicestate: bind mount ubuntu-save under + /var/lib/snapd/save on startup + - gadget/internal: tune ext4 setting for smaller filesystems + - tests/nested/core20/save: a test that verifies ubuntu-save is + present and set up + - tests: update google sru backend to support groovy + - o/ifacestate: handle interface hooks when preseeding + - tests: re-enable the apt hooks test + - interfaces,snap: use correct type: {os,snapd} for test data + - secboot: set metadata and keyslots sizes when formatting LUKS2 + volumes + - tests: improve uc20-create-partitions-reinstall test + - client, daemon, cmd/snap: cleanups from #9489 + more unit tests + - cmd/snap-bootstrap: mount ubuntu-save during boot if present + - secboot: fix doc comment on helper for unlocking volume with key + - tests: add spread test for refreshing from an old snapd and core18 + - o/snapstate: generate snapd snap wrappers again after restart on + refresh + - secboot: version bump, unlock volume with key + - tests/snap-advise-command: re-enable test + - cmd/snap, snapmgr, tests: cleanups after #9418 + - interfaces: deny connected x11 plugs access to ICE + - daemon,client: write and read a maintenance.json file for when + snapd is shut down + - many: update to secboot v1 (part 1) + - osutil/disks/mockdisk: panic if same mountpoint shows up again + with diff opts + - tests/nested/core20/gadget,kernel-reseal: add sanity checks to the + reseal tests + - many: implement snap routine console-conf-start for synchronizing + auto-refreshes + - dirs, boot: add ubuntu-save directories and related locations + - usersession: fix typo in test name + - overlord/snapstate: refactor ihibitRefresh + - overlord/snapstate: stop warning about inhibited refreshes + - cmd/snap: do not hardcode snapshot age value + - overlord,usersession: initial notifications of pending refreshes + - tests: add a unit test for UpdateMany where a single snap fails + - o/snapstate/catalogrefresh.go: don't refresh catalog in install + mode uc20 + - tests: also check snapst.Current in undo-unlink tests + - tests: new nested tool + - o/snapstate: implement undo handler for unlink-snap + - tests: clean systems.sh helper and migrate last set of tests + - tests: moving the lib section from systems.sh helper to os.query + tool + - tests/uc20-create-partitions: don't check for grub.cfg + - packaging: make sure that static binaries are indeed static, fix + openSUSE + - many: have install return encryption keys for data and save, + improve tests + - overlord: add link participant for linkage transitions + - tests: lxd smoke test + - tests: add tests for fsck; cmd/s-b/initramfs-mounts: fsck ubuntu- + seed too + - tests: moving main suite from systems.sh to os.query tool + - tests: moving the core test suite from systems.sh to os.query tool + - cmd/snap-confine: mask host's apparmor config + - o/snapstate: move setting updated SnapState after error paths + - tests: add value to INSTANCE_KEY/regular + - spread, tests: tweaks for openSUSE + - cmd/snap-confine: update path to snap-device-helper in AppArmor + profile + - tests: new os.query tool + - overlord/snapshotstate/backend: specify tar format for snapshots + - tests/nested/manual/minimal-smoke: use 384MB of RAM for nested + UC20 + - client,daemon,snap: auto-import does not error on managed devices + - interfaces: PTP hardware clock interface + - tests: use tests.backup tool + - many: verify that unit tests work with nosecboot tag and without + secboot package + - wrappers: do not error out on read-only /etc/dbus-1/session.d + filesystem on core18 + - snapshots: import of a snapshot set + - tests: more output for sbuild test + - o/snapstate: re-order remove tasks for individual snap revisions + to remove current last + - boot: skip some unit tests when running as root + - o/assertstate: introduce + ValidationTrackingKey/ValidationSetTracking and basic methods + - many: allow ignoring running apps for specific request + - tests: allow the searching test to fail under load + - overlord/snapstate: inhibit startup while unlinked + - seed/seedwriter/writer.go: check DevModeConfinement for dangerous + features + - tests/main/sudo-env: snap bin is available on Fedora + - boot, overlord/devicestate: list trusted and managed assets + upfront + - gadget, gadget/install: support for ubuntu-save, create one during + install if needed + - spread-shellcheck: temporary workaround for deadlock, drop + unnecessary test + - snap: support different exit-code in the snap command + - logger: use strutil.KernelCommandLineSplit in + debugEnabledOnKernelCmdline + - logger: fix snapd.debug=1 parsing + - overlord: increase refresh postpone limit to 14 days + - spread-shellcheck: use single thread pool executor + - gadget/install,secboot: add debug messages + - spread-shellcheck: speed up spread-shellcheck even more + - spread-shellcheck: process paths from arguments in parallel + - tests: tweak error from tests.cleanup + - spread: remove workaround for openSUSE go issue + - o/configstate: create /etc/sysctl.d when applying early config + defaults + - tests: new tests.backup tool + - tests: add tests.cleanup pop sub-command + - tests: migration of the main suite to snaps-state tool part 6 + - tests: fix journal-state test + - cmd/snap-bootstrap/initramfs-mounts: split off new helper for misc + recover files + - cmd/snap-bootstrap/initramfs-mounts: also copy /etc/machine-id for + same IP addr + - packaging/{ubuntu,debian}: add liblzo2-dev as a dependency for + building snapd + - boot, gadget, bootloader: observer preserves managed bootloader + configs + - tests/nested/manual: add uc20 grade signed cloud-init test + - o/snapstate/autorefresh.go: eliminate race when launching + autorefresh + - daemon,snapshotstate: do not return "size" from Import() + - daemon: limit reading from snapshot import to Content-Length + - many: set/expect Content-Length header when importing snapshots + - github: switch from ::set-env command to environment file + - tests: migration of the main suite to snaps-state tool part 5 + - client: cleanup the Client.raw* and Client.do* method families + - tests: moving main suite to snaps-state tool part 4 + - client,daemon,snap: use constant for snapshot content-type + - many: fix typos and repeated "the" + - secboot: fix tpm connection leak when it's not enabled + - many: scaffolding for snapshots import API + - run-checks: run spread-shellcheck too + - interfaces: update network-manager interface to allow + ObjectManager access from unconfined clients + - tests: move core and regression suites to snaps-state tool + - tests: moving interfaces tests to snaps-state tool + - gadget: preserve files when indicated by content change observer + - tests: moving smoke test suite and some tests from main suite to + snaps-state tool + - o/snapshotstate: pass set id to backend.Open, update tests + - asserts/snapasserts: introduce ValidationSets + - o/snapshotstate: improve allocation of new set IDs + - boot: look at the gadget for run mode bootloader when making the + system bootable + - cmd/snap: allow snap help vs --all to diverge purposefully + - usersession/userd: separate bus name ownership from defining + interfaces + - o/snapshotstate: set snapshot set id from its filename + - o/snapstate: move remove-related tests to snapstate_remove_test.go + - desktop/notification: switch ExpireTimeout to time.Duration + - desktop/notification: add unit tests + - snap: snap help output refresh + - tests/nested/manual/preseed: include a system-usernames snap when + preseeding + - tests: fix sudo-env test + - tests: fix nested core20 shellcheck bug + - tests/lib: move to new directory when restoring PWD, cleanup + unpacked unpacked snap directories + - desktop/notification: add bindings for FDO notifications + - dbustest: fix stale comment references + - many: move ManagedAssetsBootloader into TrustedAssetsBootloader, + drop former + - snap-repair: add uc20 support + - tests: print all the serial logs for the nested test + - o/snapstate/check_snap_test.go: mock osutil.Find{U,G}id to avoid + bug in test + - cmd/snap/auto-import: stop importing system user assertions from + initramfs mnts + - osutil/group.go: treat all non-nil errs from user.Lookup{Group,} + as Unknown* + - asserts: deserialize grouping only once in Pool.AddBatch if needed + - gadget: allow content observer to have opinions about a change + - tests: new snaps-state command - part1 + - o/assertstate: support refreshing any number of snap-declarations + - boot: use test helpers + - tests/core/snap-debug-bootvars: also check snap_mode + - many/apparmor: adjust rules for reading profile/ execing new + profiles for new kernel + - tests/core/snap-debug-bootvars: spread test for snap debug boot- + vars + - tests/lib/nested.sh: more little tweaks + - tests/nested/manual/grade-signed-above-testkeys-boot: enable kvm + - cmd/s-b/initramfs-mounts: use ConfigureTargetSystem for install, + recover modes + - overlord: explicitly set refresh-app-awareness in tests + - kernel: remove "edition" from kernel.yaml and add "update" + - spread: drop vendor from the packed project archive + - boot: fix debug bootloader variables dump on UC20 systems + - wrappers, systemd: allow empty root dir and conditionally do not + pass --root to systemctl + - tests/nested/manual: add test for grades above signed booting with + testkeys + - tests/nested: misc robustness fixes + - o/assertstate,asserts: use bulk refresh to refresh snap- + declarations + - tests/lib/prepare.sh: stop patching the uc20 initrd since it has + been updated now + - tests/nested/manual/refresh-revert-fundamentals: re-enable test + - update-pot: ignore .go files inside .git when running xgettext-go + - tests: disable part of the lxd test completely on 16.04. + - o/snapshotstate: tweak comment regarding snapshot filename + - o/snapstate: improve snapshot iteration + - bootloader: lk cleanups + - tests: update to support nested kvm without reboots on UC20 + - tests/nested/manual/preseed: disable system-key check for 20.04 + image + - spread.yaml: add ubuntu-20.10-64 to qemu + - store: handle v2 error when fetching assertions + - gadget: resolve device mapper devices for fallback device lookup + - tests/nested/cloud-init-many: simplify tests and unify + helpers/seed inputs + - tests: copy /usr/lib/snapd/info to correct directory + - check-pr-title.py * : allow "*" in the first part of the title + - many: typos and small test tweak + - tests/main/lxd: disable cgroup combination for 16.04 that is + failing a lot + - tests: make nested signing helpers less confusing + - tests: misc nested changes + - tests/nested/manual/refresh-revert-fundamentals: disable + temporarily + - tests/lib/cla_check: default to Python 3, tweaks, formatting + - tests/lib/cl_check.py: use python3 compatible code + * Thu Oct 08 2020 Michael Vogt - New upstream release 2.47.1 - o/configstate: create /etc/sysctl.d when applying early config diff -Nru snapd-2.47.1+20.10.1build1/packaging/fedora-32/snapd.spec snapd-2.48+21.04/packaging/fedora-32/snapd.spec --- snapd-2.47.1+20.10.1build1/packaging/fedora-32/snapd.spec 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/packaging/fedora-32/snapd.spec 2020-11-19 16:51:02.000000000 +0000 @@ -97,7 +97,7 @@ %endif Name: snapd -Version: 2.47.1 +Version: 2.48 Release: 0%{?dist} Summary: A transactional software package manager License: GPLv3 @@ -915,6 +915,321 @@ %changelog +* Thu Nov 19 2020 Michael Vogt +- New upstream release 2.48 + - osutil: add KernelCommandLineKeyValue + - devicestate: implement boot.HasFDESetupHook + - boot/makebootable.go: set snapd_recovery_mode=install at image- + build time + - bootloader: use ForGadget when installing boot config + - interfaces/raw_usb: allow read access to /proc/tty/drivers + - boot: add scaffolding for "fde-setup" hook support for sealing + - tests: fix basic20 test on arm devices + - seed: make a shared seed system label validation helper + - snap: add new "fde-setup" hooktype + - cmd/snap-bootstrap, secboot, tests: misc cleanups, add spread test + - secboot,cmd/snap-bootstrap: fix degraded mode cases with better + device handling + - boot,dirs,c/snap-bootstrap: avoid InstallHost* at the cost of some + messiness + - tests/nested/manual/refresh-revert-fundamentals: temporarily + disable secure boot + - snap-bootstrap,secboot: call BlockPCRProtectionPolicies in all + boot modes + - many: address degraded recover mode feedback, cleanups + - tests: Use systemd-run on tests part2 + - tests: set the opensuse tumbleweed system as manual in spread.yaml + - secboot: call BlockPCRProtectionPolicies even if the TPM is + disabled + - vendor: update to current secboot + - cmd/snap-bootstrap,o/devicestate: use a secret to pair data and + save + - spread.yaml: increase number of workers on 20.10 + - snap: add new `snap recovery --show-keys` option + - tests: minor test tweaks suggested in the review of 9607 + - snapd-generator: set standard snapfuse options when generating + units for containers + - tests: enable lxd test on ubuntu-core-20 and 16.04-32 + - interfaces: share /tmp/.X11-unix/ from host or provider + - tests: enable main lxd test on 20.10 + - cmd/s-b/initramfs-mounts: refactor recover mode to implement + degraded mode + - gadget/install: add progress logging + - packaging: keep secboot/encrypt_dummy.go in debian + - interfaces/udev: use distro specific path to snap-device-helper + - o/devistate: fix chaining of tasks related to regular snaps when + preseeding + - gadget, overlord/devicestate: validate that system supports + encrypted data before install + - interfaces/fwupd: enforce the confined fwupd to align Ubuntu Core + ESP layout + - many: add /v2/system-recovery-keys API and client + - secboot, many: return UnlockMethod from Unlock* methods for future + usage + - many: mv keys to ubuntu-boot, move model file, rename keyring + prefix for secboot + - tests: using systemd-run instead of manually create a systemd unit + - part 1 + - secboot, cmd/snap-bootstrap: enable or disable activation with + recovery key + - secboot: refactor Unlock...IfEncrypted to take keyfile + check + disks first + - secboot: add LockTPMSealedKeys() to lock access to keys + independently + - gadget: correct sfdisk arguments + - bootloader/assets/grub: adjust fwsetup menuentry label + - tests: new boot state tool + - spread: use the official image for Ubuntu 20.10, no longer an + unstable system + - tests/lib/nested: enable snapd logging to console for core18 + - osutil/disks: re-implement partition searching for disk w/ non- + adjacent parts + - tests: using the nested-state tool in nested tests + - many: seal a fallback object to the recovery boot chain + - gadget, gadget/install: move helpers to install package, refactor + unit tests + - dirs: add "gentoo" to altDirDistros + - update-pot: include file locations in translation template, and + extract strings from desktop files + - gadget/many: drop usage of gpt attr 59 for indicating creation of + partitions + - gadget/quantity: tweak test name + - snap: fix failing unittest for quantity.FormatDuration() + - gadget/quantity: introduce a new package that captures quantities + - o/devicestate,a/sysdb: make a backup of the device serial to save + - tests: fix rare interaction of tests.session and specific tests + - features: enable classic-preserves-xdg-runtime-dir + - tests/nested/core20/save: check the bind mount and size bump + - o/devicetate,dirs: keep device keys in ubuntu-save/save for UC20 + - tests: rename hasHooks to hasInterfaceHooks in the ifacestate + tests + - o/devicestate: unit test tweaks + - boot: store the TPM{PolicyAuthKey,LockoutAuth}File in ubuntu-save + - testutil, cmd/snap/version: fix misc little errors + - overlord/devicestate: bind mount ubuntu-save under + /var/lib/snapd/save on startup + - gadget/internal: tune ext4 setting for smaller filesystems + - tests/nested/core20/save: a test that verifies ubuntu-save is + present and set up + - tests: update google sru backend to support groovy + - o/ifacestate: handle interface hooks when preseeding + - tests: re-enable the apt hooks test + - interfaces,snap: use correct type: {os,snapd} for test data + - secboot: set metadata and keyslots sizes when formatting LUKS2 + volumes + - tests: improve uc20-create-partitions-reinstall test + - client, daemon, cmd/snap: cleanups from #9489 + more unit tests + - cmd/snap-bootstrap: mount ubuntu-save during boot if present + - secboot: fix doc comment on helper for unlocking volume with key + - tests: add spread test for refreshing from an old snapd and core18 + - o/snapstate: generate snapd snap wrappers again after restart on + refresh + - secboot: version bump, unlock volume with key + - tests/snap-advise-command: re-enable test + - cmd/snap, snapmgr, tests: cleanups after #9418 + - interfaces: deny connected x11 plugs access to ICE + - daemon,client: write and read a maintenance.json file for when + snapd is shut down + - many: update to secboot v1 (part 1) + - osutil/disks/mockdisk: panic if same mountpoint shows up again + with diff opts + - tests/nested/core20/gadget,kernel-reseal: add sanity checks to the + reseal tests + - many: implement snap routine console-conf-start for synchronizing + auto-refreshes + - dirs, boot: add ubuntu-save directories and related locations + - usersession: fix typo in test name + - overlord/snapstate: refactor ihibitRefresh + - overlord/snapstate: stop warning about inhibited refreshes + - cmd/snap: do not hardcode snapshot age value + - overlord,usersession: initial notifications of pending refreshes + - tests: add a unit test for UpdateMany where a single snap fails + - o/snapstate/catalogrefresh.go: don't refresh catalog in install + mode uc20 + - tests: also check snapst.Current in undo-unlink tests + - tests: new nested tool + - o/snapstate: implement undo handler for unlink-snap + - tests: clean systems.sh helper and migrate last set of tests + - tests: moving the lib section from systems.sh helper to os.query + tool + - tests/uc20-create-partitions: don't check for grub.cfg + - packaging: make sure that static binaries are indeed static, fix + openSUSE + - many: have install return encryption keys for data and save, + improve tests + - overlord: add link participant for linkage transitions + - tests: lxd smoke test + - tests: add tests for fsck; cmd/s-b/initramfs-mounts: fsck ubuntu- + seed too + - tests: moving main suite from systems.sh to os.query tool + - tests: moving the core test suite from systems.sh to os.query tool + - cmd/snap-confine: mask host's apparmor config + - o/snapstate: move setting updated SnapState after error paths + - tests: add value to INSTANCE_KEY/regular + - spread, tests: tweaks for openSUSE + - cmd/snap-confine: update path to snap-device-helper in AppArmor + profile + - tests: new os.query tool + - overlord/snapshotstate/backend: specify tar format for snapshots + - tests/nested/manual/minimal-smoke: use 384MB of RAM for nested + UC20 + - client,daemon,snap: auto-import does not error on managed devices + - interfaces: PTP hardware clock interface + - tests: use tests.backup tool + - many: verify that unit tests work with nosecboot tag and without + secboot package + - wrappers: do not error out on read-only /etc/dbus-1/session.d + filesystem on core18 + - snapshots: import of a snapshot set + - tests: more output for sbuild test + - o/snapstate: re-order remove tasks for individual snap revisions + to remove current last + - boot: skip some unit tests when running as root + - o/assertstate: introduce + ValidationTrackingKey/ValidationSetTracking and basic methods + - many: allow ignoring running apps for specific request + - tests: allow the searching test to fail under load + - overlord/snapstate: inhibit startup while unlinked + - seed/seedwriter/writer.go: check DevModeConfinement for dangerous + features + - tests/main/sudo-env: snap bin is available on Fedora + - boot, overlord/devicestate: list trusted and managed assets + upfront + - gadget, gadget/install: support for ubuntu-save, create one during + install if needed + - spread-shellcheck: temporary workaround for deadlock, drop + unnecessary test + - snap: support different exit-code in the snap command + - logger: use strutil.KernelCommandLineSplit in + debugEnabledOnKernelCmdline + - logger: fix snapd.debug=1 parsing + - overlord: increase refresh postpone limit to 14 days + - spread-shellcheck: use single thread pool executor + - gadget/install,secboot: add debug messages + - spread-shellcheck: speed up spread-shellcheck even more + - spread-shellcheck: process paths from arguments in parallel + - tests: tweak error from tests.cleanup + - spread: remove workaround for openSUSE go issue + - o/configstate: create /etc/sysctl.d when applying early config + defaults + - tests: new tests.backup tool + - tests: add tests.cleanup pop sub-command + - tests: migration of the main suite to snaps-state tool part 6 + - tests: fix journal-state test + - cmd/snap-bootstrap/initramfs-mounts: split off new helper for misc + recover files + - cmd/snap-bootstrap/initramfs-mounts: also copy /etc/machine-id for + same IP addr + - packaging/{ubuntu,debian}: add liblzo2-dev as a dependency for + building snapd + - boot, gadget, bootloader: observer preserves managed bootloader + configs + - tests/nested/manual: add uc20 grade signed cloud-init test + - o/snapstate/autorefresh.go: eliminate race when launching + autorefresh + - daemon,snapshotstate: do not return "size" from Import() + - daemon: limit reading from snapshot import to Content-Length + - many: set/expect Content-Length header when importing snapshots + - github: switch from ::set-env command to environment file + - tests: migration of the main suite to snaps-state tool part 5 + - client: cleanup the Client.raw* and Client.do* method families + - tests: moving main suite to snaps-state tool part 4 + - client,daemon,snap: use constant for snapshot content-type + - many: fix typos and repeated "the" + - secboot: fix tpm connection leak when it's not enabled + - many: scaffolding for snapshots import API + - run-checks: run spread-shellcheck too + - interfaces: update network-manager interface to allow + ObjectManager access from unconfined clients + - tests: move core and regression suites to snaps-state tool + - tests: moving interfaces tests to snaps-state tool + - gadget: preserve files when indicated by content change observer + - tests: moving smoke test suite and some tests from main suite to + snaps-state tool + - o/snapshotstate: pass set id to backend.Open, update tests + - asserts/snapasserts: introduce ValidationSets + - o/snapshotstate: improve allocation of new set IDs + - boot: look at the gadget for run mode bootloader when making the + system bootable + - cmd/snap: allow snap help vs --all to diverge purposefully + - usersession/userd: separate bus name ownership from defining + interfaces + - o/snapshotstate: set snapshot set id from its filename + - o/snapstate: move remove-related tests to snapstate_remove_test.go + - desktop/notification: switch ExpireTimeout to time.Duration + - desktop/notification: add unit tests + - snap: snap help output refresh + - tests/nested/manual/preseed: include a system-usernames snap when + preseeding + - tests: fix sudo-env test + - tests: fix nested core20 shellcheck bug + - tests/lib: move to new directory when restoring PWD, cleanup + unpacked unpacked snap directories + - desktop/notification: add bindings for FDO notifications + - dbustest: fix stale comment references + - many: move ManagedAssetsBootloader into TrustedAssetsBootloader, + drop former + - snap-repair: add uc20 support + - tests: print all the serial logs for the nested test + - o/snapstate/check_snap_test.go: mock osutil.Find{U,G}id to avoid + bug in test + - cmd/snap/auto-import: stop importing system user assertions from + initramfs mnts + - osutil/group.go: treat all non-nil errs from user.Lookup{Group,} + as Unknown* + - asserts: deserialize grouping only once in Pool.AddBatch if needed + - gadget: allow content observer to have opinions about a change + - tests: new snaps-state command - part1 + - o/assertstate: support refreshing any number of snap-declarations + - boot: use test helpers + - tests/core/snap-debug-bootvars: also check snap_mode + - many/apparmor: adjust rules for reading profile/ execing new + profiles for new kernel + - tests/core/snap-debug-bootvars: spread test for snap debug boot- + vars + - tests/lib/nested.sh: more little tweaks + - tests/nested/manual/grade-signed-above-testkeys-boot: enable kvm + - cmd/s-b/initramfs-mounts: use ConfigureTargetSystem for install, + recover modes + - overlord: explicitly set refresh-app-awareness in tests + - kernel: remove "edition" from kernel.yaml and add "update" + - spread: drop vendor from the packed project archive + - boot: fix debug bootloader variables dump on UC20 systems + - wrappers, systemd: allow empty root dir and conditionally do not + pass --root to systemctl + - tests/nested/manual: add test for grades above signed booting with + testkeys + - tests/nested: misc robustness fixes + - o/assertstate,asserts: use bulk refresh to refresh snap- + declarations + - tests/lib/prepare.sh: stop patching the uc20 initrd since it has + been updated now + - tests/nested/manual/refresh-revert-fundamentals: re-enable test + - update-pot: ignore .go files inside .git when running xgettext-go + - tests: disable part of the lxd test completely on 16.04. + - o/snapshotstate: tweak comment regarding snapshot filename + - o/snapstate: improve snapshot iteration + - bootloader: lk cleanups + - tests: update to support nested kvm without reboots on UC20 + - tests/nested/manual/preseed: disable system-key check for 20.04 + image + - spread.yaml: add ubuntu-20.10-64 to qemu + - store: handle v2 error when fetching assertions + - gadget: resolve device mapper devices for fallback device lookup + - tests/nested/cloud-init-many: simplify tests and unify + helpers/seed inputs + - tests: copy /usr/lib/snapd/info to correct directory + - check-pr-title.py * : allow "*" in the first part of the title + - many: typos and small test tweak + - tests/main/lxd: disable cgroup combination for 16.04 that is + failing a lot + - tests: make nested signing helpers less confusing + - tests: misc nested changes + - tests/nested/manual/refresh-revert-fundamentals: disable + temporarily + - tests/lib/cla_check: default to Python 3, tweaks, formatting + - tests/lib/cl_check.py: use python3 compatible code + * Thu Oct 08 2020 Michael Vogt - New upstream release 2.47.1 - o/configstate: create /etc/sysctl.d when applying early config diff -Nru snapd-2.47.1+20.10.1build1/packaging/fedora-rawhide/snapd.spec snapd-2.48+21.04/packaging/fedora-rawhide/snapd.spec --- snapd-2.47.1+20.10.1build1/packaging/fedora-rawhide/snapd.spec 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/packaging/fedora-rawhide/snapd.spec 2020-11-19 16:51:02.000000000 +0000 @@ -97,7 +97,7 @@ %endif Name: snapd -Version: 2.47.1 +Version: 2.48 Release: 0%{?dist} Summary: A transactional software package manager License: GPLv3 @@ -915,6 +915,321 @@ %changelog +* Thu Nov 19 2020 Michael Vogt +- New upstream release 2.48 + - osutil: add KernelCommandLineKeyValue + - devicestate: implement boot.HasFDESetupHook + - boot/makebootable.go: set snapd_recovery_mode=install at image- + build time + - bootloader: use ForGadget when installing boot config + - interfaces/raw_usb: allow read access to /proc/tty/drivers + - boot: add scaffolding for "fde-setup" hook support for sealing + - tests: fix basic20 test on arm devices + - seed: make a shared seed system label validation helper + - snap: add new "fde-setup" hooktype + - cmd/snap-bootstrap, secboot, tests: misc cleanups, add spread test + - secboot,cmd/snap-bootstrap: fix degraded mode cases with better + device handling + - boot,dirs,c/snap-bootstrap: avoid InstallHost* at the cost of some + messiness + - tests/nested/manual/refresh-revert-fundamentals: temporarily + disable secure boot + - snap-bootstrap,secboot: call BlockPCRProtectionPolicies in all + boot modes + - many: address degraded recover mode feedback, cleanups + - tests: Use systemd-run on tests part2 + - tests: set the opensuse tumbleweed system as manual in spread.yaml + - secboot: call BlockPCRProtectionPolicies even if the TPM is + disabled + - vendor: update to current secboot + - cmd/snap-bootstrap,o/devicestate: use a secret to pair data and + save + - spread.yaml: increase number of workers on 20.10 + - snap: add new `snap recovery --show-keys` option + - tests: minor test tweaks suggested in the review of 9607 + - snapd-generator: set standard snapfuse options when generating + units for containers + - tests: enable lxd test on ubuntu-core-20 and 16.04-32 + - interfaces: share /tmp/.X11-unix/ from host or provider + - tests: enable main lxd test on 20.10 + - cmd/s-b/initramfs-mounts: refactor recover mode to implement + degraded mode + - gadget/install: add progress logging + - packaging: keep secboot/encrypt_dummy.go in debian + - interfaces/udev: use distro specific path to snap-device-helper + - o/devistate: fix chaining of tasks related to regular snaps when + preseeding + - gadget, overlord/devicestate: validate that system supports + encrypted data before install + - interfaces/fwupd: enforce the confined fwupd to align Ubuntu Core + ESP layout + - many: add /v2/system-recovery-keys API and client + - secboot, many: return UnlockMethod from Unlock* methods for future + usage + - many: mv keys to ubuntu-boot, move model file, rename keyring + prefix for secboot + - tests: using systemd-run instead of manually create a systemd unit + - part 1 + - secboot, cmd/snap-bootstrap: enable or disable activation with + recovery key + - secboot: refactor Unlock...IfEncrypted to take keyfile + check + disks first + - secboot: add LockTPMSealedKeys() to lock access to keys + independently + - gadget: correct sfdisk arguments + - bootloader/assets/grub: adjust fwsetup menuentry label + - tests: new boot state tool + - spread: use the official image for Ubuntu 20.10, no longer an + unstable system + - tests/lib/nested: enable snapd logging to console for core18 + - osutil/disks: re-implement partition searching for disk w/ non- + adjacent parts + - tests: using the nested-state tool in nested tests + - many: seal a fallback object to the recovery boot chain + - gadget, gadget/install: move helpers to install package, refactor + unit tests + - dirs: add "gentoo" to altDirDistros + - update-pot: include file locations in translation template, and + extract strings from desktop files + - gadget/many: drop usage of gpt attr 59 for indicating creation of + partitions + - gadget/quantity: tweak test name + - snap: fix failing unittest for quantity.FormatDuration() + - gadget/quantity: introduce a new package that captures quantities + - o/devicestate,a/sysdb: make a backup of the device serial to save + - tests: fix rare interaction of tests.session and specific tests + - features: enable classic-preserves-xdg-runtime-dir + - tests/nested/core20/save: check the bind mount and size bump + - o/devicetate,dirs: keep device keys in ubuntu-save/save for UC20 + - tests: rename hasHooks to hasInterfaceHooks in the ifacestate + tests + - o/devicestate: unit test tweaks + - boot: store the TPM{PolicyAuthKey,LockoutAuth}File in ubuntu-save + - testutil, cmd/snap/version: fix misc little errors + - overlord/devicestate: bind mount ubuntu-save under + /var/lib/snapd/save on startup + - gadget/internal: tune ext4 setting for smaller filesystems + - tests/nested/core20/save: a test that verifies ubuntu-save is + present and set up + - tests: update google sru backend to support groovy + - o/ifacestate: handle interface hooks when preseeding + - tests: re-enable the apt hooks test + - interfaces,snap: use correct type: {os,snapd} for test data + - secboot: set metadata and keyslots sizes when formatting LUKS2 + volumes + - tests: improve uc20-create-partitions-reinstall test + - client, daemon, cmd/snap: cleanups from #9489 + more unit tests + - cmd/snap-bootstrap: mount ubuntu-save during boot if present + - secboot: fix doc comment on helper for unlocking volume with key + - tests: add spread test for refreshing from an old snapd and core18 + - o/snapstate: generate snapd snap wrappers again after restart on + refresh + - secboot: version bump, unlock volume with key + - tests/snap-advise-command: re-enable test + - cmd/snap, snapmgr, tests: cleanups after #9418 + - interfaces: deny connected x11 plugs access to ICE + - daemon,client: write and read a maintenance.json file for when + snapd is shut down + - many: update to secboot v1 (part 1) + - osutil/disks/mockdisk: panic if same mountpoint shows up again + with diff opts + - tests/nested/core20/gadget,kernel-reseal: add sanity checks to the + reseal tests + - many: implement snap routine console-conf-start for synchronizing + auto-refreshes + - dirs, boot: add ubuntu-save directories and related locations + - usersession: fix typo in test name + - overlord/snapstate: refactor ihibitRefresh + - overlord/snapstate: stop warning about inhibited refreshes + - cmd/snap: do not hardcode snapshot age value + - overlord,usersession: initial notifications of pending refreshes + - tests: add a unit test for UpdateMany where a single snap fails + - o/snapstate/catalogrefresh.go: don't refresh catalog in install + mode uc20 + - tests: also check snapst.Current in undo-unlink tests + - tests: new nested tool + - o/snapstate: implement undo handler for unlink-snap + - tests: clean systems.sh helper and migrate last set of tests + - tests: moving the lib section from systems.sh helper to os.query + tool + - tests/uc20-create-partitions: don't check for grub.cfg + - packaging: make sure that static binaries are indeed static, fix + openSUSE + - many: have install return encryption keys for data and save, + improve tests + - overlord: add link participant for linkage transitions + - tests: lxd smoke test + - tests: add tests for fsck; cmd/s-b/initramfs-mounts: fsck ubuntu- + seed too + - tests: moving main suite from systems.sh to os.query tool + - tests: moving the core test suite from systems.sh to os.query tool + - cmd/snap-confine: mask host's apparmor config + - o/snapstate: move setting updated SnapState after error paths + - tests: add value to INSTANCE_KEY/regular + - spread, tests: tweaks for openSUSE + - cmd/snap-confine: update path to snap-device-helper in AppArmor + profile + - tests: new os.query tool + - overlord/snapshotstate/backend: specify tar format for snapshots + - tests/nested/manual/minimal-smoke: use 384MB of RAM for nested + UC20 + - client,daemon,snap: auto-import does not error on managed devices + - interfaces: PTP hardware clock interface + - tests: use tests.backup tool + - many: verify that unit tests work with nosecboot tag and without + secboot package + - wrappers: do not error out on read-only /etc/dbus-1/session.d + filesystem on core18 + - snapshots: import of a snapshot set + - tests: more output for sbuild test + - o/snapstate: re-order remove tasks for individual snap revisions + to remove current last + - boot: skip some unit tests when running as root + - o/assertstate: introduce + ValidationTrackingKey/ValidationSetTracking and basic methods + - many: allow ignoring running apps for specific request + - tests: allow the searching test to fail under load + - overlord/snapstate: inhibit startup while unlinked + - seed/seedwriter/writer.go: check DevModeConfinement for dangerous + features + - tests/main/sudo-env: snap bin is available on Fedora + - boot, overlord/devicestate: list trusted and managed assets + upfront + - gadget, gadget/install: support for ubuntu-save, create one during + install if needed + - spread-shellcheck: temporary workaround for deadlock, drop + unnecessary test + - snap: support different exit-code in the snap command + - logger: use strutil.KernelCommandLineSplit in + debugEnabledOnKernelCmdline + - logger: fix snapd.debug=1 parsing + - overlord: increase refresh postpone limit to 14 days + - spread-shellcheck: use single thread pool executor + - gadget/install,secboot: add debug messages + - spread-shellcheck: speed up spread-shellcheck even more + - spread-shellcheck: process paths from arguments in parallel + - tests: tweak error from tests.cleanup + - spread: remove workaround for openSUSE go issue + - o/configstate: create /etc/sysctl.d when applying early config + defaults + - tests: new tests.backup tool + - tests: add tests.cleanup pop sub-command + - tests: migration of the main suite to snaps-state tool part 6 + - tests: fix journal-state test + - cmd/snap-bootstrap/initramfs-mounts: split off new helper for misc + recover files + - cmd/snap-bootstrap/initramfs-mounts: also copy /etc/machine-id for + same IP addr + - packaging/{ubuntu,debian}: add liblzo2-dev as a dependency for + building snapd + - boot, gadget, bootloader: observer preserves managed bootloader + configs + - tests/nested/manual: add uc20 grade signed cloud-init test + - o/snapstate/autorefresh.go: eliminate race when launching + autorefresh + - daemon,snapshotstate: do not return "size" from Import() + - daemon: limit reading from snapshot import to Content-Length + - many: set/expect Content-Length header when importing snapshots + - github: switch from ::set-env command to environment file + - tests: migration of the main suite to snaps-state tool part 5 + - client: cleanup the Client.raw* and Client.do* method families + - tests: moving main suite to snaps-state tool part 4 + - client,daemon,snap: use constant for snapshot content-type + - many: fix typos and repeated "the" + - secboot: fix tpm connection leak when it's not enabled + - many: scaffolding for snapshots import API + - run-checks: run spread-shellcheck too + - interfaces: update network-manager interface to allow + ObjectManager access from unconfined clients + - tests: move core and regression suites to snaps-state tool + - tests: moving interfaces tests to snaps-state tool + - gadget: preserve files when indicated by content change observer + - tests: moving smoke test suite and some tests from main suite to + snaps-state tool + - o/snapshotstate: pass set id to backend.Open, update tests + - asserts/snapasserts: introduce ValidationSets + - o/snapshotstate: improve allocation of new set IDs + - boot: look at the gadget for run mode bootloader when making the + system bootable + - cmd/snap: allow snap help vs --all to diverge purposefully + - usersession/userd: separate bus name ownership from defining + interfaces + - o/snapshotstate: set snapshot set id from its filename + - o/snapstate: move remove-related tests to snapstate_remove_test.go + - desktop/notification: switch ExpireTimeout to time.Duration + - desktop/notification: add unit tests + - snap: snap help output refresh + - tests/nested/manual/preseed: include a system-usernames snap when + preseeding + - tests: fix sudo-env test + - tests: fix nested core20 shellcheck bug + - tests/lib: move to new directory when restoring PWD, cleanup + unpacked unpacked snap directories + - desktop/notification: add bindings for FDO notifications + - dbustest: fix stale comment references + - many: move ManagedAssetsBootloader into TrustedAssetsBootloader, + drop former + - snap-repair: add uc20 support + - tests: print all the serial logs for the nested test + - o/snapstate/check_snap_test.go: mock osutil.Find{U,G}id to avoid + bug in test + - cmd/snap/auto-import: stop importing system user assertions from + initramfs mnts + - osutil/group.go: treat all non-nil errs from user.Lookup{Group,} + as Unknown* + - asserts: deserialize grouping only once in Pool.AddBatch if needed + - gadget: allow content observer to have opinions about a change + - tests: new snaps-state command - part1 + - o/assertstate: support refreshing any number of snap-declarations + - boot: use test helpers + - tests/core/snap-debug-bootvars: also check snap_mode + - many/apparmor: adjust rules for reading profile/ execing new + profiles for new kernel + - tests/core/snap-debug-bootvars: spread test for snap debug boot- + vars + - tests/lib/nested.sh: more little tweaks + - tests/nested/manual/grade-signed-above-testkeys-boot: enable kvm + - cmd/s-b/initramfs-mounts: use ConfigureTargetSystem for install, + recover modes + - overlord: explicitly set refresh-app-awareness in tests + - kernel: remove "edition" from kernel.yaml and add "update" + - spread: drop vendor from the packed project archive + - boot: fix debug bootloader variables dump on UC20 systems + - wrappers, systemd: allow empty root dir and conditionally do not + pass --root to systemctl + - tests/nested/manual: add test for grades above signed booting with + testkeys + - tests/nested: misc robustness fixes + - o/assertstate,asserts: use bulk refresh to refresh snap- + declarations + - tests/lib/prepare.sh: stop patching the uc20 initrd since it has + been updated now + - tests/nested/manual/refresh-revert-fundamentals: re-enable test + - update-pot: ignore .go files inside .git when running xgettext-go + - tests: disable part of the lxd test completely on 16.04. + - o/snapshotstate: tweak comment regarding snapshot filename + - o/snapstate: improve snapshot iteration + - bootloader: lk cleanups + - tests: update to support nested kvm without reboots on UC20 + - tests/nested/manual/preseed: disable system-key check for 20.04 + image + - spread.yaml: add ubuntu-20.10-64 to qemu + - store: handle v2 error when fetching assertions + - gadget: resolve device mapper devices for fallback device lookup + - tests/nested/cloud-init-many: simplify tests and unify + helpers/seed inputs + - tests: copy /usr/lib/snapd/info to correct directory + - check-pr-title.py * : allow "*" in the first part of the title + - many: typos and small test tweak + - tests/main/lxd: disable cgroup combination for 16.04 that is + failing a lot + - tests: make nested signing helpers less confusing + - tests: misc nested changes + - tests/nested/manual/refresh-revert-fundamentals: disable + temporarily + - tests/lib/cla_check: default to Python 3, tweaks, formatting + - tests/lib/cl_check.py: use python3 compatible code + * Thu Oct 08 2020 Michael Vogt - New upstream release 2.47.1 - o/configstate: create /etc/sysctl.d when applying early config diff -Nru snapd-2.47.1+20.10.1build1/packaging/opensuse/snapd.changes snapd-2.48+21.04/packaging/opensuse/snapd.changes --- snapd-2.47.1+20.10.1build1/packaging/opensuse/snapd.changes 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/packaging/opensuse/snapd.changes 2020-11-19 16:51:02.000000000 +0000 @@ -1,4 +1,9 @@ ------------------------------------------------------------------- +Thu, 19 Nov 2020 17:55:24 +0100 - mvo@ubuntu.com + +- Update to upstream release 2.48 + +------------------------------------------------------------------- Thu Oct 08 2020 09:30:44 +0200 - mvo@ubuntu.com - Update to upstream release 2.47.1 diff -Nru snapd-2.47.1+20.10.1build1/packaging/opensuse/snapd.spec snapd-2.48+21.04/packaging/opensuse/snapd.spec --- snapd-2.47.1+20.10.1build1/packaging/opensuse/snapd.spec 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/packaging/opensuse/snapd.spec 2020-11-19 16:51:02.000000000 +0000 @@ -83,7 +83,7 @@ Name: snapd -Version: 2.47.1 +Version: 2.48 Release: 0 Summary: Tools enabling systems to work with .snap files License: GPL-3.0 @@ -256,6 +256,10 @@ %make_build -f %{indigo_srcdir}/packaging/snapd.mk GOPATH=%{indigo_gopath}:$GOPATH all %check +for binary in snap-exec snap-update-ns snapctl; do + ldd $binary 2>&1 | grep 'not a dynamic executable' +done + %make_build -C %{indigo_srcdir}/cmd check # Use the common packaging helper for testing. %make_build -f %{indigo_srcdir}/packaging/snapd.mk GOPATH=%{indigo_gopath}:$GOPATH check @@ -317,7 +321,7 @@ %post %set_permissions %{_libexecdir}/snapd/snap-confine %if %{with apparmor} -%apparmor_reload /etc/apparmor.d/usr.lib.snapd.snap-confine +%apparmor_reload /etc/apparmor.d/%{apparmor_snapconfine_profile} %endif %service_add_post %{systemd_services_list} %systemd_user_post %{systemd_user_services_list} diff -Nru snapd-2.47.1+20.10.1build1/packaging/opensuse-15.0/snapd.changes snapd-2.48+21.04/packaging/opensuse-15.0/snapd.changes --- snapd-2.47.1+20.10.1build1/packaging/opensuse-15.0/snapd.changes 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/packaging/opensuse-15.0/snapd.changes 2020-11-19 16:51:02.000000000 +0000 @@ -1,4 +1,9 @@ ------------------------------------------------------------------- +Thu, 19 Nov 2020 17:55:24 +0100 - mvo@ubuntu.com + +- Update to upstream release 2.48 + +------------------------------------------------------------------- Thu Oct 08 2020 09:30:44 +0200 - mvo@ubuntu.com - Update to upstream release 2.47.1 diff -Nru snapd-2.47.1+20.10.1build1/packaging/opensuse-15.0/snapd.spec snapd-2.48+21.04/packaging/opensuse-15.0/snapd.spec --- snapd-2.47.1+20.10.1build1/packaging/opensuse-15.0/snapd.spec 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/packaging/opensuse-15.0/snapd.spec 2020-11-19 16:51:02.000000000 +0000 @@ -83,7 +83,7 @@ Name: snapd -Version: 2.47.1 +Version: 2.48 Release: 0 Summary: Tools enabling systems to work with .snap files License: GPL-3.0 @@ -256,6 +256,10 @@ %make_build -f %{indigo_srcdir}/packaging/snapd.mk GOPATH=%{indigo_gopath}:$GOPATH all %check +for binary in snap-exec snap-update-ns snapctl; do + ldd $binary 2>&1 | grep 'not a dynamic executable' +done + %make_build -C %{indigo_srcdir}/cmd check # Use the common packaging helper for testing. %make_build -f %{indigo_srcdir}/packaging/snapd.mk GOPATH=%{indigo_gopath}:$GOPATH check @@ -317,7 +321,7 @@ %post %set_permissions %{_libexecdir}/snapd/snap-confine %if %{with apparmor} -%apparmor_reload /etc/apparmor.d/usr.lib.snapd.snap-confine +%apparmor_reload /etc/apparmor.d/%{apparmor_snapconfine_profile} %endif %service_add_post %{systemd_services_list} %systemd_user_post %{systemd_user_services_list} diff -Nru snapd-2.47.1+20.10.1build1/packaging/opensuse-15.1/snapd.changes snapd-2.48+21.04/packaging/opensuse-15.1/snapd.changes --- snapd-2.47.1+20.10.1build1/packaging/opensuse-15.1/snapd.changes 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/packaging/opensuse-15.1/snapd.changes 2020-11-19 16:51:02.000000000 +0000 @@ -1,4 +1,9 @@ ------------------------------------------------------------------- +Thu, 19 Nov 2020 17:55:24 +0100 - mvo@ubuntu.com + +- Update to upstream release 2.48 + +------------------------------------------------------------------- Thu Oct 08 2020 09:30:44 +0200 - mvo@ubuntu.com - Update to upstream release 2.47.1 diff -Nru snapd-2.47.1+20.10.1build1/packaging/opensuse-15.1/snapd.spec snapd-2.48+21.04/packaging/opensuse-15.1/snapd.spec --- snapd-2.47.1+20.10.1build1/packaging/opensuse-15.1/snapd.spec 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/packaging/opensuse-15.1/snapd.spec 2020-11-19 16:51:02.000000000 +0000 @@ -83,7 +83,7 @@ Name: snapd -Version: 2.47.1 +Version: 2.48 Release: 0 Summary: Tools enabling systems to work with .snap files License: GPL-3.0 @@ -256,6 +256,10 @@ %make_build -f %{indigo_srcdir}/packaging/snapd.mk GOPATH=%{indigo_gopath}:$GOPATH all %check +for binary in snap-exec snap-update-ns snapctl; do + ldd $binary 2>&1 | grep 'not a dynamic executable' +done + %make_build -C %{indigo_srcdir}/cmd check # Use the common packaging helper for testing. %make_build -f %{indigo_srcdir}/packaging/snapd.mk GOPATH=%{indigo_gopath}:$GOPATH check @@ -317,7 +321,7 @@ %post %set_permissions %{_libexecdir}/snapd/snap-confine %if %{with apparmor} -%apparmor_reload /etc/apparmor.d/usr.lib.snapd.snap-confine +%apparmor_reload /etc/apparmor.d/%{apparmor_snapconfine_profile} %endif %service_add_post %{systemd_services_list} %systemd_user_post %{systemd_user_services_list} diff -Nru snapd-2.47.1+20.10.1build1/packaging/opensuse-15.2/snapd.changes snapd-2.48+21.04/packaging/opensuse-15.2/snapd.changes --- snapd-2.47.1+20.10.1build1/packaging/opensuse-15.2/snapd.changes 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/packaging/opensuse-15.2/snapd.changes 2020-11-19 16:51:02.000000000 +0000 @@ -1,4 +1,9 @@ ------------------------------------------------------------------- +Thu, 19 Nov 2020 17:55:24 +0100 - mvo@ubuntu.com + +- Update to upstream release 2.48 + +------------------------------------------------------------------- Thu Oct 08 2020 09:30:44 +0200 - mvo@ubuntu.com - Update to upstream release 2.47.1 diff -Nru snapd-2.47.1+20.10.1build1/packaging/opensuse-15.2/snapd.spec snapd-2.48+21.04/packaging/opensuse-15.2/snapd.spec --- snapd-2.47.1+20.10.1build1/packaging/opensuse-15.2/snapd.spec 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/packaging/opensuse-15.2/snapd.spec 2020-11-19 16:51:02.000000000 +0000 @@ -83,7 +83,7 @@ Name: snapd -Version: 2.47.1 +Version: 2.48 Release: 0 Summary: Tools enabling systems to work with .snap files License: GPL-3.0 @@ -256,6 +256,10 @@ %make_build -f %{indigo_srcdir}/packaging/snapd.mk GOPATH=%{indigo_gopath}:$GOPATH all %check +for binary in snap-exec snap-update-ns snapctl; do + ldd $binary 2>&1 | grep 'not a dynamic executable' +done + %make_build -C %{indigo_srcdir}/cmd check # Use the common packaging helper for testing. %make_build -f %{indigo_srcdir}/packaging/snapd.mk GOPATH=%{indigo_gopath}:$GOPATH check @@ -317,7 +321,7 @@ %post %set_permissions %{_libexecdir}/snapd/snap-confine %if %{with apparmor} -%apparmor_reload /etc/apparmor.d/usr.lib.snapd.snap-confine +%apparmor_reload /etc/apparmor.d/%{apparmor_snapconfine_profile} %endif %service_add_post %{systemd_services_list} %systemd_user_post %{systemd_user_services_list} diff -Nru snapd-2.47.1+20.10.1build1/packaging/opensuse-tumbleweed/snapd.changes snapd-2.48+21.04/packaging/opensuse-tumbleweed/snapd.changes --- snapd-2.47.1+20.10.1build1/packaging/opensuse-tumbleweed/snapd.changes 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/packaging/opensuse-tumbleweed/snapd.changes 2020-11-19 16:51:02.000000000 +0000 @@ -1,4 +1,9 @@ ------------------------------------------------------------------- +Thu, 19 Nov 2020 17:55:24 +0100 - mvo@ubuntu.com + +- Update to upstream release 2.48 + +------------------------------------------------------------------- Thu Oct 08 2020 09:30:44 +0200 - mvo@ubuntu.com - Update to upstream release 2.47.1 diff -Nru snapd-2.47.1+20.10.1build1/packaging/opensuse-tumbleweed/snapd.spec snapd-2.48+21.04/packaging/opensuse-tumbleweed/snapd.spec --- snapd-2.47.1+20.10.1build1/packaging/opensuse-tumbleweed/snapd.spec 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/packaging/opensuse-tumbleweed/snapd.spec 2020-11-19 16:51:02.000000000 +0000 @@ -83,7 +83,7 @@ Name: snapd -Version: 2.47.1 +Version: 2.48 Release: 0 Summary: Tools enabling systems to work with .snap files License: GPL-3.0 @@ -256,6 +256,10 @@ %make_build -f %{indigo_srcdir}/packaging/snapd.mk GOPATH=%{indigo_gopath}:$GOPATH all %check +for binary in snap-exec snap-update-ns snapctl; do + ldd $binary 2>&1 | grep 'not a dynamic executable' +done + %make_build -C %{indigo_srcdir}/cmd check # Use the common packaging helper for testing. %make_build -f %{indigo_srcdir}/packaging/snapd.mk GOPATH=%{indigo_gopath}:$GOPATH check @@ -317,7 +321,7 @@ %post %set_permissions %{_libexecdir}/snapd/snap-confine %if %{with apparmor} -%apparmor_reload /etc/apparmor.d/usr.lib.snapd.snap-confine +%apparmor_reload /etc/apparmor.d/%{apparmor_snapconfine_profile} %endif %service_add_post %{systemd_services_list} %systemd_user_post %{systemd_user_services_list} diff -Nru snapd-2.47.1+20.10.1build1/packaging/snapd.mk snapd-2.48+21.04/packaging/snapd.mk --- snapd-2.47.1+20.10.1build1/packaging/snapd.mk 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/packaging/snapd.mk 2020-11-19 16:51:02.000000000 +0000 @@ -65,7 +65,9 @@ # nearly-arbitrary mount namespace that does not contain anything we can depend # on (no standard library, for example). snap-update-ns snap-exec snapctl: - go build -buildmode=default -ldflags '-extldflags "-static"' $(import_path)/cmd/$@ + # Explicit request to use an external linker, otherwise extldflags may not be + # used + go build -buildmode=default -ldflags '-linkmode external -extldflags "-static"' $(import_path)/cmd/$@ # Snapd can be built with test keys. This is only used by the internal test # suite to add test assertions. Do not enable this in distribution packages. diff -Nru snapd-2.47.1+20.10.1build1/packaging/ubuntu-14.04/changelog snapd-2.48+21.04/packaging/ubuntu-14.04/changelog --- snapd-2.47.1+20.10.1build1/packaging/ubuntu-14.04/changelog 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/packaging/ubuntu-14.04/changelog 2020-11-19 16:51:02.000000000 +0000 @@ -1,3 +1,321 @@ +snapd (2.48~14.04) trusty; urgency=medium + + * New upstream release, LP: #1904098 + - osutil: add KernelCommandLineKeyValue + - devicestate: implement boot.HasFDESetupHook + - boot/makebootable.go: set snapd_recovery_mode=install at image- + build time + - bootloader: use ForGadget when installing boot config + - interfaces/raw_usb: allow read access to /proc/tty/drivers + - boot: add scaffolding for "fde-setup" hook support for sealing + - tests: fix basic20 test on arm devices + - seed: make a shared seed system label validation helper + - snap: add new "fde-setup" hooktype + - cmd/snap-bootstrap, secboot, tests: misc cleanups, add spread test + - secboot,cmd/snap-bootstrap: fix degraded mode cases with better + device handling + - boot,dirs,c/snap-bootstrap: avoid InstallHost* at the cost of some + messiness + - tests/nested/manual/refresh-revert-fundamentals: temporarily + disable secure boot + - snap-bootstrap,secboot: call BlockPCRProtectionPolicies in all + boot modes + - many: address degraded recover mode feedback, cleanups + - tests: Use systemd-run on tests part2 + - tests: set the opensuse tumbleweed system as manual in spread.yaml + - secboot: call BlockPCRProtectionPolicies even if the TPM is + disabled + - vendor: update to current secboot + - cmd/snap-bootstrap,o/devicestate: use a secret to pair data and + save + - spread.yaml: increase number of workers on 20.10 + - snap: add new `snap recovery --show-keys` option + - tests: minor test tweaks suggested in the review of 9607 + - snapd-generator: set standard snapfuse options when generating + units for containers + - tests: enable lxd test on ubuntu-core-20 and 16.04-32 + - interfaces: share /tmp/.X11-unix/ from host or provider + - tests: enable main lxd test on 20.10 + - cmd/s-b/initramfs-mounts: refactor recover mode to implement + degraded mode + - gadget/install: add progress logging + - packaging: keep secboot/encrypt_dummy.go in debian + - interfaces/udev: use distro specific path to snap-device-helper + - o/devistate: fix chaining of tasks related to regular snaps when + preseeding + - gadget, overlord/devicestate: validate that system supports + encrypted data before install + - interfaces/fwupd: enforce the confined fwupd to align Ubuntu Core + ESP layout + - many: add /v2/system-recovery-keys API and client + - secboot, many: return UnlockMethod from Unlock* methods for future + usage + - many: mv keys to ubuntu-boot, move model file, rename keyring + prefix for secboot + - tests: using systemd-run instead of manually create a systemd unit + - part 1 + - secboot, cmd/snap-bootstrap: enable or disable activation with + recovery key + - secboot: refactor Unlock...IfEncrypted to take keyfile + check + disks first + - secboot: add LockTPMSealedKeys() to lock access to keys + independently + - gadget: correct sfdisk arguments + - bootloader/assets/grub: adjust fwsetup menuentry label + - tests: new boot state tool + - spread: use the official image for Ubuntu 20.10, no longer an + unstable system + - tests/lib/nested: enable snapd logging to console for core18 + - osutil/disks: re-implement partition searching for disk w/ non- + adjacent parts + - tests: using the nested-state tool in nested tests + - many: seal a fallback object to the recovery boot chain + - gadget, gadget/install: move helpers to install package, refactor + unit tests + - dirs: add "gentoo" to altDirDistros + - update-pot: include file locations in translation template, and + extract strings from desktop files + - gadget/many: drop usage of gpt attr 59 for indicating creation of + partitions + - gadget/quantity: tweak test name + - snap: fix failing unittest for quantity.FormatDuration() + - gadget/quantity: introduce a new package that captures quantities + - o/devicestate,a/sysdb: make a backup of the device serial to save + - tests: fix rare interaction of tests.session and specific tests + - features: enable classic-preserves-xdg-runtime-dir + - tests/nested/core20/save: check the bind mount and size bump + - o/devicetate,dirs: keep device keys in ubuntu-save/save for UC20 + - tests: rename hasHooks to hasInterfaceHooks in the ifacestate + tests + - o/devicestate: unit test tweaks + - boot: store the TPM{PolicyAuthKey,LockoutAuth}File in ubuntu-save + - testutil, cmd/snap/version: fix misc little errors + - overlord/devicestate: bind mount ubuntu-save under + /var/lib/snapd/save on startup + - gadget/internal: tune ext4 setting for smaller filesystems + - tests/nested/core20/save: a test that verifies ubuntu-save is + present and set up + - tests: update google sru backend to support groovy + - o/ifacestate: handle interface hooks when preseeding + - tests: re-enable the apt hooks test + - interfaces,snap: use correct type: {os,snapd} for test data + - secboot: set metadata and keyslots sizes when formatting LUKS2 + volumes + - tests: improve uc20-create-partitions-reinstall test + - client, daemon, cmd/snap: cleanups from #9489 + more unit tests + - cmd/snap-bootstrap: mount ubuntu-save during boot if present + - secboot: fix doc comment on helper for unlocking volume with key + - tests: add spread test for refreshing from an old snapd and core18 + - o/snapstate: generate snapd snap wrappers again after restart on + refresh + - secboot: version bump, unlock volume with key + - tests/snap-advise-command: re-enable test + - cmd/snap, snapmgr, tests: cleanups after #9418 + - interfaces: deny connected x11 plugs access to ICE + - daemon,client: write and read a maintenance.json file for when + snapd is shut down + - many: update to secboot v1 (part 1) + - osutil/disks/mockdisk: panic if same mountpoint shows up again + with diff opts + - tests/nested/core20/gadget,kernel-reseal: add sanity checks to the + reseal tests + - many: implement snap routine console-conf-start for synchronizing + auto-refreshes + - dirs, boot: add ubuntu-save directories and related locations + - usersession: fix typo in test name + - overlord/snapstate: refactor ihibitRefresh + - overlord/snapstate: stop warning about inhibited refreshes + - cmd/snap: do not hardcode snapshot age value + - overlord,usersession: initial notifications of pending refreshes + - tests: add a unit test for UpdateMany where a single snap fails + - o/snapstate/catalogrefresh.go: don't refresh catalog in install + mode uc20 + - tests: also check snapst.Current in undo-unlink tests + - tests: new nested tool + - o/snapstate: implement undo handler for unlink-snap + - tests: clean systems.sh helper and migrate last set of tests + - tests: moving the lib section from systems.sh helper to os.query + tool + - tests/uc20-create-partitions: don't check for grub.cfg + - packaging: make sure that static binaries are indeed static, fix + openSUSE + - many: have install return encryption keys for data and save, + improve tests + - overlord: add link participant for linkage transitions + - tests: lxd smoke test + - tests: add tests for fsck; cmd/s-b/initramfs-mounts: fsck ubuntu- + seed too + - tests: moving main suite from systems.sh to os.query tool + - tests: moving the core test suite from systems.sh to os.query tool + - cmd/snap-confine: mask host's apparmor config + - o/snapstate: move setting updated SnapState after error paths + - tests: add value to INSTANCE_KEY/regular + - spread, tests: tweaks for openSUSE + - cmd/snap-confine: update path to snap-device-helper in AppArmor + profile + - tests: new os.query tool + - overlord/snapshotstate/backend: specify tar format for snapshots + - tests/nested/manual/minimal-smoke: use 384MB of RAM for nested + UC20 + - client,daemon,snap: auto-import does not error on managed devices + - interfaces: PTP hardware clock interface + - tests: use tests.backup tool + - many: verify that unit tests work with nosecboot tag and without + secboot package + - wrappers: do not error out on read-only /etc/dbus-1/session.d + filesystem on core18 + - snapshots: import of a snapshot set + - tests: more output for sbuild test + - o/snapstate: re-order remove tasks for individual snap revisions + to remove current last + - boot: skip some unit tests when running as root + - o/assertstate: introduce + ValidationTrackingKey/ValidationSetTracking and basic methods + - many: allow ignoring running apps for specific request + - tests: allow the searching test to fail under load + - overlord/snapstate: inhibit startup while unlinked + - seed/seedwriter/writer.go: check DevModeConfinement for dangerous + features + - tests/main/sudo-env: snap bin is available on Fedora + - boot, overlord/devicestate: list trusted and managed assets + upfront + - gadget, gadget/install: support for ubuntu-save, create one during + install if needed + - spread-shellcheck: temporary workaround for deadlock, drop + unnecessary test + - snap: support different exit-code in the snap command + - logger: use strutil.KernelCommandLineSplit in + debugEnabledOnKernelCmdline + - logger: fix snapd.debug=1 parsing + - overlord: increase refresh postpone limit to 14 days + - spread-shellcheck: use single thread pool executor + - gadget/install,secboot: add debug messages + - spread-shellcheck: speed up spread-shellcheck even more + - spread-shellcheck: process paths from arguments in parallel + - tests: tweak error from tests.cleanup + - spread: remove workaround for openSUSE go issue + - o/configstate: create /etc/sysctl.d when applying early config + defaults + - tests: new tests.backup tool + - tests: add tests.cleanup pop sub-command + - tests: migration of the main suite to snaps-state tool part 6 + - tests: fix journal-state test + - cmd/snap-bootstrap/initramfs-mounts: split off new helper for misc + recover files + - cmd/snap-bootstrap/initramfs-mounts: also copy /etc/machine-id for + same IP addr + - packaging/{ubuntu,debian}: add liblzo2-dev as a dependency for + building snapd + - boot, gadget, bootloader: observer preserves managed bootloader + configs + - tests/nested/manual: add uc20 grade signed cloud-init test + - o/snapstate/autorefresh.go: eliminate race when launching + autorefresh + - daemon,snapshotstate: do not return "size" from Import() + - daemon: limit reading from snapshot import to Content-Length + - many: set/expect Content-Length header when importing snapshots + - github: switch from ::set-env command to environment file + - tests: migration of the main suite to snaps-state tool part 5 + - client: cleanup the Client.raw* and Client.do* method families + - tests: moving main suite to snaps-state tool part 4 + - client,daemon,snap: use constant for snapshot content-type + - many: fix typos and repeated "the" + - secboot: fix tpm connection leak when it's not enabled + - many: scaffolding for snapshots import API + - run-checks: run spread-shellcheck too + - interfaces: update network-manager interface to allow + ObjectManager access from unconfined clients + - tests: move core and regression suites to snaps-state tool + - tests: moving interfaces tests to snaps-state tool + - gadget: preserve files when indicated by content change observer + - tests: moving smoke test suite and some tests from main suite to + snaps-state tool + - o/snapshotstate: pass set id to backend.Open, update tests + - asserts/snapasserts: introduce ValidationSets + - o/snapshotstate: improve allocation of new set IDs + - boot: look at the gadget for run mode bootloader when making the + system bootable + - cmd/snap: allow snap help vs --all to diverge purposefully + - usersession/userd: separate bus name ownership from defining + interfaces + - o/snapshotstate: set snapshot set id from its filename + - o/snapstate: move remove-related tests to snapstate_remove_test.go + - desktop/notification: switch ExpireTimeout to time.Duration + - desktop/notification: add unit tests + - snap: snap help output refresh + - tests/nested/manual/preseed: include a system-usernames snap when + preseeding + - tests: fix sudo-env test + - tests: fix nested core20 shellcheck bug + - tests/lib: move to new directory when restoring PWD, cleanup + unpacked unpacked snap directories + - desktop/notification: add bindings for FDO notifications + - dbustest: fix stale comment references + - many: move ManagedAssetsBootloader into TrustedAssetsBootloader, + drop former + - snap-repair: add uc20 support + - tests: print all the serial logs for the nested test + - o/snapstate/check_snap_test.go: mock osutil.Find{U,G}id to avoid + bug in test + - cmd/snap/auto-import: stop importing system user assertions from + initramfs mnts + - osutil/group.go: treat all non-nil errs from user.Lookup{Group,} + as Unknown* + - asserts: deserialize grouping only once in Pool.AddBatch if needed + - gadget: allow content observer to have opinions about a change + - tests: new snaps-state command - part1 + - o/assertstate: support refreshing any number of snap-declarations + - boot: use test helpers + - tests/core/snap-debug-bootvars: also check snap_mode + - many/apparmor: adjust rules for reading profile/ execing new + profiles for new kernel + - tests/core/snap-debug-bootvars: spread test for snap debug boot- + vars + - tests/lib/nested.sh: more little tweaks + - tests/nested/manual/grade-signed-above-testkeys-boot: enable kvm + - cmd/s-b/initramfs-mounts: use ConfigureTargetSystem for install, + recover modes + - overlord: explicitly set refresh-app-awareness in tests + - kernel: remove "edition" from kernel.yaml and add "update" + - spread: drop vendor from the packed project archive + - boot: fix debug bootloader variables dump on UC20 systems + - wrappers, systemd: allow empty root dir and conditionally do not + pass --root to systemctl + - tests/nested/manual: add test for grades above signed booting with + testkeys + - tests/nested: misc robustness fixes + - o/assertstate,asserts: use bulk refresh to refresh snap- + declarations + - tests/lib/prepare.sh: stop patching the uc20 initrd since it has + been updated now + - tests/nested/manual/refresh-revert-fundamentals: re-enable test + - update-pot: ignore .go files inside .git when running xgettext-go + - tests: disable part of the lxd test completely on 16.04. + - o/snapshotstate: tweak comment regarding snapshot filename + - o/snapstate: improve snapshot iteration + - bootloader: lk cleanups + - tests: update to support nested kvm without reboots on UC20 + - tests/nested/manual/preseed: disable system-key check for 20.04 + image + - spread.yaml: add ubuntu-20.10-64 to qemu + - store: handle v2 error when fetching assertions + - gadget: resolve device mapper devices for fallback device lookup + - tests/nested/cloud-init-many: simplify tests and unify + helpers/seed inputs + - tests: copy /usr/lib/snapd/info to correct directory + - check-pr-title.py * : allow "*" in the first part of the title + - many: typos and small test tweak + - tests/main/lxd: disable cgroup combination for 16.04 that is + failing a lot + - tests: make nested signing helpers less confusing + - tests: misc nested changes + - tests/nested/manual/refresh-revert-fundamentals: disable + temporarily + - tests/lib/cla_check: default to Python 3, tweaks, formatting + - tests/lib/cl_check.py: use python3 compatible code + + -- Michael Vogt Thu, 19 Nov 2020 17:51:02 +0100 + snapd (2.47.1~14.04) trusty; urgency=medium * New upstream release, LP: #1895929 diff -Nru snapd-2.47.1+20.10.1build1/packaging/ubuntu-16.04/changelog snapd-2.48+21.04/packaging/ubuntu-16.04/changelog --- snapd-2.47.1+20.10.1build1/packaging/ubuntu-16.04/changelog 2020-11-11 22:25:55.000000000 +0000 +++ snapd-2.48+21.04/packaging/ubuntu-16.04/changelog 2020-11-19 16:51:02.000000000 +0000 @@ -1,17 +1,322 @@ -snapd (2.47.1+20.10.1build1) hirsute; urgency=medium +snapd (2.48+21.04) hirsute; urgency=medium - * No-change rebuild using new golang - - -- Steve Langasek Wed, 11 Nov 2020 22:25:55 +0000 - -snapd (2.47.1+20.10.1) groovy; urgency=medium - - * cherry-pick PR#9516 to fix docker and multipass snaps - with apparmor3 (LP: #1898038) + * New upstream release, LP: #1904098 + - osutil: add KernelCommandLineKeyValue + - devicestate: implement boot.HasFDESetupHook + - boot/makebootable.go: set snapd_recovery_mode=install at image- + build time + - bootloader: use ForGadget when installing boot config + - interfaces/raw_usb: allow read access to /proc/tty/drivers + - boot: add scaffolding for "fde-setup" hook support for sealing + - tests: fix basic20 test on arm devices + - seed: make a shared seed system label validation helper + - snap: add new "fde-setup" hooktype + - cmd/snap-bootstrap, secboot, tests: misc cleanups, add spread test + - secboot,cmd/snap-bootstrap: fix degraded mode cases with better + device handling + - boot,dirs,c/snap-bootstrap: avoid InstallHost* at the cost of some + messiness + - tests/nested/manual/refresh-revert-fundamentals: temporarily + disable secure boot + - snap-bootstrap,secboot: call BlockPCRProtectionPolicies in all + boot modes + - many: address degraded recover mode feedback, cleanups + - tests: Use systemd-run on tests part2 + - tests: set the opensuse tumbleweed system as manual in spread.yaml + - secboot: call BlockPCRProtectionPolicies even if the TPM is + disabled + - vendor: update to current secboot + - cmd/snap-bootstrap,o/devicestate: use a secret to pair data and + save + - spread.yaml: increase number of workers on 20.10 + - snap: add new `snap recovery --show-keys` option + - tests: minor test tweaks suggested in the review of 9607 + - snapd-generator: set standard snapfuse options when generating + units for containers + - tests: enable lxd test on ubuntu-core-20 and 16.04-32 + - interfaces: share /tmp/.X11-unix/ from host or provider + - tests: enable main lxd test on 20.10 + - cmd/s-b/initramfs-mounts: refactor recover mode to implement + degraded mode + - gadget/install: add progress logging + - packaging: keep secboot/encrypt_dummy.go in debian + - interfaces/udev: use distro specific path to snap-device-helper + - o/devistate: fix chaining of tasks related to regular snaps when + preseeding + - gadget, overlord/devicestate: validate that system supports + encrypted data before install + - interfaces/fwupd: enforce the confined fwupd to align Ubuntu Core + ESP layout + - many: add /v2/system-recovery-keys API and client + - secboot, many: return UnlockMethod from Unlock* methods for future + usage + - many: mv keys to ubuntu-boot, move model file, rename keyring + prefix for secboot + - tests: using systemd-run instead of manually create a systemd unit + - part 1 + - secboot, cmd/snap-bootstrap: enable or disable activation with + recovery key + - secboot: refactor Unlock...IfEncrypted to take keyfile + check + disks first + - secboot: add LockTPMSealedKeys() to lock access to keys + independently + - gadget: correct sfdisk arguments + - bootloader/assets/grub: adjust fwsetup menuentry label + - tests: new boot state tool + - spread: use the official image for Ubuntu 20.10, no longer an + unstable system + - tests/lib/nested: enable snapd logging to console for core18 + - osutil/disks: re-implement partition searching for disk w/ non- + adjacent parts + - tests: using the nested-state tool in nested tests + - many: seal a fallback object to the recovery boot chain + - gadget, gadget/install: move helpers to install package, refactor + unit tests + - dirs: add "gentoo" to altDirDistros + - update-pot: include file locations in translation template, and + extract strings from desktop files + - gadget/many: drop usage of gpt attr 59 for indicating creation of + partitions + - gadget/quantity: tweak test name + - snap: fix failing unittest for quantity.FormatDuration() + - gadget/quantity: introduce a new package that captures quantities + - o/devicestate,a/sysdb: make a backup of the device serial to save + - tests: fix rare interaction of tests.session and specific tests + - features: enable classic-preserves-xdg-runtime-dir + - tests/nested/core20/save: check the bind mount and size bump + - o/devicetate,dirs: keep device keys in ubuntu-save/save for UC20 + - tests: rename hasHooks to hasInterfaceHooks in the ifacestate + tests + - o/devicestate: unit test tweaks + - boot: store the TPM{PolicyAuthKey,LockoutAuth}File in ubuntu-save + - testutil, cmd/snap/version: fix misc little errors + - overlord/devicestate: bind mount ubuntu-save under + /var/lib/snapd/save on startup + - gadget/internal: tune ext4 setting for smaller filesystems + - tests/nested/core20/save: a test that verifies ubuntu-save is + present and set up + - tests: update google sru backend to support groovy + - o/ifacestate: handle interface hooks when preseeding + - tests: re-enable the apt hooks test + - interfaces,snap: use correct type: {os,snapd} for test data + - secboot: set metadata and keyslots sizes when formatting LUKS2 + volumes + - tests: improve uc20-create-partitions-reinstall test + - client, daemon, cmd/snap: cleanups from #9489 + more unit tests + - cmd/snap-bootstrap: mount ubuntu-save during boot if present + - secboot: fix doc comment on helper for unlocking volume with key + - tests: add spread test for refreshing from an old snapd and core18 + - o/snapstate: generate snapd snap wrappers again after restart on + refresh + - secboot: version bump, unlock volume with key + - tests/snap-advise-command: re-enable test + - cmd/snap, snapmgr, tests: cleanups after #9418 + - interfaces: deny connected x11 plugs access to ICE + - daemon,client: write and read a maintenance.json file for when + snapd is shut down + - many: update to secboot v1 (part 1) + - osutil/disks/mockdisk: panic if same mountpoint shows up again + with diff opts + - tests/nested/core20/gadget,kernel-reseal: add sanity checks to the + reseal tests + - many: implement snap routine console-conf-start for synchronizing + auto-refreshes + - dirs, boot: add ubuntu-save directories and related locations + - usersession: fix typo in test name + - overlord/snapstate: refactor ihibitRefresh + - overlord/snapstate: stop warning about inhibited refreshes + - cmd/snap: do not hardcode snapshot age value + - overlord,usersession: initial notifications of pending refreshes + - tests: add a unit test for UpdateMany where a single snap fails + - o/snapstate/catalogrefresh.go: don't refresh catalog in install + mode uc20 + - tests: also check snapst.Current in undo-unlink tests + - tests: new nested tool + - o/snapstate: implement undo handler for unlink-snap + - tests: clean systems.sh helper and migrate last set of tests + - tests: moving the lib section from systems.sh helper to os.query + tool + - tests/uc20-create-partitions: don't check for grub.cfg + - packaging: make sure that static binaries are indeed static, fix + openSUSE + - many: have install return encryption keys for data and save, + improve tests + - overlord: add link participant for linkage transitions + - tests: lxd smoke test + - tests: add tests for fsck; cmd/s-b/initramfs-mounts: fsck ubuntu- + seed too + - tests: moving main suite from systems.sh to os.query tool + - tests: moving the core test suite from systems.sh to os.query tool + - cmd/snap-confine: mask host's apparmor config + - o/snapstate: move setting updated SnapState after error paths + - tests: add value to INSTANCE_KEY/regular + - spread, tests: tweaks for openSUSE + - cmd/snap-confine: update path to snap-device-helper in AppArmor + profile + - tests: new os.query tool + - overlord/snapshotstate/backend: specify tar format for snapshots + - tests/nested/manual/minimal-smoke: use 384MB of RAM for nested + UC20 + - client,daemon,snap: auto-import does not error on managed devices + - interfaces: PTP hardware clock interface + - tests: use tests.backup tool + - many: verify that unit tests work with nosecboot tag and without + secboot package + - wrappers: do not error out on read-only /etc/dbus-1/session.d + filesystem on core18 + - snapshots: import of a snapshot set + - tests: more output for sbuild test + - o/snapstate: re-order remove tasks for individual snap revisions + to remove current last + - boot: skip some unit tests when running as root + - o/assertstate: introduce + ValidationTrackingKey/ValidationSetTracking and basic methods + - many: allow ignoring running apps for specific request + - tests: allow the searching test to fail under load + - overlord/snapstate: inhibit startup while unlinked + - seed/seedwriter/writer.go: check DevModeConfinement for dangerous + features + - tests/main/sudo-env: snap bin is available on Fedora + - boot, overlord/devicestate: list trusted and managed assets + upfront + - gadget, gadget/install: support for ubuntu-save, create one during + install if needed + - spread-shellcheck: temporary workaround for deadlock, drop + unnecessary test + - snap: support different exit-code in the snap command + - logger: use strutil.KernelCommandLineSplit in + debugEnabledOnKernelCmdline + - logger: fix snapd.debug=1 parsing + - overlord: increase refresh postpone limit to 14 days + - spread-shellcheck: use single thread pool executor + - gadget/install,secboot: add debug messages + - spread-shellcheck: speed up spread-shellcheck even more + - spread-shellcheck: process paths from arguments in parallel + - tests: tweak error from tests.cleanup + - spread: remove workaround for openSUSE go issue + - o/configstate: create /etc/sysctl.d when applying early config + defaults + - tests: new tests.backup tool + - tests: add tests.cleanup pop sub-command + - tests: migration of the main suite to snaps-state tool part 6 + - tests: fix journal-state test + - cmd/snap-bootstrap/initramfs-mounts: split off new helper for misc + recover files + - cmd/snap-bootstrap/initramfs-mounts: also copy /etc/machine-id for + same IP addr + - packaging/{ubuntu,debian}: add liblzo2-dev as a dependency for + building snapd + - boot, gadget, bootloader: observer preserves managed bootloader + configs + - tests/nested/manual: add uc20 grade signed cloud-init test + - o/snapstate/autorefresh.go: eliminate race when launching + autorefresh + - daemon,snapshotstate: do not return "size" from Import() + - daemon: limit reading from snapshot import to Content-Length + - many: set/expect Content-Length header when importing snapshots + - github: switch from ::set-env command to environment file + - tests: migration of the main suite to snaps-state tool part 5 + - client: cleanup the Client.raw* and Client.do* method families + - tests: moving main suite to snaps-state tool part 4 + - client,daemon,snap: use constant for snapshot content-type + - many: fix typos and repeated "the" + - secboot: fix tpm connection leak when it's not enabled + - many: scaffolding for snapshots import API + - run-checks: run spread-shellcheck too + - interfaces: update network-manager interface to allow + ObjectManager access from unconfined clients + - tests: move core and regression suites to snaps-state tool + - tests: moving interfaces tests to snaps-state tool + - gadget: preserve files when indicated by content change observer + - tests: moving smoke test suite and some tests from main suite to + snaps-state tool + - o/snapshotstate: pass set id to backend.Open, update tests + - asserts/snapasserts: introduce ValidationSets + - o/snapshotstate: improve allocation of new set IDs + - boot: look at the gadget for run mode bootloader when making the + system bootable + - cmd/snap: allow snap help vs --all to diverge purposefully + - usersession/userd: separate bus name ownership from defining + interfaces + - o/snapshotstate: set snapshot set id from its filename + - o/snapstate: move remove-related tests to snapstate_remove_test.go + - desktop/notification: switch ExpireTimeout to time.Duration + - desktop/notification: add unit tests + - snap: snap help output refresh + - tests/nested/manual/preseed: include a system-usernames snap when + preseeding + - tests: fix sudo-env test + - tests: fix nested core20 shellcheck bug + - tests/lib: move to new directory when restoring PWD, cleanup + unpacked unpacked snap directories + - desktop/notification: add bindings for FDO notifications + - dbustest: fix stale comment references + - many: move ManagedAssetsBootloader into TrustedAssetsBootloader, + drop former + - snap-repair: add uc20 support + - tests: print all the serial logs for the nested test + - o/snapstate/check_snap_test.go: mock osutil.Find{U,G}id to avoid + bug in test + - cmd/snap/auto-import: stop importing system user assertions from + initramfs mnts + - osutil/group.go: treat all non-nil errs from user.Lookup{Group,} + as Unknown* + - asserts: deserialize grouping only once in Pool.AddBatch if needed + - gadget: allow content observer to have opinions about a change + - tests: new snaps-state command - part1 + - o/assertstate: support refreshing any number of snap-declarations + - boot: use test helpers + - tests/core/snap-debug-bootvars: also check snap_mode + - many/apparmor: adjust rules for reading profile/ execing new + profiles for new kernel + - tests/core/snap-debug-bootvars: spread test for snap debug boot- + vars + - tests/lib/nested.sh: more little tweaks + - tests/nested/manual/grade-signed-above-testkeys-boot: enable kvm + - cmd/s-b/initramfs-mounts: use ConfigureTargetSystem for install, + recover modes + - overlord: explicitly set refresh-app-awareness in tests + - kernel: remove "edition" from kernel.yaml and add "update" + - spread: drop vendor from the packed project archive + - boot: fix debug bootloader variables dump on UC20 systems + - wrappers, systemd: allow empty root dir and conditionally do not + pass --root to systemctl + - tests/nested/manual: add test for grades above signed booting with + testkeys + - tests/nested: misc robustness fixes + - o/assertstate,asserts: use bulk refresh to refresh snap- + declarations + - tests/lib/prepare.sh: stop patching the uc20 initrd since it has + been updated now + - tests/nested/manual/refresh-revert-fundamentals: re-enable test + - update-pot: ignore .go files inside .git when running xgettext-go + - tests: disable part of the lxd test completely on 16.04. + - o/snapshotstate: tweak comment regarding snapshot filename + - o/snapstate: improve snapshot iteration + - bootloader: lk cleanups + - tests: update to support nested kvm without reboots on UC20 + - tests/nested/manual/preseed: disable system-key check for 20.04 + image + - spread.yaml: add ubuntu-20.10-64 to qemu + - store: handle v2 error when fetching assertions + - gadget: resolve device mapper devices for fallback device lookup + - tests/nested/cloud-init-many: simplify tests and unify + helpers/seed inputs + - tests: copy /usr/lib/snapd/info to correct directory + - check-pr-title.py * : allow "*" in the first part of the title + - many: typos and small test tweak + - tests/main/lxd: disable cgroup combination for 16.04 that is + failing a lot + - tests: make nested signing helpers less confusing + - tests: misc nested changes + - tests/nested/manual/refresh-revert-fundamentals: disable + temporarily + - tests/lib/cla_check: default to Python 3, tweaks, formatting + - tests/lib/cl_check.py: use python3 compatible code - -- Michael Vogt Mon, 19 Oct 2020 20:24:02 +0200 + -- Michael Vogt Thu, 19 Nov 2020 17:51:02 +0100 -snapd (2.47.1+20.10) groovy; urgency=medium +snapd (2.47.1) xenial; urgency=medium * New upstream release, LP: #1895929 - o/configstate: create /etc/sysctl.d when applying early config @@ -4517,7 +4822,7 @@ - logger: try to not have double dates - debian: use deb-systemd-invoke instead of systemctl directly - tests: run all main tests on core18 - - many: finish sharing a single TaskRunner with all the the managers + - many: finish sharing a single TaskRunner with all the managers - interfaces/repo: added AllHotplugInterfaces helper - snapstate: ensure kernel-track is honored on switch/refresh - overlord/ifacestate: support implicit slots on snapd diff -Nru snapd-2.47.1+20.10.1build1/packaging/ubuntu-16.04/copyright snapd-2.48+21.04/packaging/ubuntu-16.04/copyright --- snapd-2.47.1+20.10.1build1/packaging/ubuntu-16.04/copyright 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/packaging/ubuntu-16.04/copyright 2020-11-19 16:51:02.000000000 +0000 @@ -6,7 +6,7 @@ Copyright: Copyright (C) 2014,2015 Canonical, Ltd. License: GPL-3 This program is free software: you can redistribute it and/or modify it - under the terms of the the GNU General Public License version 3, as + 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 diff -Nru snapd-2.47.1+20.10.1build1/run-checks snapd-2.48+21.04/run-checks --- snapd-2.47.1+20.10.1build1/run-checks 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/run-checks 2020-11-19 16:51:02.000000000 +0000 @@ -170,6 +170,7 @@ done if [ -n "$fmt" ]; then echo "Formatting wrong in following files:" + # shellcheck disable=SC2001 echo "$fmt" | sed -e 's/\\n/\n/g' exit 1 fi @@ -225,6 +226,8 @@ exit 1 fi unset regexp + # also run spread-shellcheck + ./spread-shellcheck spread.yaml tests fi echo "Checking spelling errors" @@ -290,14 +293,21 @@ command -v go go version + tags= + if [ -n "${GO_BUILD_TAGS-}" ]; then + echo "Using build tags: $GO_BUILD_TAGS" + tags="-tags $GO_BUILD_TAGS" + fi + echo Building - go build -v github.com/snapcore/snapd/... + # shellcheck disable=SC2086 + go build -v $tags github.com/snapcore/snapd/... # tests echo Running tests from "$PWD" if [ "$short" = 1 ]; then - # shellcheck disable=SC2046 - GOTRACEBACK=1 $goctest -short -timeout 5m $(go list ./... | grep -v '/vendor/' ) + # shellcheck disable=SC2046,SC2086 + GOTRACEBACK=1 $goctest $tags -short -timeout 5m $(go list ./... | grep -v '/vendor/' ) else # Prepare the coverage output profile. rm -rf .coverage @@ -305,12 +315,14 @@ echo "mode: $COVERMODE" > .coverage/coverage.out if dpkg --compare-versions "$(go version | awk '$3 ~ /^go[0-9]/ {print substr($3, 3)}')" ge 1.10; then - # shellcheck disable=SC2046 - GOTRACEBACK=1 $goctest -timeout 5m -coverprofile=.coverage/coverage.out -covermode="$COVERMODE" $(go list ./... | grep -v '/vendor/' ) + # shellcheck disable=SC2046,SC2086 + GOTRACEBACK=1 $goctest $tags -timeout 5m -coverprofile=.coverage/coverage.out -covermode="$COVERMODE" $(go list ./... | grep -v '/vendor/' ) else for pkg in $(go list ./... | grep -v '/vendor/' ); do - GOTRACEBACK=1 go test -timeout 5m -i "$pkg" - GOTRACEBACK=1 $goctest -timeout 5m -coverprofile=.coverage/profile.out -covermode="$COVERMODE" "$pkg" + # shellcheck disable=SC2086 + GOTRACEBACK=1 go test $tags -timeout 5m -i "$pkg" + # shellcheck disable=SC2086 + GOTRACEBACK=1 $goctest $tags -timeout 5m -coverprofile=.coverage/profile.out -covermode="$COVERMODE" "$pkg" append_coverage .coverage/profile.out done fi @@ -395,6 +407,10 @@ exit 1 fi +if [ -n "${SKIP_DIRTY_CHECK-}" ]; then + exit 0 +fi + if git describe --always --dirty | grep -q dirty; then echo "Build tree is dirty" git diff diff -Nru snapd-2.47.1+20.10.1build1/secboot/encrypt_dummy.go snapd-2.48+21.04/secboot/encrypt_dummy.go --- snapd-2.47.1+20.10.1build1/secboot/encrypt_dummy.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48+21.04/secboot/encrypt_dummy.go 2020-11-19 16:51:02.000000000 +0000 @@ -0,0 +1,25 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- +// +build nosecboot + +/* + * 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 secboot + +func (k RecoveryKey) String() string { + return "not-implemented" +} diff -Nru snapd-2.47.1+20.10.1build1/secboot/encrypt.go snapd-2.48+21.04/secboot/encrypt.go --- snapd-2.47.1+20.10.1build1/secboot/encrypt.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/secboot/encrypt.go 2020-11-19 16:51:02.000000000 +0000 @@ -21,12 +21,23 @@ import ( "crypto/rand" + "fmt" + "io" + "os" + "path/filepath" + + "github.com/snapcore/snapd/osutil" ) const ( // The encryption key size is set so it has the same entropy as the derived // key. encryptionKeySize = 64 + + // XXX: needs to be in sync with + // github.com/snapcore/secboot/crypto.go:"type RecoveryKey" + // Size of the recovery key. + recoveryKeySize = 16 ) // EncryptionKey is the key used to encrypt the data partition. @@ -39,3 +50,52 @@ // On return, n == len(b) if and only if err == nil return key, err } + +// Save writes the key in the location specified by filename. +func (key EncryptionKey) Save(filename string) error { + if err := os.MkdirAll(filepath.Dir(filename), 0755); err != nil { + return err + } + return osutil.AtomicWriteFile(filename, key[:], 0600, 0) +} + +// RecoveryKey is a key used to unlock the encrypted partition when +// the encryption key can't be used, for example when unseal fails. +type RecoveryKey [recoveryKeySize]byte + +func NewRecoveryKey() (RecoveryKey, error) { + var key RecoveryKey + // rand.Read() is protected against short reads + _, err := rand.Read(key[:]) + // On return, n == len(b) if and only if err == nil + return key, err +} + +// Save writes the recovery key in the location specified by filename. +func (key RecoveryKey) Save(filename string) error { + if err := os.MkdirAll(filepath.Dir(filename), 0755); err != nil { + return err + } + return osutil.AtomicWriteFile(filename, key[:], 0600, 0) +} + +func RecoveryKeyFromFile(recoveryKeyFile string) (*RecoveryKey, error) { + f, err := os.Open(recoveryKeyFile) + if err != nil { + return nil, fmt.Errorf("cannot open recovery key: %v", err) + } + defer f.Close() + st, err := f.Stat() + if err != nil { + return nil, fmt.Errorf("cannot stat recovery key: %v", err) + } + if st.Size() != int64(len(RecoveryKey{})) { + return nil, fmt.Errorf("cannot read recovery key: unexpected size %v for the recovery key file %s", st.Size(), recoveryKeyFile) + } + + var rkey RecoveryKey + if _, err := io.ReadFull(f, rkey[:]); err != nil { + return nil, fmt.Errorf("cannot read recovery key: %v", err) + } + return &rkey, nil +} diff -Nru snapd-2.47.1+20.10.1build1/secboot/encrypt_test.go snapd-2.48+21.04/secboot/encrypt_test.go --- snapd-2.47.1+20.10.1build1/secboot/encrypt_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/secboot/encrypt_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -1,4 +1,5 @@ // -*- Mode: Go; indent-tabs-mode: t -*- +// +build !nosecboot /* * Copyright (C) 2019-2020 Canonical Ltd @@ -20,27 +21,63 @@ package secboot_test import ( - "io/ioutil" "os" + "path/filepath" . "gopkg.in/check.v1" "github.com/snapcore/snapd/secboot" + "github.com/snapcore/snapd/testutil" ) -type encryptSuite struct{} +type encryptSuite struct { + dir string +} var _ = Suite(&encryptSuite{}) +func (s *encryptSuite) SetUpTest(c *C) { + s.dir = c.MkDir() +} + func (s *encryptSuite) TestRecoveryKeySave(c *C) { + kf := filepath.Join(s.dir, "test-key") + kfNested := filepath.Join(s.dir, "deeply/nested/test-key") + rkey := secboot.RecoveryKey{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 255} - err := rkey.Save("test-key") + err := rkey.Save(kf) c.Assert(err, IsNil) - fileInfo, err := os.Stat("test-key") + c.Assert(kf, testutil.FileEquals, rkey[:]) + + fileInfo, err := os.Stat(kf) c.Assert(err, IsNil) c.Assert(fileInfo.Mode(), Equals, os.FileMode(0600)) - data, err := ioutil.ReadFile("test-key") + + err = rkey.Save(kfNested) + c.Assert(err, IsNil) + c.Assert(kfNested, testutil.FileEquals, rkey[:]) + di, err := os.Stat(filepath.Dir(kfNested)) + c.Assert(err, IsNil) + c.Assert(di.Mode().Perm(), Equals, os.FileMode(0755)) +} + +func (s *encryptSuite) TestEncryptionKeySave(c *C) { + kf := filepath.Join(s.dir, "test-key") + kfNested := filepath.Join(s.dir, "deeply/nested/test-key") + + ekey := secboot.EncryptionKey{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 255} + err := ekey.Save(kf) + c.Assert(err, IsNil) + c.Assert(kf, testutil.FileEquals, ekey[:]) + + fileInfo, err := os.Stat(kf) + c.Assert(err, IsNil) + c.Assert(fileInfo.Mode(), Equals, os.FileMode(0600)) + + err = ekey.Save(kfNested) + c.Assert(err, IsNil) + c.Assert(kfNested, testutil.FileEquals, ekey[:]) + di, err := os.Stat(filepath.Dir(kfNested)) c.Assert(err, IsNil) - c.Assert(data, DeepEquals, rkey[:]) - os.Remove("test-key") + c.Assert(di.Mode().Perm(), Equals, os.FileMode(0755)) } diff -Nru snapd-2.47.1+20.10.1build1/secboot/encrypt_tpm.go snapd-2.48+21.04/secboot/encrypt_tpm.go --- snapd-2.47.1+20.10.1build1/secboot/encrypt_tpm.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/secboot/encrypt_tpm.go 2020-11-19 16:51:02.000000000 +0000 @@ -21,13 +21,7 @@ package secboot import ( - "crypto/rand" - "os" - "path/filepath" - sb "github.com/snapcore/secboot" - - "github.com/snapcore/snapd/osutil" ) var ( @@ -35,31 +29,20 @@ sbAddRecoveryKeyToLUKS2Container = sb.AddRecoveryKeyToLUKS2Container ) -// RecoveryKey is a key used to unlock the encrypted partition when -// the encryption key can't be used, for example when unseal fails. -type RecoveryKey sb.RecoveryKey - -func NewRecoveryKey() (RecoveryKey, error) { - var key RecoveryKey - // rand.Read() is protected against short reads - _, err := rand.Read(key[:]) - // On return, n == len(b) if and only if err == nil - return key, err -} - -// Save writes the recovery key in the location specified by filename. -func (key RecoveryKey) Save(filename string) error { - if err := os.MkdirAll(filepath.Dir(filename), 0755); err != nil { - return err - } - return osutil.AtomicWriteFile(filename, key[:], 0600, 0) -} +const keyslotsAreaKiBSize = 2560 // 2.5MB +const metadataKiBSize = 2048 // 2MB // FormatEncryptedDevice initializes an encrypted volume on the block device -// given by node, setting the specified label. The key used to unlock the -// volume is provided using the key argument. +// given by node, setting the specified label. The key used to unlock the volume +// is provided using the key argument. func FormatEncryptedDevice(key EncryptionKey, label, node string) error { - return sbInitializeLUKS2Container(node, label, key[:]) + opts := &sb.InitializeLUKS2ContainerOptions{ + // use a lower, but still reasonable size that should give us + // enough room + MetadataKiBSize: metadataKiBSize, + KeyslotsAreaKiBSize: keyslotsAreaKiBSize, + } + return sbInitializeLUKS2Container(node, label, key[:], opts) } // AddRecoveryKey adds a fallback recovery key rkey to the existing encrypted @@ -68,3 +51,7 @@ func AddRecoveryKey(key EncryptionKey, rkey RecoveryKey, node string) error { return sbAddRecoveryKeyToLUKS2Container(node, key[:], sb.RecoveryKey(rkey)) } + +func (k RecoveryKey) String() string { + return sb.RecoveryKey(k).String() +} diff -Nru snapd-2.47.1+20.10.1build1/secboot/encrypt_tpm_test.go snapd-2.48+21.04/secboot/encrypt_tpm_test.go --- snapd-2.47.1+20.10.1build1/secboot/encrypt_tpm_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/secboot/encrypt_tpm_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -44,11 +44,16 @@ } calls := 0 - restore := secboot.MockSbInitializeLUKS2Container(func(devicePath, label string, key []byte) error { + restore := secboot.MockSbInitializeLUKS2Container(func(devicePath, label string, key []byte, + opts *sb.InitializeLUKS2ContainerOptions) error { calls++ c.Assert(devicePath, Equals, "/dev/node") c.Assert(label, Equals, "my label") c.Assert(key, DeepEquals, myKey[:]) + c.Assert(opts, DeepEquals, &sb.InitializeLUKS2ContainerOptions{ + MetadataKiBSize: 2048, + KeyslotsAreaKiBSize: 2560, + }) return tc.initErr }) defer restore() diff -Nru snapd-2.47.1+20.10.1build1/secboot/export_test.go snapd-2.48+21.04/secboot/export_test.go --- snapd-2.47.1+20.10.1build1/secboot/export_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/secboot/export_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -38,11 +38,11 @@ } } -func MockSbProvisionTPM(f func(tpm *sb.TPMConnection, mode sb.ProvisionMode, newLockoutAuth []byte) error) (restore func()) { - old := sbProvisionTPM - sbProvisionTPM = f +func MockProvisionTPM(f func(tpm *sb.TPMConnection, mode sb.ProvisionMode, newLockoutAuth []byte) error) (restore func()) { + old := provisionTPM + provisionTPM = f return func() { - sbProvisionTPM = old + provisionTPM = old } } @@ -78,32 +78,32 @@ } } -func MockSbSealKeyToTPM(f func(tpm *sb.TPMConnection, key []byte, keyPath, policyUpdatePath string, params *sb.KeyCreationParams) error) (restore func()) { - old := sbSealKeyToTPM - sbSealKeyToTPM = f +func MockSbSealKeyToTPMMultiple(f func(tpm *sb.TPMConnection, keys []*sb.SealKeyRequest, params *sb.KeyCreationParams) (sb.TPMPolicyAuthKey, error)) (restore func()) { + old := sbSealKeyToTPMMultiple + sbSealKeyToTPMMultiple = f return func() { - sbSealKeyToTPM = old + sbSealKeyToTPMMultiple = old } } -func MockSbUpdateKeyPCRProtectionPolicy(f func(tpm *sb.TPMConnection, keyPath, policyUpdatePath string, pcrProfile *sb.PCRProtectionProfile) error) (restore func()) { - old := sbUpdateKeyPCRProtectionPolicy - sbUpdateKeyPCRProtectionPolicy = f +func MockSbUpdateKeyPCRProtectionPolicyMultiple(f func(tpm *sb.TPMConnection, keyPaths []string, authKey sb.TPMPolicyAuthKey, pcrProfile *sb.PCRProtectionProfile) error) (restore func()) { + old := sbUpdateKeyPCRProtectionPolicyMultiple + sbUpdateKeyPCRProtectionPolicyMultiple = f return func() { - sbUpdateKeyPCRProtectionPolicy = old + sbUpdateKeyPCRProtectionPolicyMultiple = old } } -func MockSbLockAccessToSealedKeys(f func(tpm *sb.TPMConnection) error) (restore func()) { - old := sbLockAccessToSealedKeys - sbLockAccessToSealedKeys = f +func MockSbBlockPCRProtectionPolicies(f func(tpm *sb.TPMConnection, pcrs []int) error) (restore func()) { + old := sbBlockPCRProtectionPolicies + sbBlockPCRProtectionPolicies = f return func() { - sbLockAccessToSealedKeys = old + sbBlockPCRProtectionPolicies = old } } func MockSbActivateVolumeWithRecoveryKey(f func(volumeName, sourceDevicePath string, - keyReader io.Reader, options *sb.ActivateWithRecoveryKeyOptions) error) (restore func()) { + keyReader io.Reader, options *sb.ActivateVolumeOptions) error) (restore func()) { old := sbActivateVolumeWithRecoveryKey sbActivateVolumeWithRecoveryKey = f return func() { @@ -112,7 +112,7 @@ } func MockSbActivateVolumeWithTPMSealedKey(f func(tpm *sb.TPMConnection, volumeName, sourceDevicePath, keyPath string, - pinReader io.Reader, options *sb.ActivateWithTPMSealedKeyOptions) (bool, error)) (restore func()) { + pinReader io.Reader, options *sb.ActivateVolumeOptions) (bool, error)) (restore func()) { old := sbActivateVolumeWithTPMSealedKey sbActivateVolumeWithTPMSealedKey = f return func() { @@ -120,6 +120,15 @@ } } +func MockSbActivateVolumeWithKey(f func(volumeName, sourceDevicePath string, key []byte, + options *sb.ActivateVolumeOptions) error) (restore func()) { + old := sbActivateVolumeWithKey + sbActivateVolumeWithKey = f + return func() { + sbActivateVolumeWithKey = old + } +} + func MockSbMeasureSnapSystemEpochToTPM(f func(tpm *sb.TPMConnection, pcrIndex int) error) (restore func()) { old := sbMeasureSnapSystemEpochToTPM sbMeasureSnapSystemEpochToTPM = f @@ -144,7 +153,8 @@ } } -func MockSbInitializeLUKS2Container(f func(devicePath, label string, key []byte) error) (restore func()) { +func MockSbInitializeLUKS2Container(f func(devicePath, label string, key []byte, + opts *sb.InitializeLUKS2ContainerOptions) error) (restore func()) { old := sbInitializeLUKS2Container sbInitializeLUKS2Container = f return func() { diff -Nru snapd-2.47.1+20.10.1build1/secboot/secboot_dummy.go snapd-2.48+21.04/secboot/secboot_dummy.go --- snapd-2.47.1+20.10.1build1/secboot/secboot_dummy.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/secboot/secboot_dummy.go 2020-11-19 16:51:02.000000000 +0000 @@ -28,10 +28,10 @@ return fmt.Errorf("build without secboot support") } -func SealKey(key EncryptionKey, params *SealKeyParams) error { +func SealKeys(keys []SealKeyRequest, params *SealKeysParams) error { return fmt.Errorf("build without secboot support") } -func ResealKey(params *ResealKeyParams) error { +func ResealKeys(params *ResealKeysParams) error { return fmt.Errorf("build without secboot support") } diff -Nru snapd-2.47.1+20.10.1build1/secboot/secboot.go snapd-2.48+21.04/secboot/secboot.go --- snapd-2.47.1+20.10.1build1/secboot/secboot.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/secboot/secboot.go 2020-11-19 16:51:02.000000000 +0000 @@ -25,10 +25,18 @@ // Debian does run "go list" without any support for passing -tags. import ( + "crypto/ecdsa" + "github.com/snapcore/snapd/asserts" "github.com/snapcore/snapd/bootloader" ) +const ( + // Handles are in the block reserved for TPM owner objects (0x01800000 - 0x01bfffff) + RunObjectPCRPolicyCounterHandle = 0x01880001 + FallbackObjectPCRPolicyCounterHandle = 0x01880002 +) + type LoadChain struct { *bootloader.BootFile // Next is a list of alternative chains that can be loaded @@ -45,6 +53,13 @@ } } +type SealKeyRequest struct { + // The key to seal + Key EncryptionKey + // The path to store the sealed key file + KeyFile string +} + type SealKeyModelParams struct { // The snap model Model *asserts.Model @@ -55,22 +70,76 @@ KernelCmdlines []string } -type SealKeyParams struct { +type SealKeysParams struct { // The parameters we're sealing the key to ModelParams []*SealKeyModelParams - // The path to store the sealed key file - KeyFile string - // The path to the authorization policy update data file (only relevant for TPM) - TPMPolicyUpdateDataFile string - // The path to the lockout authorization file (only relevant for TPM) + // The authorization policy update key file (only relevant for TPM) + TPMPolicyAuthKey *ecdsa.PrivateKey + // The path to the authorization policy update key file (only relevant for TPM, + // if empty the key will not be saved) + TPMPolicyAuthKeyFile string + // The path to the lockout authorization file (only relevant for TPM and only + // used if TPMProvision is set to true) TPMLockoutAuthFile string + // Whether we should provision the TPM + TPMProvision bool + // The handle at which to create a NV index for dynamic authorization policy revocation support + PCRPolicyCounterHandle uint32 } -type ResealKeyParams struct { +type ResealKeysParams struct { // The snap model parameters ModelParams []*SealKeyModelParams - // The path to the sealed key file - KeyFile string - // The path to the authorization policy update data file (only relevant for TPM) - TPMPolicyUpdateDataFile string + // The path to the sealed key files + KeyFiles []string + // The path to the authorization policy update key file (only relevant for TPM) + TPMPolicyAuthKeyFile string +} + +// UnlockVolumeUsingSealedKeyOptions contains options for unlocking encrypted +// volumes using keys sealed to the TPM. +type UnlockVolumeUsingSealedKeyOptions struct { + // AllowRecoveryKey when true indicates activation with the recovery key + // will be attempted if activation with the sealed key failed. + AllowRecoveryKey bool +} + +// UnlockMethod is the method that was used to unlock a volume. +type UnlockMethod int + +const ( + // NotUnlocked indicates that the device was either not unlocked or is not + // an encrypted device. + NotUnlocked UnlockMethod = iota + // UnlockedWithSealedKey indicates that the device was unlocked with the + // provided sealed key object. + UnlockedWithSealedKey + // UnlockedWithRecoveryKey indicates that the device was unlocked by the + // user providing the recovery key at the prompt. + UnlockedWithRecoveryKey + // UnlockedWithKey indicates that the device was unlocked with the provided + // key, which is not sealed. + UnlockedWithKey + // UnlockStatusUnknown indicates that the unlock status of the device is not clear. + UnlockStatusUnknown +) + +// UnlockResult is the result of trying to unlock a volume. +type UnlockResult struct { + // FsDevice is the device with filesystem ready to mount. + // It is the activated device if encrypted or just + // the underlying device (same as PartDevice) if non-encrypted. + // FsDevice can be empty when none was found. + FsDevice string + // PartDevice is the underlying partition device. + // PartDevice can be empty when no device was found. + PartDevice string + // IsEncrypted indicates that PartDevice is encrypted. + IsEncrypted bool + // UnlockMethod is the method used to unlock the device. Valid values are + // - NotUnlocked + // - UnlockedWithRecoveryKey + // - UnlockedWithSealedKey + // - UnlockedWithKey + UnlockMethod UnlockMethod } diff -Nru snapd-2.47.1+20.10.1build1/secboot/secboot_tpm.go snapd-2.48+21.04/secboot/secboot_tpm.go --- snapd-2.47.1+20.10.1build1/secboot/secboot_tpm.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/secboot/secboot_tpm.go 2020-11-19 16:51:02.000000000 +0000 @@ -24,6 +24,7 @@ "crypto/rand" "errors" "fmt" + "io/ioutil" "os" "path/filepath" @@ -42,28 +43,28 @@ ) const ( - // Handles are in the block reserved for owner objects (0x01800000 - 0x01bfffff) - pinHandle = 0x01880000 + keyringPrefix = "ubuntu-fde" ) var ( - sbConnectToDefaultTPM = sb.ConnectToDefaultTPM - sbMeasureSnapSystemEpochToTPM = sb.MeasureSnapSystemEpochToTPM - sbMeasureSnapModelToTPM = sb.MeasureSnapModelToTPM - sbLockAccessToSealedKeys = sb.LockAccessToSealedKeys - sbActivateVolumeWithTPMSealedKey = sb.ActivateVolumeWithTPMSealedKey - sbActivateVolumeWithRecoveryKey = sb.ActivateVolumeWithRecoveryKey - sbAddEFISecureBootPolicyProfile = sb.AddEFISecureBootPolicyProfile - sbAddEFIBootManagerProfile = sb.AddEFIBootManagerProfile - sbAddSystemdEFIStubProfile = sb.AddSystemdEFIStubProfile - sbAddSnapModelProfile = sb.AddSnapModelProfile - sbProvisionTPM = sb.ProvisionTPM - sbSealKeyToTPM = sb.SealKeyToTPM - sbUpdateKeyPCRProtectionPolicy = sb.UpdateKeyPCRProtectionPolicy + sbConnectToDefaultTPM = sb.ConnectToDefaultTPM + sbMeasureSnapSystemEpochToTPM = sb.MeasureSnapSystemEpochToTPM + sbMeasureSnapModelToTPM = sb.MeasureSnapModelToTPM + sbBlockPCRProtectionPolicies = sb.BlockPCRProtectionPolicies + sbActivateVolumeWithTPMSealedKey = sb.ActivateVolumeWithTPMSealedKey + sbActivateVolumeWithRecoveryKey = sb.ActivateVolumeWithRecoveryKey + sbActivateVolumeWithKey = sb.ActivateVolumeWithKey + sbAddEFISecureBootPolicyProfile = sb.AddEFISecureBootPolicyProfile + sbAddEFIBootManagerProfile = sb.AddEFIBootManagerProfile + sbAddSystemdEFIStubProfile = sb.AddSystemdEFIStubProfile + sbAddSnapModelProfile = sb.AddSnapModelProfile + sbSealKeyToTPMMultiple = sb.SealKeyToTPMMultiple + sbUpdateKeyPCRProtectionPolicyMultiple = sb.UpdateKeyPCRProtectionPolicyMultiple randutilRandomKernelUUID = randutil.RandomKernelUUID isTPMEnabled = isTPMEnabledImpl + provisionTPM = provisionTPMImpl ) func isTPMEnabledImpl(tpm *sb.TPMConnection) bool { @@ -85,6 +86,7 @@ logger.Noticef("%v", err) return err } + defer tpm.Close() if !isTPMEnabled(tpm) { logger.Noticef("TPM device detected but not enabled") @@ -93,7 +95,7 @@ logger.Noticef("TPM device detected and enabled") - return tpm.Close() + return nil } func checkSecureBootEnabled() error { @@ -115,7 +117,9 @@ return nil } -const tpmPCR = 12 +// initramfsPCR is the TPM PCR that we reserve for the EFI image and use +// for measurement from the initramfs. +const initramfsPCR = 12 func secureConnectToTPM(ekcfile string) (*sb.TPMConnection, error) { ekCertReader, err := os.Open(ekcfile) @@ -153,7 +157,7 @@ // TPM device is available. If there's no TPM device success is returned. func MeasureSnapSystemEpochWhenPossible() error { measure := func(tpm *sb.TPMConnection) error { - return sbMeasureSnapSystemEpochToTPM(tpm, tpmPCR) + return sbMeasureSnapSystemEpochToTPM(tpm, initramfsPCR) } if err := measureWhenPossible(measure); err != nil { @@ -171,7 +175,7 @@ if err != nil { return err } - return sbMeasureSnapModelToTPM(tpm, tpmPCR, model) + return sbMeasureSnapModelToTPM(tpm, initramfsPCR, model) } if err := measureWhenPossible(measure); err != nil { @@ -181,98 +185,169 @@ return nil } -// UnlockVolumeIfEncrypted verifies whether an encrypted volume with the specified -// name exists and unlocks it. With lockKeysOnFinish set, access to the sealed -// keys will be locked when this function completes. The path to the device node -// is returned as well as whether the device node is an decrypted device node ( -// in the encrypted case). If no encrypted volume was found, then the returned -// device node is an unencrypted normal volume. -func UnlockVolumeIfEncrypted(disk disks.Disk, name string, encryptionKeyDir string, lockKeysOnFinish bool) (string, bool, error) { - // TODO:UC20: use sb.SecureConnectToDefaultTPM() if we decide there's benefit in doing that or - // we have a hard requirement for a valid EK cert chain for every boot (ie, panic - // if there isn't one). But we can't do that as long as we need to download - // intermediate certs from the manufacturer. +// LockTPMSealedKeys manually locks access to the sealed keys. Meant to be +// called in place of passing lockKeysOnFinish as true to +// UnlockVolumeUsingSealedKeyIfEncrypted for cases where we don't know if a +// given call is the last one to unlock a volume like in degraded recover mode. +func LockTPMSealedKeys() error { tpm, tpmErr := sbConnectToDefaultTPM() if tpmErr != nil { - if !xerrors.Is(tpmErr, sb.ErrNoTPM2Device) { - return "", false, fmt.Errorf("cannot unlock encrypted device %q: %v", name, tpmErr) + if xerrors.Is(tpmErr, sb.ErrNoTPM2Device) { + logger.Noticef("cannot open TPM connection: %v", tpmErr) + return nil } - logger.Noticef("cannot open TPM connection: %v", tpmErr) - } else { - defer tpm.Close() + return fmt.Errorf("cannot lock TPM: %v", tpmErr) } + defer tpm.Close() - // Also check if the TPM device is enabled. The platform firmware may disable the storage - // and endorsement hierarchies, but the device will remain visible to the operating system. - tpmDeviceAvailable := tpmErr == nil && isTPMEnabled(tpm) + // Lock access to the sealed keys. This should be called whenever there + // is a TPM device detected, regardless of whether secure boot is enabled + // or there is an encrypted volume to unlock. Note that snap-bootstrap can + // be called several times during initialization, and if there are multiple + // volumes to unlock we should lock access to the sealed keys only after + // the last encrypted volume is unlocked, in which case lockKeysOnFinish + // should be set to true. + // + // We should only touch the PCR that we've currently reserved for the kernel + // EFI image. Touching others will break the ability to perform any kind of + // attestation using the TPM because it will make the log inconsistent. + return sbBlockPCRProtectionPolicies(tpm, []int{initramfsPCR}) +} + +// UnlockVolumeUsingSealedKeyIfEncrypted verifies whether an encrypted volume +// with the specified name exists and unlocks it using a sealed key in a file +// with a corresponding name. The options control activation with the +// recovery key will be attempted if a prior activation attempt with +// the sealed key fails. +// +// Note that if the function proceeds to the point where it knows definitely +// whether there is an encrypted device or not, IsEncrypted on the return +// value will be true, even if error is non-nil. This is so that callers can be +// robust and try unlocking using another method for example. +func UnlockVolumeUsingSealedKeyIfEncrypted( + disk disks.Disk, name string, sealedEncryptionKeyFile string, opts *UnlockVolumeUsingSealedKeyOptions, +) (UnlockResult, error) { + res := UnlockResult{ + UnlockMethod: NotUnlocked, + } - var lockErr error - var mapperName string - err, foundEncDev := func() (error, bool) { - defer func() { - if lockKeysOnFinish && tpmDeviceAvailable { - // Lock access to the sealed keys. This should be called whenever there - // is a TPM device detected, regardless of whether secure boot is enabled - // or there is an encrypted volume to unlock. Note that snap-bootstrap can - // be called several times during initialization, and if there are multiple - // volumes to unlock we should lock access to the sealed keys only after - // the last encrypted volume is unlocked, in which case lockKeysOnFinish - // should be set to true. - lockErr = sbLockAccessToSealedKeys(tpm) - } - }() + if opts == nil { + opts = &UnlockVolumeUsingSealedKeyOptions{} + } + // TODO:UC20: use sb.SecureConnectToDefaultTPM() if we decide there's benefit in doing that or + // we have a hard requirement for a valid EK cert chain for every boot (ie, panic + // if there isn't one). But we can't do that as long as we need to download + // intermediate certs from the manufacturer. - // find the encrypted device using the disk we were provided - note that - // we do not specify IsDecryptedDevice in opts because here we are - // looking for the encrypted device to unlock, later on in the boot - // process we will look for the decrypted device to ensure it matches - // what we expected - partUUID, err := disk.FindMatchingPartitionUUID(name + "-enc") + // find the encrypted device using the disk we were provided - note that + // we do not specify IsDecryptedDevice in opts because here we are + // looking for the encrypted device to unlock, later on in the boot + // process we will look for the decrypted device to ensure it matches + // what we expected + partUUID, err := disk.FindMatchingPartitionUUID(name + "-enc") + if err == nil { + res.IsEncrypted = true + } else { var errNotFound disks.FilesystemLabelNotFoundError - if xerrors.As(err, &errNotFound) { - // didn't find the encrypted label, so return nil to try the - // decrypted label again - return nil, false - } + if !xerrors.As(err, &errNotFound) { + // some other kind of catastrophic error searching + // TODO: need to defer the connection to the default TPM somehow + return res, fmt.Errorf("error enumerating partitions for disk to find encrypted device %q: %v", name, err) + } + // otherwise it is an error not found and we should search for the + // unencrypted device + partUUID, err = disk.FindMatchingPartitionUUID(name) if err != nil { - return err, false + return res, fmt.Errorf("error enumerating partitions for disk to find unencrypted device %q: %v", name, err) } - encdev := filepath.Join("/dev/disk/by-partuuid", partUUID) + } - mapperName = name + "-" + randutilRandomKernelUUID() - if !tpmDeviceAvailable { - return unlockEncryptedPartitionWithRecoveryKey(mapperName, encdev), true - } + res.PartDevice = filepath.Join("/dev/disk/by-partuuid", partUUID) - sealedKeyPath := filepath.Join(encryptionKeyDir, name+".sealed-key") - return unlockEncryptedPartitionWithSealedKey(tpm, mapperName, encdev, sealedKeyPath, "", lockKeysOnFinish), true - }() - if err != nil { - return "", false, err - } - if lockErr != nil { - return "", false, fmt.Errorf("cannot lock access to sealed keys: %v", lockErr) + if !res.IsEncrypted { + // if we didn't find an encrypted device just return, don't try to + // unlock it + // the filesystem device for the unencrypted case is the same as the + // partition device + res.FsDevice = res.PartDevice + return res, nil } - if foundEncDev { - // return the encrypted device if the device we are maybe unlocking is - // an encrypted device - return filepath.Join("/dev/mapper", mapperName), true, nil + // Obtain a TPM connection. + tpm, tpmErr := sbConnectToDefaultTPM() + if tpmErr != nil { + if !xerrors.Is(tpmErr, sb.ErrNoTPM2Device) { + return res, fmt.Errorf("cannot unlock encrypted device %q: %v", name, tpmErr) + } + logger.Noticef("cannot open TPM connection: %v", tpmErr) + } else { + defer tpm.Close() } - // otherwise find the device from the disk - partUUID, err := disk.FindMatchingPartitionUUID(name) - if err != nil { - return "", false, err - } - return filepath.Join("/dev/disk/by-partuuid", partUUID), false, nil -} + // Also check if the TPM device is enabled. The platform firmware may disable the storage + // and endorsement hierarchies, but the device will remain visible to the operating system. + tpmDeviceAvailable := tpmErr == nil && isTPMEnabled(tpm) -// unlockEncryptedPartitionWithRecoveryKey prompts for the recovery key and use -// it to open an encrypted device. -func unlockEncryptedPartitionWithRecoveryKey(name, device string) error { - options := sb.ActivateWithRecoveryKeyOptions{ - Tries: 3, + mapperName := name + "-" + randutilRandomKernelUUID() + sourceDevice := res.PartDevice + targetDevice := filepath.Join("/dev/mapper", mapperName) + + // if we don't have a tpm, and we allow using a recovery key, do that + // directly + if !tpmDeviceAvailable && opts.AllowRecoveryKey { + if err := UnlockEncryptedVolumeWithRecoveryKey(mapperName, sourceDevice); err != nil { + return res, err + } + res.FsDevice = targetDevice + res.UnlockMethod = UnlockedWithRecoveryKey + return res, nil + } + + // otherwise we have a tpm and we should use the sealed key first, but + // this method will fallback to using the recovery key if enabled + method, err := unlockEncryptedPartitionWithSealedKey(tpm, mapperName, sourceDevice, sealedEncryptionKeyFile, "", opts.AllowRecoveryKey) + res.UnlockMethod = method + if err == nil { + res.FsDevice = targetDevice + } + return res, err +} + +// UnlockEncryptedVolumeUsingKey unlocks an existing volume using the provided key. +func UnlockEncryptedVolumeUsingKey(disk disks.Disk, name string, key []byte) (UnlockResult, error) { + unlockRes := UnlockResult{ + UnlockMethod: NotUnlocked, + } + // find the encrypted device using the disk we were provided - note that + // we do not specify IsDecryptedDevice in opts because here we are + // looking for the encrypted device to unlock, later on in the boot + // process we will look for the decrypted device to ensure it matches + // what we expected + partUUID, err := disk.FindMatchingPartitionUUID(name + "-enc") + if err != nil { + return unlockRes, err + } + unlockRes.IsEncrypted = true + // we have a device + encdev := filepath.Join("/dev/disk/by-partuuid", partUUID) + unlockRes.PartDevice = encdev + // make up a new name for the mapped device + mapperName := name + "-" + randutilRandomKernelUUID() + if err := unlockEncryptedPartitionWithKey(mapperName, encdev, key); err != nil { + return unlockRes, err + } + + unlockRes.FsDevice = filepath.Join("/dev/mapper/", mapperName) + unlockRes.UnlockMethod = UnlockedWithKey + return unlockRes, nil +} + +// UnlockEncryptedVolumeWithRecoveryKey prompts for the recovery key and uses it +// to open an encrypted device. +func UnlockEncryptedVolumeWithRecoveryKey(name, device string) error { + options := sb.ActivateVolumeOptions{ + RecoveryKeyTries: 3, + KeyringPrefix: keyringPrefix, } if err := sbActivateVolumeWithRecoveryKey(name, device, nil, &options); err != nil { @@ -282,35 +357,71 @@ return nil } +func isActivatedWithRecoveryKey(err error) bool { + if err == nil { + return false + } + // with non-nil err, we should check for err being ActivateWithTPMSealedKeyError + // and RecoveryKeyUsageErr inside that being nil - this indicates that the + // recovery key was used to unlock it + activateErr, ok := err.(*sb.ActivateWithTPMSealedKeyError) + if !ok { + return false + } + return activateErr.RecoveryKeyUsageErr == nil +} + // unlockEncryptedPartitionWithSealedKey unseals the keyfile and opens an encrypted // device. If activation with the sealed key fails, this function will attempt to // activate it with the fallback recovery key instead. -func unlockEncryptedPartitionWithSealedKey(tpm *sb.TPMConnection, name, device, keyfile, pinfile string, lock bool) error { - options := sb.ActivateWithTPMSealedKeyOptions{ - PINTries: 1, - RecoveryKeyTries: 3, - LockSealedKeyAccess: lock, +func unlockEncryptedPartitionWithSealedKey(tpm *sb.TPMConnection, name, device, keyfile, pinfile string, allowRecovery bool) (UnlockMethod, error) { + options := sb.ActivateVolumeOptions{ + PassphraseTries: 1, + // disable recovery key by default + RecoveryKeyTries: 0, + KeyringPrefix: keyringPrefix, + } + if allowRecovery { + // enable recovery key only when explicitly allowed + options.RecoveryKeyTries = 3 } // XXX: pinfile is currently not used activated, err := sbActivateVolumeWithTPMSealedKey(tpm, name, device, keyfile, nil, &options) - if !activated { - // ActivateVolumeWithTPMSealedKey should always return an error if activated == false - return fmt.Errorf("cannot activate encrypted device %q: %v", device, err) - } - if err != nil { - logger.Noticef("successfully activated encrypted device %q using a fallback activation method", device) - } else { - logger.Noticef("successfully activated encrypted device %q with TPM", device) - } - return nil + if activated { + // non nil error may indicate the volume was unlocked using the + // recovery key + if err == nil { + logger.Noticef("successfully activated encrypted device %q with TPM", device) + return UnlockedWithSealedKey, nil + } else if isActivatedWithRecoveryKey(err) { + logger.Noticef("successfully activated encrypted device %q using a fallback activation method", device) + return UnlockedWithRecoveryKey, nil + } + // no other error is possible when activation succeeded + return UnlockStatusUnknown, fmt.Errorf("internal error: volume activated with unexpected error: %v", err) + } + // ActivateVolumeWithTPMSealedKey should always return an error if activated == false + return NotUnlocked, fmt.Errorf("cannot activate encrypted device %q: %v", device, err) +} + +// unlockEncryptedPartitionWithKey unlocks encrypted partition with the provided +// key. +func unlockEncryptedPartitionWithKey(name, device string, key []byte) error { + // no special options set + options := sb.ActivateVolumeOptions{} + err := sbActivateVolumeWithKey(name, device, key, &options) + if err == nil { + logger.Noticef("successfully activated encrypted device %v using a key", device) + } + return err } -// SealKey provisions the TPM and seals a partition encryption key according to the +// SealKeys provisions the TPM and seals the encryption keys according to the // specified parameters. If the TPM is already provisioned, or a sealed key already -// exists, SealKey will fail and return an error. -func SealKey(key EncryptionKey, params *SealKeyParams) error { +// exists, SealKeys will fail and return an error. +func SealKeys(keys []SealKeyRequest, params *SealKeysParams) error { numModels := len(params.ModelParams) if numModels < 1 { return fmt.Errorf("at least one set of model-specific parameters is required") @@ -330,22 +441,44 @@ return err } - // Provision the TPM as late as possible - if err := tpmProvision(tpm, params.TPMLockoutAuthFile); err != nil { - return err + if params.TPMProvision { + // Provision the TPM as late as possible + if err := tpmProvision(tpm, params.TPMLockoutAuthFile); err != nil { + return err + } } - // Seal key to the TPM + // Seal the provided keys to the TPM creationParams := sb.KeyCreationParams{ - PCRProfile: pcrProfile, - PINHandle: pinHandle, + PCRProfile: pcrProfile, + PCRPolicyCounterHandle: tpm2.Handle(params.PCRPolicyCounterHandle), + AuthKey: params.TPMPolicyAuthKey, + } + + sbKeys := make([]*sb.SealKeyRequest, 0, len(keys)) + for i := range keys { + sbKeys = append(sbKeys, &sb.SealKeyRequest{ + Key: keys[i].Key[:], + Path: keys[i].KeyFile, + }) + } + + authKey, err := sbSealKeyToTPMMultiple(tpm, sbKeys, &creationParams) + if err != nil { + return err + } + if params.TPMPolicyAuthKeyFile != "" { + if err := osutil.AtomicWriteFile(params.TPMPolicyAuthKeyFile, authKey, 0600, 0); err != nil { + return fmt.Errorf("cannot write the policy auth key file: %v", err) + } } - return sbSealKeyToTPM(tpm, key[:], params.KeyFile, params.TPMPolicyUpdateDataFile, &creationParams) + + return nil } -// ResealKey updates the PCR protection policy for the sealed encryption key according to -// the specified parameters. -func ResealKey(params *ResealKeyParams) error { +// ResealKeys updates the PCR protection policy for the sealed encryption keys +// according to the specified parameters. +func ResealKeys(params *ResealKeysParams) error { numModels := len(params.ModelParams) if numModels < 1 { return fmt.Errorf("at least one set of model-specific parameters is required") @@ -365,7 +498,12 @@ return err } - return sbUpdateKeyPCRProtectionPolicy(tpm, params.KeyFile, params.TPMPolicyUpdateDataFile, pcrProfile) + authKey, err := ioutil.ReadFile(params.TPMPolicyAuthKeyFile) + if err != nil { + return fmt.Errorf("cannot read the policy auth key file: %v", err) + } + + return sbUpdateKeyPCRProtectionPolicyMultiple(tpm, params.KeyFiles, authKey, pcrProfile) } func buildPCRProtectionProfile(modelParams []*SealKeyModelParams) (*sb.PCRProtectionProfile, error) { @@ -407,7 +545,7 @@ if len(mp.KernelCmdlines) != 0 { systemdStubParams := sb.SystemdEFIStubProfileParams{ PCRAlgorithm: tpm2.HashAlgorithmSHA256, - PCRIndex: tpmPCR, + PCRIndex: initramfsPCR, KernelCmdlines: mp.KernelCmdlines, } if err := sbAddSystemdEFIStubProfile(modelProfile, &systemdStubParams); err != nil { @@ -419,7 +557,7 @@ if mp.Model != nil { snapModelParams := sb.SnapModelProfileParams{ PCRAlgorithm: tpm2.HashAlgorithmSHA256, - PCRIndex: tpmPCR, + PCRIndex: initramfsPCR, Models: []sb.SnapModel{mp.Model}, } if err := sbAddSnapModelProfile(modelProfile, &snapModelParams); err != nil { @@ -437,6 +575,8 @@ pcrProfile = modelPCRProfiles[0] } + logger.Debugf("PCR protection profile:\n%s", pcrProfile.String()) + return pcrProfile, nil } @@ -455,13 +595,17 @@ // TODO:UC20: ideally we should ask the firmware to clear the TPM and then reboot // if the device has previously been provisioned, see // https://godoc.org/github.com/snapcore/secboot#RequestTPMClearUsingPPI - if err := sbProvisionTPM(tpm, sb.ProvisionModeFull, lockoutAuth); err != nil { + if err := provisionTPM(tpm, sb.ProvisionModeFull, lockoutAuth); err != nil { logger.Noticef("TPM provisioning error: %v", err) return fmt.Errorf("cannot provision TPM: %v", err) } return nil } +func provisionTPMImpl(tpm *sb.TPMConnection, mode sb.ProvisionMode, lockoutAuth []byte) error { + return tpm.EnsureProvisioned(mode, lockoutAuth) +} + // buildLoadSequences builds EFI load image event trees from this package LoadChains func buildLoadSequences(chains []*LoadChain) (loadseqs []*sb.EFIImageLoadEvent, err error) { // this will build load event trees for the current diff -Nru snapd-2.47.1+20.10.1build1/secboot/secboot_tpm_test.go snapd-2.48+21.04/secboot/secboot_tpm_test.go --- snapd-2.47.1+20.10.1build1/secboot/secboot_tpm_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/secboot/secboot_tpm_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -21,6 +21,7 @@ package secboot_test import ( + "crypto/ecdsa" "errors" "fmt" "io" @@ -37,6 +38,7 @@ "github.com/snapcore/snapd/bootloader" "github.com/snapcore/snapd/bootloader/efi" "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/osutil/disks" "github.com/snapcore/snapd/secboot" "github.com/snapcore/snapd/snap" @@ -234,7 +236,77 @@ } } -func (s *secbootSuite) TestUnlockIfEncrypted(c *C) { +func (s *secbootSuite) TestLockTPMSealedKeys(c *C) { + tt := []struct { + tpmErr error + tpmEnabled bool + lockOk bool + expError string + }{ + // can't connect to tpm + { + tpmErr: fmt.Errorf("failed to connect to tpm"), + expError: "cannot lock TPM: failed to connect to tpm", + }, + // no TPM2 device, shouldn't return an error + { + tpmErr: sb.ErrNoTPM2Device, + }, + // tpm is not enabled but we can lock it + { + tpmEnabled: false, + lockOk: true, + }, + // can't lock pcr protection profile + { + tpmEnabled: true, + lockOk: false, + expError: "block failed", + }, + // tpm enabled, we can lock it + { + tpmEnabled: true, + lockOk: true, + }, + } + + for _, tc := range tt { + mockSbTPM, restoreConnect := mockSbTPMConnection(c, tc.tpmErr) + defer restoreConnect() + + restore := secboot.MockIsTPMEnabled(func(tpm *sb.TPMConnection) bool { + return tc.tpmEnabled + }) + defer restore() + + sbBlockPCRProtectionPolicesCalls := 0 + restore = secboot.MockSbBlockPCRProtectionPolicies(func(tpm *sb.TPMConnection, pcrs []int) error { + sbBlockPCRProtectionPolicesCalls++ + c.Assert(tpm, Equals, mockSbTPM) + c.Assert(pcrs, DeepEquals, []int{12}) + if tc.lockOk { + return nil + } + return errors.New("block failed") + }) + defer restore() + + err := secboot.LockTPMSealedKeys() + if tc.expError == "" { + c.Assert(err, IsNil) + } else { + c.Assert(err, ErrorMatches, tc.expError) + } + // if there was no TPM connection error, we should have tried to lock it + if tc.tpmErr == nil { + c.Assert(sbBlockPCRProtectionPolicesCalls, Equals, 1) + } else { + c.Assert(sbBlockPCRProtectionPolicesCalls, Equals, 0) + } + } +} + +func (s *secbootSuite) TestUnlockVolumeUsingSealedKeyIfEncrypted(c *C) { // setup mock disks to use for locating the partition // restore := disks.MockMountPointDisksToPartitionMapping() @@ -257,71 +329,72 @@ } for idx, tc := range []struct { - tpmErr error - tpmEnabled bool // TPM storage and endorsement hierarchies disabled, only relevant if TPM available - hasEncdev bool // an encrypted device exists - rkErr error // recovery key unlock error, only relevant if TPM not available - lockRequest bool // request to lock access to the sealed key, only relevant if TPM available - lockOk bool // the lock operation succeeded - activated bool // the activation operation succeeded - device string - err string - disk *disks.MockDiskMapping + tpmErr error + keyfile string // the keyfile to be used to unseal + tpmEnabled bool // TPM storage and endorsement hierarchies disabled, only relevant if TPM available + hasEncdev bool // an encrypted device exists + rkAllow bool // allow recovery key activation + rkErr error // recovery key unlock error, only relevant if TPM not available + activated bool // the activation operation succeeded + activateErr error // the activation error + err string + skipDiskEnsureCheck bool // whether to check to ensure the mock disk contains the device label + expUnlockMethod secboot.UnlockMethod + disk *disks.MockDiskMapping }{ { - // happy case with tpm and encrypted device (lock requested) - tpmEnabled: true, hasEncdev: true, lockRequest: true, lockOk: true, - activated: true, device: "name", - disk: mockDiskWithEncDev, - }, { - // device activation fails (lock requested) - tpmEnabled: true, hasEncdev: true, lockRequest: true, lockOk: true, - err: "cannot activate encrypted device .*: activation error", - device: "name", - disk: mockDiskWithEncDev, - }, { - // activation works but lock fails (lock requested) - tpmEnabled: true, hasEncdev: true, lockRequest: true, activated: true, - err: "cannot lock access to sealed keys: lock failed", - device: "name", - disk: mockDiskWithEncDev, + // happy case with tpm and encrypted device + tpmEnabled: true, hasEncdev: true, + activated: true, + disk: mockDiskWithEncDev, + expUnlockMethod: secboot.UnlockedWithSealedKey, }, { // happy case with tpm and encrypted device - tpmEnabled: true, hasEncdev: true, lockOk: true, activated: true, - device: "name", - disk: mockDiskWithEncDev, + // with an alternative keyfile + tpmEnabled: true, hasEncdev: true, + activated: true, + disk: mockDiskWithEncDev, + keyfile: "some-other-keyfile", + expUnlockMethod: secboot.UnlockedWithSealedKey, }, { // device activation fails tpmEnabled: true, hasEncdev: true, - err: "cannot activate encrypted device .*: activation error", - device: "name", - disk: mockDiskWithEncDev, - }, { - // activation works but lock fails - tpmEnabled: true, hasEncdev: true, activated: true, device: "name", + err: "cannot activate encrypted device .*: activation error", disk: mockDiskWithEncDev, }, { - // happy case without encrypted device (lock requested) - tpmEnabled: true, lockRequest: true, lockOk: true, activated: true, - device: "name", - disk: mockDiskWithUnencDev, - }, { - // activation works but lock fails, without encrypted device (lock requested) - tpmEnabled: true, lockRequest: true, activated: true, - err: "cannot lock access to sealed keys: lock failed", - disk: mockDiskWithUnencDev, + // device activation fails + tpmEnabled: true, hasEncdev: true, + err: "cannot activate encrypted device .*: activation error", + disk: mockDiskWithEncDev, }, { // happy case without encrypted device - tpmEnabled: true, lockOk: true, activated: true, device: "name", - disk: mockDiskWithUnencDev, + tpmEnabled: true, + disk: mockDiskWithUnencDev, }, { - // activation works but lock fails, no encrypted device - tpmEnabled: true, activated: true, device: "name", - disk: mockDiskWithUnencDev, + // happy case with tpm and encrypted device, activation + // with recovery key + tpmEnabled: true, hasEncdev: true, activated: true, + activateErr: &sb.ActivateWithTPMSealedKeyError{ + // activation error with nil recovery key error + // implies volume activated successfully using + // the recovery key, + RecoveryKeyUsageErr: nil, + }, + disk: mockDiskWithEncDev, + expUnlockMethod: secboot.UnlockedWithRecoveryKey, + }, { + // tpm and encrypted device, successful activation, but + // recovery key non-nil is an unexpected state + tpmEnabled: true, hasEncdev: true, activated: true, + activateErr: &sb.ActivateWithTPMSealedKeyError{ + RecoveryKeyUsageErr: fmt.Errorf("unexpected"), + }, + expUnlockMethod: secboot.UnlockStatusUnknown, + err: `internal error: volume activated with unexpected error: .* \(unexpected\)`, + disk: mockDiskWithEncDev, }, { // tpm error, no encrypted device tpmErr: errors.New("tpm error"), - err: `cannot unlock encrypted device "name": tpm error`, disk: mockDiskWithUnencDev, }, { // tpm error, has encrypted device @@ -330,52 +403,49 @@ disk: mockDiskWithEncDev, }, { // tpm disabled, no encrypted device - device: "name", - disk: mockDiskWithUnencDev, + disk: mockDiskWithUnencDev, }, { // tpm disabled, has encrypted device, unlocked using the recovery key - hasEncdev: true, - device: "name", - disk: mockDiskWithEncDev, + hasEncdev: true, + rkAllow: true, + disk: mockDiskWithEncDev, + expUnlockMethod: secboot.UnlockedWithRecoveryKey, }, { // tpm disabled, has encrypted device, recovery key unlocking fails hasEncdev: true, rkErr: errors.New("cannot unlock with recovery key"), - disk: mockDiskWithEncDev, - err: `cannot unlock encrypted device ".*/enc-dev-partuuid": cannot unlock with recovery key`, + rkAllow: true, + disk: mockDiskWithEncDev, + err: `cannot unlock encrypted device ".*/enc-dev-partuuid": cannot unlock with recovery key`, + }, { + // no tpm, has encrypted device, unlocked using the recovery key + tpmErr: sb.ErrNoTPM2Device, hasEncdev: true, + rkAllow: true, + disk: mockDiskWithEncDev, + expUnlockMethod: secboot.UnlockedWithRecoveryKey, }, { - // no tpm, has encrypted device, unlocked using the recovery key (lock requested) - tpmErr: sb.ErrNoTPM2Device, hasEncdev: true, lockRequest: true, - disk: mockDiskWithEncDev, - device: "name", + // no tpm, has encrypted device, unlocking with recovery key not allowed + tpmErr: sb.ErrNoTPM2Device, hasEncdev: true, + disk: mockDiskWithEncDev, + err: `cannot activate encrypted device ".*/enc-dev-partuuid": activation error`, }, { // no tpm, has encrypted device, recovery key unlocking fails rkErr: errors.New("cannot unlock with recovery key"), - tpmErr: sb.ErrNoTPM2Device, hasEncdev: true, lockRequest: true, - disk: mockDiskWithEncDev, - err: `cannot unlock encrypted device ".*/enc-dev-partuuid": cannot unlock with recovery key`, - }, { - // no tpm, has encrypted device, unlocked using the recovery key tpmErr: sb.ErrNoTPM2Device, hasEncdev: true, - disk: mockDiskWithEncDev, - device: "name", - }, { - // no tpm, no encrypted device (lock requested) - tpmErr: sb.ErrNoTPM2Device, lockRequest: true, - disk: mockDiskWithUnencDev, - device: "name", + rkAllow: true, + disk: mockDiskWithEncDev, + err: `cannot unlock encrypted device ".*/enc-dev-partuuid": cannot unlock with recovery key`, }, { // no tpm, no encrypted device tpmErr: sb.ErrNoTPM2Device, disk: mockDiskWithUnencDev, - device: "name", }, { // no disks at all - disk: mockDiskWithoutAnyDev, - device: "name", + disk: mockDiskWithoutAnyDev, + skipDiskEnsureCheck: true, // error is specifically for failing to find name, NOT name-enc, we // will properly fall back to looking for name if we didn't find // name-enc - err: "filesystem label \"name\" not found", + err: "error enumerating partitions for disk to find unencrypted device \"name\": filesystem label \"name\" not found", }, } { randomUUID := fmt.Sprintf("random-uuid-for-test-%d", idx) @@ -385,7 +455,7 @@ defer restore() c.Logf("tc %v: %+v", idx, tc) - mockSbTPM, restoreConnect := mockSbTPMConnection(c, tc.tpmErr) + _, restoreConnect := mockSbTPMConnection(c, tc.tpmErr) defer restoreConnect() restore = secboot.MockIsTPMEnabled(func(tpm *sb.TPMConnection) bool { @@ -393,68 +463,90 @@ }) defer restore() - n := 0 - restore = secboot.MockSbLockAccessToSealedKeys(func(tpm *sb.TPMConnection) error { - n++ - c.Assert(tpm, Equals, mockSbTPM) - if tc.lockOk { - return nil - } - return errors.New("lock failed") - }) - defer restore() + defaultDevice := "name" - fsLabel := tc.device + fsLabel := defaultDevice if tc.hasEncdev { fsLabel += "-enc" } - partuuid := tc.disk.FilesystemLabelToPartUUID[fsLabel] + partuuid, ok := tc.disk.FilesystemLabelToPartUUID[fsLabel] + if !tc.skipDiskEnsureCheck { + c.Assert(ok, Equals, true) + } devicePath := filepath.Join("/dev/disk/by-partuuid", partuuid) + expKeyPath := tc.keyfile + if expKeyPath == "" { + expKeyPath = "vanilla-keyfile" + } + restore = secboot.MockSbActivateVolumeWithTPMSealedKey(func(tpm *sb.TPMConnection, volumeName, sourceDevicePath, - keyPath string, pinReader io.Reader, options *sb.ActivateWithTPMSealedKeyOptions) (bool, error) { + keyPath string, pinReader io.Reader, options *sb.ActivateVolumeOptions) (bool, error) { c.Assert(volumeName, Equals, "name-"+randomUUID) c.Assert(sourceDevicePath, Equals, devicePath) - c.Assert(keyPath, Equals, filepath.Join("encrypt-key-dir", "name.sealed-key")) - c.Assert(*options, DeepEquals, sb.ActivateWithTPMSealedKeyOptions{ - PINTries: 1, - RecoveryKeyTries: 3, - LockSealedKeyAccess: tc.lockRequest, - }) - if !tc.activated { + c.Assert(keyPath, Equals, expKeyPath) + if tc.rkAllow { + c.Assert(*options, DeepEquals, sb.ActivateVolumeOptions{ + PassphraseTries: 1, + RecoveryKeyTries: 3, + KeyringPrefix: "ubuntu-fde", + }) + } else { + c.Assert(*options, DeepEquals, sb.ActivateVolumeOptions{ + PassphraseTries: 1, + // activation with recovery key was disabled + RecoveryKeyTries: 0, + KeyringPrefix: "ubuntu-fde", + }) + } + if !tc.activated && tc.activateErr == nil { return false, errors.New("activation error") } - return true, nil + return tc.activated, tc.activateErr }) defer restore() restore = secboot.MockSbActivateVolumeWithRecoveryKey(func(name, device string, keyReader io.Reader, - options *sb.ActivateWithRecoveryKeyOptions) error { + options *sb.ActivateVolumeOptions) error { + if !tc.rkAllow { + c.Fatalf("unexpected attempt to activate with recovery key") + return fmt.Errorf("unexpected call") + } return tc.rkErr }) defer restore() - device, isDecryptDev, err := secboot.UnlockVolumeIfEncrypted(tc.disk, "name", "encrypt-key-dir", tc.lockRequest) + opts := &secboot.UnlockVolumeUsingSealedKeyOptions{ + AllowRecoveryKey: tc.rkAllow, + } + unlockRes, err := secboot.UnlockVolumeUsingSealedKeyIfEncrypted(tc.disk, defaultDevice, expKeyPath, opts) if tc.err == "" { c.Assert(err, IsNil) - c.Assert(isDecryptDev, Equals, tc.hasEncdev) + c.Assert(unlockRes.IsEncrypted, Equals, tc.hasEncdev) + c.Assert(unlockRes.PartDevice, Equals, devicePath) if tc.hasEncdev { - c.Assert(device, Equals, filepath.Join("/dev/mapper", tc.device+"-"+randomUUID)) + c.Assert(unlockRes.FsDevice, Equals, filepath.Join("/dev/mapper", defaultDevice+"-"+randomUUID)) } else { - c.Assert(device, Equals, devicePath) + c.Assert(unlockRes.FsDevice, Equals, devicePath) } } else { c.Assert(err, ErrorMatches, tc.err) + // also check that the IsEncrypted value matches, this is + // important for robust callers to know whether they should try to + // unlock using a different method or not + // this is only skipped on some test cases where we get an error + // very early, like trying to connect to the tpm + c.Assert(unlockRes.IsEncrypted, Equals, tc.hasEncdev) + if tc.hasEncdev { + c.Check(unlockRes.PartDevice, Equals, devicePath) + c.Check(unlockRes.FsDevice, Equals, "") + } else { + c.Check(unlockRes.PartDevice, Equals, "") + c.Check(unlockRes.FsDevice, Equals, "") + } } - // LockAccessToSealedKeys should be called whenever there is a TPM device - // detected, regardless of whether secure boot is enabled or there is an - // encrypted volume to unlock. If we have multiple encrypted volumes, we - // should call it after the last one is unlocked. - if tc.tpmErr == nil && tc.lockRequest { - c.Assert(n, Equals, 1) - } else { - c.Assert(n, Equals, 0) - } + + c.Assert(unlockRes.UnlockMethod, Equals, tc.expUnlockMethod) } } @@ -518,6 +610,7 @@ tpmEnabled bool missingFile bool badSnapFile bool + skipProvision bool addEFISbPolicyErr error addEFIBootManagerErr error addSystemdEFIStubErr error @@ -538,6 +631,7 @@ {tpmEnabled: true, addSnapModelErr: mockErr, expectedErr: "cannot add snap model profile: some error"}, {tpmEnabled: true, provisioningErr: mockErr, provisioningCalls: 1, expectedErr: "cannot provision TPM: some error"}, {tpmEnabled: true, sealErr: mockErr, provisioningCalls: 1, sealCalls: 1, expectedErr: "some error"}, + {tpmEnabled: true, skipProvision: true, provisioningCalls: 0, sealCalls: 1, expectedErr: ""}, {tpmEnabled: true, provisioningCalls: 1, sealCalls: 1, expectedErr: ""}, } { tmpDir := c.MkDir() @@ -566,7 +660,9 @@ mockBF = append(mockBF, bootloader.NewBootFile(snapPath, "kernel.efi", bootloader.RoleRecovery)) - myParams := secboot.SealKeyParams{ + myAuthKey := &ecdsa.PrivateKey{} + + myParams := secboot.SealKeysParams{ ModelParams: []*secboot.SealKeyModelParams{ { EFILoadChains: []*secboot.LoadChain{ @@ -593,14 +689,29 @@ Model: &asserts.Model{}, }, }, - KeyFile: "keyfile", - TPMPolicyUpdateDataFile: "policy-update-data-file", - TPMLockoutAuthFile: filepath.Join(tmpDir, "lockout-auth-file"), + TPMPolicyAuthKey: myAuthKey, + TPMPolicyAuthKeyFile: filepath.Join(tmpDir, "policy-auth-key-file"), + TPMLockoutAuthFile: filepath.Join(tmpDir, "lockout-auth-file"), + TPMProvision: !tc.skipProvision, + PCRPolicyCounterHandle: 42, } myKey := secboot.EncryptionKey{} + myKey2 := secboot.EncryptionKey{} for i := range myKey { myKey[i] = byte(i) + myKey2[i] = byte(128 + i) + } + + myKeys := []secboot.SealKeyRequest{ + { + Key: myKey, + KeyFile: "keyfile", + }, + { + Key: myKey2, + KeyFile: "keyfile2", + }, } // events for @@ -752,7 +863,7 @@ // mock provisioning provisioningCalls := 0 - restore = secboot.MockSbProvisionTPM(func(t *sb.TPMConnection, mode sb.ProvisionMode, newLockoutAuth []byte) error { + restore = secboot.MockProvisionTPM(func(t *sb.TPMConnection, mode sb.ProvisionMode, newLockoutAuth []byte) error { provisioningCalls++ c.Assert(t, Equals, tpm) c.Assert(mode, Equals, sb.ProvisionModeFull) @@ -763,14 +874,13 @@ // mock sealing sealCalls := 0 - restore = secboot.MockSbSealKeyToTPM(func(t *sb.TPMConnection, key []byte, keyPath, policyUpdatePath string, params *sb.KeyCreationParams) error { + restore = secboot.MockSbSealKeyToTPMMultiple(func(t *sb.TPMConnection, kr []*sb.SealKeyRequest, params *sb.KeyCreationParams) (sb.TPMPolicyAuthKey, error) { sealCalls++ c.Assert(t, Equals, tpm) - c.Assert(key, DeepEquals, myKey[:]) - c.Assert(keyPath, Equals, myParams.KeyFile) - c.Assert(policyUpdatePath, Equals, myParams.TPMPolicyUpdateDataFile) - c.Assert(params.PINHandle, Equals, tpm2.Handle(0x01880000)) - return tc.sealErr + c.Assert(kr, DeepEquals, []*sb.SealKeyRequest{{Key: myKey[:], Path: "keyfile"}, {Key: myKey2[:], Path: "keyfile2"}}) + c.Assert(params.AuthKey, Equals, myAuthKey) + c.Assert(params.PCRPolicyCounterHandle, Equals, tpm2.Handle(42)) + return sb.TPMPolicyAuthKey{}, tc.sealErr }) defer restore() @@ -780,12 +890,13 @@ }) defer restore() - err := secboot.SealKey(myKey, &myParams) + err := secboot.SealKeys(myKeys, &myParams) if tc.expectedErr == "" { c.Assert(err, IsNil) c.Assert(addEFISbPolicyCalls, Equals, 2) c.Assert(addSystemdEfiStubCalls, Equals, 2) c.Assert(addSnapModelCalls, Equals, 2) + c.Assert(osutil.FileExists(myParams.TPMPolicyAuthKeyFile), Equals, true) } else { c.Assert(err, ErrorMatches, tc.expectedErr) } @@ -821,13 +932,18 @@ {tpmEnabled: true, resealErr: mockErr, resealCalls: 1, expectedErr: "some error"}, {tpmEnabled: true, resealCalls: 1, expectedErr: ""}, } { + mockTPMPolicyAuthKey := []byte{1, 3, 3, 7} + mockTPMPolicyAuthKeyFile := filepath.Join(c.MkDir(), "policy-auth-key-file") + err := ioutil.WriteFile(mockTPMPolicyAuthKeyFile, mockTPMPolicyAuthKey, 0600) + c.Assert(err, IsNil) + mockEFI := bootloader.NewBootFile("", filepath.Join(c.MkDir(), "file.efi"), bootloader.RoleRecovery) if !tc.missingFile { err := ioutil.WriteFile(mockEFI.Path, nil, 0644) c.Assert(err, IsNil) } - myParams := &secboot.ResealKeyParams{ + myParams := &secboot.ResealKeysParams{ ModelParams: []*secboot.SealKeyModelParams{ { EFILoadChains: []*secboot.LoadChain{secboot.NewLoadChain(mockEFI)}, @@ -835,8 +951,8 @@ Model: &asserts.Model{}, }, }, - KeyFile: "keyfile", - TPMPolicyUpdateDataFile: "policy-update-data-file", + KeyFiles: []string{"keyfile", "keyfile2"}, + TPMPolicyAuthKeyFile: mockTPMPolicyAuthKeyFile, } sequences := []*sb.EFIImageLoadEvent{ @@ -905,17 +1021,17 @@ // mock PCR protection policy update resealCalls := 0 - restore = secboot.MockSbUpdateKeyPCRProtectionPolicy(func(t *sb.TPMConnection, keyPath, polUpdatePath string, profile *sb.PCRProtectionProfile) error { + restore = secboot.MockSbUpdateKeyPCRProtectionPolicyMultiple(func(t *sb.TPMConnection, keyPaths []string, authKey sb.TPMPolicyAuthKey, profile *sb.PCRProtectionProfile) error { resealCalls++ c.Assert(t, Equals, tpm) - c.Assert(keyPath, Equals, myParams.KeyFile) - c.Assert(polUpdatePath, Equals, myParams.TPMPolicyUpdateDataFile) + c.Assert(keyPaths, DeepEquals, []string{"keyfile", "keyfile2"}) + c.Assert(authKey, DeepEquals, sb.TPMPolicyAuthKey(mockTPMPolicyAuthKey)) c.Assert(profile, Equals, pcrProfile) return tc.resealErr }) defer restore() - err := secboot.ResealKey(myParams) + err = secboot.ResealKeys(myParams) if tc.expectedErr == "" { c.Assert(err, IsNil) c.Assert(addEFISbPolicyCalls, Equals, 1) @@ -929,14 +1045,18 @@ } func (s *secbootSuite) TestSealKeyNoModelParams(c *C) { - myKey := secboot.EncryptionKey{} - myParams := secboot.SealKeyParams{ - KeyFile: "keyfile", - TPMPolicyUpdateDataFile: "policy-update-data-file", - TPMLockoutAuthFile: "lockout-auth-file", + myKeys := []secboot.SealKeyRequest{ + { + Key: secboot.EncryptionKey{}, + KeyFile: "keyfile", + }, + } + myParams := secboot.SealKeysParams{ + TPMPolicyAuthKeyFile: "policy-auth-key-file", + TPMLockoutAuthFile: "lockout-auth-file", } - err := secboot.SealKey(myKey, &myParams) + err := secboot.SealKeys(myKeys, &myParams) c.Assert(err, ErrorMatches, "at least one set of model-specific parameters is required") } @@ -969,3 +1089,66 @@ }) return tpm, restore } + +func (s *secbootSuite) TestUnlockEncryptedVolumeUsingKeyBadDisk(c *C) { + disk := &disks.MockDiskMapping{ + FilesystemLabelToPartUUID: map[string]string{}, + } + unlockRes, err := secboot.UnlockEncryptedVolumeUsingKey(disk, "ubuntu-save", []byte("fooo")) + c.Assert(err, ErrorMatches, `filesystem label "ubuntu-save-enc" not found`) + c.Check(unlockRes, DeepEquals, secboot.UnlockResult{}) +} + +func (s *secbootSuite) TestUnlockEncryptedVolumeUsingKeyHappy(c *C) { + disk := &disks.MockDiskMapping{ + FilesystemLabelToPartUUID: map[string]string{ + "ubuntu-save-enc": "123-123-123", + }, + } + restore := secboot.MockRandomKernelUUID(func() string { + return "random-uuid-123-123" + }) + defer restore() + restore = secboot.MockSbActivateVolumeWithKey(func(volumeName, sourceDevicePath string, key []byte, + options *sb.ActivateVolumeOptions) error { + c.Check(options, DeepEquals, &sb.ActivateVolumeOptions{}) + c.Check(key, DeepEquals, []byte("fooo")) + c.Check(volumeName, Matches, "ubuntu-save-random-uuid-123-123") + c.Check(sourceDevicePath, Equals, "/dev/disk/by-partuuid/123-123-123") + return nil + }) + defer restore() + unlockRes, err := secboot.UnlockEncryptedVolumeUsingKey(disk, "ubuntu-save", []byte("fooo")) + c.Assert(err, IsNil) + c.Check(unlockRes, DeepEquals, secboot.UnlockResult{ + PartDevice: "/dev/disk/by-partuuid/123-123-123", + FsDevice: "/dev/mapper/ubuntu-save-random-uuid-123-123", + IsEncrypted: true, + UnlockMethod: secboot.UnlockedWithKey, + }) +} + +func (s *secbootSuite) TestUnlockEncryptedVolumeUsingKeyErr(c *C) { + disk := &disks.MockDiskMapping{ + FilesystemLabelToPartUUID: map[string]string{ + "ubuntu-save-enc": "123-123-123", + }, + } + restore := secboot.MockRandomKernelUUID(func() string { + return "random-uuid-123-123" + }) + defer restore() + restore = secboot.MockSbActivateVolumeWithKey(func(volumeName, sourceDevicePath string, key []byte, + options *sb.ActivateVolumeOptions) error { + return fmt.Errorf("failed") + }) + defer restore() + unlockRes, err := secboot.UnlockEncryptedVolumeUsingKey(disk, "ubuntu-save", []byte("fooo")) + c.Assert(err, ErrorMatches, "failed") + // we would have at least identified that the device is a decrypted one + c.Check(unlockRes, DeepEquals, secboot.UnlockResult{ + IsEncrypted: true, + PartDevice: "/dev/disk/by-partuuid/123-123-123", + FsDevice: "", + }) +} diff -Nru snapd-2.47.1+20.10.1build1/seed/export_test.go snapd-2.48+21.04/seed/export_test.go --- snapd-2.47.1+20.10.1build1/seed/export_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/seed/export_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -27,6 +27,4 @@ var ( LoadAssertions = loadAssertions - - ValidateUC20SeedSystemLabel = validateUC20SeedSystemLabel ) diff -Nru snapd-2.47.1+20.10.1build1/seed/internal/validate.go snapd-2.48+21.04/seed/internal/validate.go --- snapd-2.47.1+20.10.1build1/seed/internal/validate.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48+21.04/seed/internal/validate.go 2020-11-19 16:51:02.000000000 +0000 @@ -0,0 +1,41 @@ +// -*- 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 ( + "fmt" + "regexp" +) + +// validSeedSystemLabel is the regex describing a valid system label. Typically +// system labels are expected to be date based, eg. 20201116, but for +// completeness follow the same rule as model names (incl. one letter model +// names and thus system labels), with the exception that uppercase letters are +// not allowed, as the systems will often be stored in a FAT filesystem. +var validSeedSystemLabel = regexp.MustCompile("^[a-z0-9](?:-?[a-z0-9])*$") + +// ValidateSeedSystemLabel checks whether the string is a valid UC20 seed system +// label. +func ValidateUC20SeedSystemLabel(label string) error { + if !validSeedSystemLabel.MatchString(label) { + return fmt.Errorf("invalid seed system label: %q", label) + } + return nil +} diff -Nru snapd-2.47.1+20.10.1build1/seed/internal/validate_test.go snapd-2.48+21.04/seed/internal/validate_test.go --- snapd-2.47.1+20.10.1build1/seed/internal/validate_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48+21.04/seed/internal/validate_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -0,0 +1,68 @@ +// -*- 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 ( + "fmt" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/seed/internal" +) + +type validateSuite struct{} + +var _ = Suite(&validateSuite{}) + +func (s *validateSuite) TestValidateSeedSystemLabel(c *C) { + valid := []string{ + "a", + "ab", + "a-a", + "a-123", + "a-a-a", + "20191119", + "foobar", + "my-system", + "brand-system-date-1234", + } + for _, label := range valid { + c.Logf("trying valid label: %q", label) + err := internal.ValidateUC20SeedSystemLabel(label) + c.Check(err, IsNil) + } + + invalid := []string{ + "", + "/bin", + "../../bin/bar", + ":invalid:", + "日本語", + "-invalid", + "invalid-", + "MYSYSTEM", + "mySystem", + } + for _, label := range invalid { + c.Logf("trying invalid label: %q", label) + err := internal.ValidateUC20SeedSystemLabel(label) + c.Check(err, ErrorMatches, fmt.Sprintf("invalid seed system label: %q", label)) + } +} diff -Nru snapd-2.47.1+20.10.1build1/seed/seed.go snapd-2.48+21.04/seed/seed.go --- snapd-2.47.1+20.10.1build1/seed/seed.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/seed/seed.go 2020-11-19 16:51:02.000000000 +0000 @@ -26,6 +26,7 @@ "path/filepath" "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/seed/internal" "github.com/snapcore/snapd/snap" "github.com/snapcore/snapd/timings" ) @@ -126,7 +127,7 @@ // label if not empty is used to identify a Core 20 recovery system seed. func Open(seedDir, label string) (Seed, error) { if label != "" { - if err := validateUC20SeedSystemLabel(label); err != nil { + if err := internal.ValidateUC20SeedSystemLabel(label); err != nil { return nil, err } return &seed20{systemDir: filepath.Join(seedDir, "systems", label)}, nil diff -Nru snapd-2.47.1+20.10.1build1/seed/seedwriter/seed20.go snapd-2.48+21.04/seed/seedwriter/seed20.go --- snapd-2.47.1+20.10.1build1/seed/seedwriter/seed20.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/seed/seedwriter/seed20.go 2020-11-19 16:51:02.000000000 +0000 @@ -25,7 +25,6 @@ "fmt" "os" "path/filepath" - "regexp" "github.com/snapcore/snapd/asserts" "github.com/snapcore/snapd/seed/internal" @@ -34,15 +33,6 @@ "github.com/snapcore/snapd/snap/naming" ) -var validSystemLabel = regexp.MustCompile("^[a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*$") - -func validateSystemLabel(label string) error { - if !validSystemLabel.MatchString(label) { - return fmt.Errorf("system label contains invalid characters: %s", label) - } - return nil -} - type policy20 struct { model *asserts.Model opts *Options @@ -50,7 +40,7 @@ warningf func(format string, a ...interface{}) } -var errNotAllowedExceptForDangerous = errors.New("cannot override channels, add local snaps or extra snaps with a model of grade higher than dangerous") +var errNotAllowedExceptForDangerous = errors.New("cannot override channels, add devmode snaps, local snaps, or extra snaps with a model of grade higher than dangerous") func (pol *policy20) checkAllowedDangerous() error { if pol.model.Grade() != asserts.ModelDangerous { diff -Nru snapd-2.47.1+20.10.1build1/seed/seedwriter/writer.go snapd-2.48+21.04/seed/seedwriter/writer.go --- snapd-2.47.1+20.10.1build1/seed/seedwriter/writer.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/seed/seedwriter/writer.go 2020-11-19 16:51:02.000000000 +0000 @@ -28,6 +28,7 @@ "github.com/snapcore/snapd/asserts" "github.com/snapcore/snapd/asserts/snapasserts" "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/seed/internal" "github.com/snapcore/snapd/snap" "github.com/snapcore/snapd/snap/channel" "github.com/snapcore/snapd/snap/naming" @@ -235,7 +236,7 @@ if opts.Label == "" { return nil, fmt.Errorf("internal error: cannot write Core 20 seed without Options.Label set") } - if err := validateSystemLabel(opts.Label); err != nil { + if err := internal.ValidateUC20SeedSystemLabel(opts.Label); err != nil { return nil, err } pol = &policy20{model: model, opts: opts, warningf: w.warningf} @@ -536,7 +537,13 @@ // SetInfo sets Info of the SeedSnap and possibly computes its // destination Path. func (w *Writer) SetInfo(sn *SeedSnap, info *snap.Info) error { + if info.Confinement == snap.DevModeConfinement { + if err := w.policy.allowsDangerousFeatures(); err != nil { + return err + } + } sn.Info = info + if sn.local { // nothing more to do return nil @@ -546,7 +553,6 @@ if err != nil { return err } - sn.Path = p return nil } diff -Nru snapd-2.47.1+20.10.1build1/seed/seedwriter/writer_test.go snapd-2.48+21.04/seed/seedwriter/writer_test.go --- snapd-2.47.1+20.10.1build1/seed/seedwriter/writer_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/seed/seedwriter/writer_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -146,6 +146,11 @@ base: core16 version: 1.0 `, + "my-devmode": `name: my-devmode +type: app +version: 1 +confinement: devmode +`, }) const pcGadgetYaml = ` @@ -1977,7 +1982,7 @@ s.opts.Label = inv w, err := seedwriter.New(model, s.opts) c.Assert(w, IsNil) - c.Check(err, ErrorMatches, `system label contains invalid characters:.*`) + c.Check(err, ErrorMatches, fmt.Sprintf(`invalid seed system label: %q`, inv)) } } @@ -2092,7 +2097,10 @@ } } -func (s *writerSuite) TestCore20NonDangerousNoChannelOverride(c *C) { +func (s *writerSuite) TestCore20NonDangerousDisallowedDevmodeSnaps(c *C) { + + s.makeSnap(c, "my-devmode", "canonical") + model := s.Brands.Model("my-brand", "my-model", map[string]interface{}{ "display-name": "my model", "architecture": "amd64", @@ -2111,14 +2119,40 @@ "type": "gadget", "default-channel": "20", }, + map[string]interface{}{ + "name": "my-devmode", + "id": s.AssertedSnapID("my-devmode"), + "type": "app", + }, }, }) - s.opts.DefaultChannel = "stable" s.opts.Label = "20191107" + + const expectedErr = `cannot override channels, add local snaps or extra snaps with a model of grade higher than dangerous` + w, err := seedwriter.New(model, s.opts) - c.Assert(w, IsNil) - c.Check(err, ErrorMatches, `cannot override channels, add local snaps or extra snaps with a model of grade higher than dangerous`) + c.Assert(err, IsNil) + + _, err = w.Start(s.db, s.newFetcher) + c.Assert(err, IsNil) + + localSnaps, err := w.LocalSnaps() + c.Assert(err, IsNil) + c.Assert(localSnaps, HasLen, 0) + + snaps, err := w.SnapsToDownload() + c.Check(err, IsNil) + c.Assert(snaps, HasLen, 5) + + c.Assert(snaps[4].SnapName(), Equals, "my-devmode") + sn := snaps[4] + + info := s.AssertedSnapInfo(sn.SnapName()) + c.Assert(info, NotNil, Commentf("%s not defined", sn.SnapName())) + err = w.SetInfo(sn, info) + c.Assert(err, ErrorMatches, "cannot override channels, add devmode snaps, local snaps, or extra snaps with a model of grade higher than dangerous") + c.Check(sn.Info, Not(Equals), info) } func (s *writerSuite) TestCore20NonDangerousDisallowedOptionsSnaps(c *C) { @@ -2155,7 +2189,7 @@ {&seedwriter.OptionsSnap{Name: "pc", Channel: "edge"}}, } - const expectedErr = `cannot override channels, add local snaps or extra snaps with a model of grade higher than dangerous` + const expectedErr = `cannot override channels, add devmode snaps, local snaps, or extra snaps with a model of grade higher than dangerous` for _, t := range tests { w, err := seedwriter.New(model, s.opts) @@ -2198,6 +2232,35 @@ } } +func (s *writerSuite) TestCore20NonDangerousNoChannelOverride(c *C) { + model := s.Brands.Model("my-brand", "my-model", map[string]interface{}{ + "display-name": "my model", + "architecture": "amd64", + "store": "my-store", + "base": "core20", + "snaps": []interface{}{ + map[string]interface{}{ + "name": "pc-kernel", + "id": s.AssertedSnapID("pc-kernel"), + "type": "kernel", + "default-channel": "20", + }, + map[string]interface{}{ + "name": "pc", + "id": s.AssertedSnapID("pc"), + "type": "gadget", + "default-channel": "20", + }, + }, + }) + + s.opts.DefaultChannel = "stable" + s.opts.Label = "20191107" + w, err := seedwriter.New(model, s.opts) + c.Assert(w, IsNil) + c.Check(err, ErrorMatches, `cannot override channels, add devmode snaps, local snaps, or extra snaps with a model of grade higher than dangerous`) +} + func (s *writerSuite) TestSeedSnapsWriteMetaCore20LocalSnaps(c *C) { // add store assertion storeAs, err := s.StoreSigning.Sign(asserts.StoreType, map[string]interface{}{ diff -Nru snapd-2.47.1+20.10.1build1/seed/validate.go snapd-2.48+21.04/seed/validate.go --- snapd-2.47.1+20.10.1build1/seed/validate.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/seed/validate.go 2020-11-19 16:51:02.000000000 +0000 @@ -23,7 +23,6 @@ "bytes" "fmt" "path/filepath" - "regexp" "sort" "github.com/snapcore/snapd/snap" @@ -136,16 +135,3 @@ return nil } - -// TODO:UC20: move these to internal, use also in seedwriter - -var validSeedSystemLabel = regexp.MustCompile("^[a-zA-Z0-9](?:-?[a-zA-Z0-9])+$") - -// validateSeedSystemLabel checks whether the string is a valid UC20 seed system -// label. -func validateUC20SeedSystemLabel(label string) error { - if !validSeedSystemLabel.MatchString(label) { - return fmt.Errorf("invalid seed system label: %q", label) - } - return nil -} diff -Nru snapd-2.47.1+20.10.1build1/seed/validate_test.go snapd-2.48+21.04/seed/validate_test.go --- snapd-2.47.1+20.10.1build1/seed/validate_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/seed/validate_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -403,36 +403,3 @@ c.Assert(err, ErrorMatches, `cannot validate seed: - cannot read seed yaml: empty element in seed`) } - -func (s *validateSuite) TestValidateSeedSystemLabel(c *C) { - valid := []string{ - "ab", - "20191119", - "foobar", - "MYSYSTEM", - "mySystem", - "my-system", - "brand-system-date-1234", - } - for _, label := range valid { - c.Logf("trying valid label: %q", label) - err := seed.ValidateUC20SeedSystemLabel(label) - c.Check(err, IsNil) - } - - invalid := []string{ - "", - "a", // too short - "/bin", - "../../bin/bar", - ":invalid:", - "日本語", - "-invalid", - "invalid-", - } - for _, label := range invalid { - c.Logf("trying invalid label: %q", label) - err := seed.ValidateUC20SeedSystemLabel(label) - c.Check(err, ErrorMatches, fmt.Sprintf("invalid seed system label: %q", label)) - } -} diff -Nru snapd-2.47.1+20.10.1build1/snap/hooktypes.go snapd-2.48+21.04/snap/hooktypes.go --- snapd-2.47.1+20.10.1build1/snap/hooktypes.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/snap/hooktypes.go 2020-11-19 16:51:02.000000000 +0000 @@ -35,6 +35,7 @@ NewHookType(regexp.MustCompile("^connect-(?:plug|slot)-[-a-z0-9]+$")), NewHookType(regexp.MustCompile("^disconnect-(?:plug|slot)-[-a-z0-9]+$")), NewHookType(regexp.MustCompile("^check-health$")), + NewHookType(regexp.MustCompile("^fde-setup$")), } // HookType represents a pattern of supported hook names. diff -Nru snapd-2.47.1+20.10.1build1/snap/pack/pack.go snapd-2.48+21.04/snap/pack/pack.go --- snapd-2.47.1+20.10.1build1/snap/pack/pack.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/snap/pack/pack.go 2020-11-19 16:51:02.000000000 +0000 @@ -126,7 +126,10 @@ } if info.SnapType == snap.TypeGadget { - if err := gadget.Validate(sourceDir, nil); err != nil { + // TODO:UC20: optionally pass model + // TODO:UC20: pass validation constraints which indicate intent + // to have data encrypted + if err := gadget.Validate(sourceDir, nil, nil); err != nil { return nil, err } } diff -Nru snapd-2.47.1+20.10.1build1/snap/snaptest/snaptest.go snapd-2.48+21.04/snap/snaptest/snaptest.go --- snapd-2.47.1+20.10.1build1/snap/snaptest/snaptest.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/snap/snaptest/snaptest.go 2020-11-19 16:51:02.000000000 +0000 @@ -131,6 +131,13 @@ defer restoreSanitize() snapInfo, err := snap.InfoFromSnapYaml([]byte(yamlText)) c.Assert(err, check.IsNil) + if snapInfo.InstanceName() == "core" && snapInfo.Type() != snap.TypeOS { + panic("core snap must use type: os") + } + if snapInfo.InstanceName() == "snapd" && snapInfo.Type() != snap.TypeSnapd { + panic("snapd snap must use type: snapd") + } + snapInfo.SideInfo = *sideInfo err = snap.Validate(snapInfo) c.Assert(err, check.IsNil) diff -Nru snapd-2.47.1+20.10.1build1/snap/snaptest/snaptest_test.go snapd-2.48+21.04/snap/snaptest/snaptest_test.go --- snapd-2.47.1+20.10.1build1/snap/snaptest/snaptest_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/snap/snaptest/snaptest_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -153,6 +153,7 @@ func (s *snapTestSuite) TestRenameSlot(c *C) { snapInfo := snaptest.MockInfo(c, `name: core +type: os version: 0 slots: old: diff -Nru snapd-2.47.1+20.10.1build1/snap/squashfs/squashfs.go snapd-2.48+21.04/snap/squashfs/squashfs.go --- snapd-2.47.1+20.10.1build1/snap/squashfs/squashfs.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/snap/squashfs/squashfs.go 2020-11-19 16:51:02.000000000 +0000 @@ -475,6 +475,14 @@ return errPaths.asErr() } +type MksquashfsError struct { + msg string +} + +func (m MksquashfsError) Error() string { + return m.msg +} + type BuildOpts struct { SnapType string Compression string @@ -527,7 +535,7 @@ return osutil.ChDir(sourceDir, func() error { output, err := cmd.CombinedOutput() if err != nil { - return fmt.Errorf("mksquashfs call failed: %s", osutil.OutputErr(output, err)) + return MksquashfsError{fmt.Sprintf("mksquashfs call failed: %s", osutil.OutputErr(output, err))} } return nil diff -Nru snapd-2.47.1+20.10.1build1/spread-shellcheck snapd-2.48+21.04/spread-shellcheck --- snapd-2.47.1+20.10.1build1/spread-shellcheck 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/spread-shellcheck 2020-11-19 16:51:02.000000000 +0000 @@ -18,6 +18,7 @@ import os import subprocess import argparse +import itertools from concurrent.futures import ThreadPoolExecutor from multiprocessing import cpu_count from typing import Dict @@ -107,6 +108,7 @@ # shellcheck knows about that script_data = [] script_data.append('set -e') + for key, value in env.items(): value = str(value) # Unpack the special "$(HOST: ...) syntax and tell shellcheck not to @@ -129,12 +131,12 @@ stdout=subprocess.PIPE, stdin=subprocess.PIPE, shell=True) - stdout, _ = proc.communicate(input='\n'.join(script_data).encode('utf-8'), timeout=10) + stdout, _ = proc.communicate(input='\n'.join(script_data).encode('utf-8'), timeout=30) if proc.returncode != 0: raise ShellcheckRunError(stdout) -def checkfile(path): +def checkfile(path, executor): logging.debug("checking file %s", path) with open(path) as inf: data = yaml.load(inf, Loader=yaml.CSafeLoader) @@ -160,54 +162,58 @@ if path.endswith('spread.yaml') and 'suites' in data: # check suites + suites_sections_and_futures = [] for suite in data['suites'].keys(): for section in SECTIONS: if section not in data['suites'][suite]: continue - try: - logging.debug("%s (suite %s): checking section %s", path, suite, section) - checksection(data['suites'][suite][section], env) - except ShellcheckRunError as serr: - errors.addfailure('suites/' + suite + '/' + section, - serr.stderr.decode('utf-8')) + logging.debug("%s (suite %s): checking section %s", path, suite, section) + future = executor.submit(checksection, data['suites'][suite][section], env) + suites_sections_and_futures.append((suite, section, future)) + for item in suites_sections_and_futures: + suite, section, future = item + try: + future.result() + except ShellcheckRunError as serr: + errors.addfailure('suites/' + suite + '/' + section, + serr.stderr.decode('utf-8')) if errors: raise errors -def findfiles(indir): - for root, _, files in os.walk(indir, topdown=True): - for name in files: - if name in ['spread.yaml', 'task.yaml']: - yield os.path.join(root, name) +def findfiles(locations): + for loc in locations: + if os.path.isdir(loc): + for root, _, files in os.walk(loc, topdown=True): + for name in files: + if name in ['spread.yaml', 'task.yaml']: + yield os.path.join(root, name) + else: + yield loc -def checkpath(loc, max_workers): - if os.path.isdir(loc): - # setup iterator - locations = findfiles(loc) - else: - locations = [loc] +def check1path(path, executor): + try: + checkfile(path, executor) + except ShellcheckError as err: + return err + return None - failed = [] - def check1path(path): - try: - checkfile(path) - except ShellcheckError as err: - return err - return None - - with ThreadPoolExecutor(max_workers=max_workers) as executor: - for serr in executor.map(check1path, locations): - if serr is None: - continue - logging.error(('shellcheck failed for file %s in sections: ' - '%s; error log follows'), - serr.path, ', '.join(serr.sectionerrors.keys())) - for section, error in serr.sectionerrors.items(): - logging.error("%s: section '%s':\n%s", serr.path, section, error) - failed.append(serr.path) +def checkpaths(locs, executor): + # setup iterator + locations = findfiles(locs) + failed = [] + for serr in executor.map(check1path, locations, itertools.repeat(executor)): + if serr is None: + continue + logging.error(('shellcheck failed for file %s in sections: ' + '%s; error log follows'), + serr.path, ', '.join(serr.sectionerrors.keys())) + for section, error in serr.sectionerrors.items(): + logging.error("%s: section '%s':\n%s", serr.path, section, error) + failed.append(serr.path) if failed: raise ShellcheckFailures(failures=failed) @@ -225,9 +231,9 @@ def main(opts): paths = opts.paths or ['.'] failures = ShellcheckFailures() - for pth in paths: + with ThreadPoolExecutor(max_workers=opts.max_procs) as executor: try: - checkpath(pth, opts.max_procs) + checkpaths(paths, executor) except ShellcheckFailures as sf: failures.merge(sf) @@ -277,4 +283,10 @@ if NO_FAIL: opts.no_errors = True + if opts.max_procs == 1: + # TODO: temporary workaround for a deadlock when running with a single + # worker + opts.max_procs += 1 + logging.warning('workers count bumped to 2 to workaround a deadlock') + main(opts) diff -Nru snapd-2.47.1+20.10.1build1/spread.yaml snapd-2.48+21.04/spread.yaml --- snapd-2.47.1+20.10.1build1/spread.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/spread.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -37,8 +37,9 @@ SNAPD_CHANNEL: '$(HOST: echo "${SPREAD_SNAPD_CHANNEL:-edge}")' REMOTE_STORE: '$(HOST: echo "${SPREAD_REMOTE_STORE:-production}")' SNAPPY_USE_STAGING_STORE: '$(HOST: if [ "$SPREAD_REMOTE_STORE" = staging ]; then echo 1; else echo 0; fi)' - DELTA_REF: 2.44 + DELTA_REF: 2.46 DELTA_PREFIX: snapd-$DELTA_REF/ + REPACK_KEEP_VENDOR: '$(HOST: echo "${REPACK_KEEP_VENDOR:-n}")' SNAPD_PUBLISHED_VERSION: '$(HOST: echo "$SPREAD_SNAPD_PUBLISHED_VERSION")' HTTP_PROXY: '$(HOST: echo "$SPREAD_HTTP_PROXY")' HTTPS_PROXY: '$(HOST: echo "$SPREAD_HTTPS_PROXY")' @@ -53,6 +54,19 @@ # Use the installed snapd and reset the systems without removing snapd REUSE_SNAPD: '$(HOST: echo "${SPREAD_REUSE_SNAPD:-0}")' PROFILE_SNAPS: '$(HOST: echo "${SPREAD_PROFILE_SNAPS:-0}")' + + # Directory where the nested images and test assets are stored + NESTED_WORK_DIR: '$(HOST: echo "${NESTED_WORK_DIR:-/tmp/work-dir}")' + # Channel used to create the nested vm + NESTED_CORE_CHANNEL: '$(HOST: echo "${NESTED_CORE_CHANNEL:-edge}")' + # Use cloud init to make initial system configuration instead of user assertion + NESTED_CORE_REFRESH_CHANNEL: '$(HOST: echo "${NESTED_CORE_REFRESH_CHANNEL:-edge}")' + # Use cloud init to make initial system configuration instead of user assertion + NESTED_USE_CLOUD_INIT: '$(HOST: echo "${NESTED_USE_CLOUD_INIT:-true}")' + # Build and use snapd from current branch + NESTED_BUILD_SNAPD_FROM_CURRENT: '$(HOST: echo "${NESTED_BUILD_SNAPD_FROM_CURRENT:-true}")' + # Download and use an custom image from this url + NESTED_CUSTOM_IMAGE_URL: '$(HOST: echo "${NESTED_CUSTOM_IMAGE_URL:-}")' # Configure nested images to be reused on the following tests NESTED_CONFIGURE_IMAGES: '$(HOST: echo "${NESTED_CONFIGURE_IMAGES:-false}")' @@ -88,6 +102,9 @@ secure-boot: true # XXX: old name, remove once new spread is deployed secureboot: true + - ubuntu-20.10-64: + workers: 8 + image: ubuntu-20.10-64 - debian-9-64: workers: 6 @@ -119,13 +136,11 @@ workers: 6 - opensuse-tumbleweed-64: workers: 6 + manual: true - centos-8-64: workers: 4 storage: preserve-size image: centos-8-64 - - ubuntu-20.10-64: - workers: 6 - image: ubuntu-os-cloud-devel/ubuntu-2010 google-sru: type: google @@ -139,6 +154,8 @@ workers: 6 - ubuntu-20.04-64: workers: 6 + - ubuntu-20.10-64: + workers: 6 google-nested: type: google @@ -215,6 +232,9 @@ - ubuntu-20.04-32: username: ubuntu password: ubuntu + - ubuntu-20.10-64: + username: ubuntu + password: ubuntu - debian-sid-64: username: debian password: debian @@ -407,12 +427,12 @@ if nested_is_nested_system; then echo '# nested VM status' systemctl status nested-vm || true - journalctl -e --no-pager -u nested-vm || true + journalctl --no-pager -u nested-vm || true + nested_print_serial_log + # add another echo in case the serial log is missing a newline + echo - if [ -f "${NESTED_LOGS_DIR}/serial.log" ]; then - echo '# nested VM serial boot log' - cat "${NESTED_LOGS_DIR}/serial.log" - fi + nested_exec sudo journalctl --no-pager -u snapd || true fi echo '# journal messages for snapd' @@ -430,10 +450,14 @@ ) | grep -v snappy_home_t || true find /var/snap -printf '%Z\t%H/%P\n' | grep -v snappy_var_t || true ;; + opensuse-*) + echo '# apparmor denials logged by auditd' + ausearch -m AVC | grep DENIED || true + ;; *) - echo '# apparmor denials ' - dmesg --ctime | grep DENIED || true - ;; + echo '# apparmor denials ' + dmesg --ctime | grep DENIED || true + ;; esac echo '# seccomp denials (kills) ' dmesg --ctime | grep type=1326 || true @@ -467,9 +491,20 @@ elif ! git show-ref "$DELTA_REF" > /dev/null; then cat <&3 >&4 else - trap "rm -f delta-ref.tar current.delta" EXIT + tmpdir="$(mktemp -d)" + #shellcheck disable=SC2064 + trap "rm -rf delta-ref.tar current.delta repacked-current.tar $tmpdir" EXIT + if [ "$REPACK_KEEP_VENDOR" = "n" ]; then + tar -C "$tmpdir" -xvf - <&3 + find "$tmpdir/$DELTA_PREFIX/vendor/" ! -wholename "$tmpdir/$DELTA_PREFIX/vendor/" \ + ! -wholename "$tmpdir/$DELTA_PREFIX/vendor/vendor.json" \ + -delete + tar -C "$tmpdir" -c "$DELTA_PREFIX" > repacked-current.tar + else + cat <&3 > repacked-current.tar + fi git archive -o delta-ref.tar --format=tar --prefix="$DELTA_PREFIX" "$DELTA_REF" - xdelta3 -S none -s delta-ref.tar <&3 > current.delta + xdelta3 -S none -s delta-ref.tar repacked-current.tar > current.delta tar c current.delta >&4 fi @@ -576,7 +611,7 @@ esac rm -f "$tf" curl -sS -o - "https://codeload.github.com/snapcore/snapd/tar.gz/$DELTA_REF" | gunzip > delta-ref.tar - xdelta3 -q -d -s delta-ref.tar current.delta | tar x --strip-components=1 + xdelta3 -q -c -d -s delta-ref.tar current.delta | tar x --strip-components=1 rm -f delta-ref.tar current.delta elif [ -d "$DELTA_PREFIX" ]; then find "$DELTA_PREFIX" -mindepth 1 -maxdepth 1 -exec mv {} . \; @@ -592,22 +627,11 @@ # context. type MATCH | tail -n +2 > "$TESTSLIB"/spread-funcs.sh unset MATCH + type NOMATCH | tail -n +2 >> "$TESTSLIB"/spread-funcs.sh + unset NOMATCH type REBOOT | tail -n +2 >> "$TESTSLIB"/spread-funcs.sh unset REBOOT - if [ -e /etc/profile.d/go.sh ]; then - # Up until recently openSUSE golang packaging injected environment - # variables into the global shell profile. This caused issues across - # updates and was, in fact, entirely useless. As such, if we are - # working against an older image that still has that file, we can - # remove it and unset certain variables to avoid the problem. - unset GOBIN - unset GOARCH - unset GOROOT - unset GOOS - rm -f /etc/profile.d/go.sh - fi - # NOTE: At this stage the source tree is available and no more special # considerations apply. "$TESTSLIB"/prepare-restore.sh --prepare-project @@ -818,33 +842,19 @@ - ubuntu-20.04-64 environment: NESTED_TYPE: "classic" - # Channel used to create the nested vm - NESTED_CORE_CHANNEL: '$(HOST: echo "${NESTED_CORE_CHANNEL:-edge}")' - # Channel used to refresh when testing refresh and revert - NESTED_CORE_REFRESH_CHANNEL: '$(HOST: echo "${NESTED_CORE_REFRESH_CHANNEL:-edge}")' - # Use cloud init to make initial system configuration instead of user assertion - NESTED_USE_CLOUD_INIT: '$(HOST: echo "${NESTED_USE_CLOUD_INIT:-true}")' # Enable kvm in the qemu command line NESTED_ENABLE_KVM: '$(HOST: echo "${NESTED_ENABLE_KVM:-true}")' # Enable tpm in the nested vm in case it is supported NESTED_ENABLE_TPM: '$(HOST: echo "${NESTED_ENABLE_TPM:-false}")' # Enable secure boot in the nested vm in case it is supported NESTED_ENABLE_SECURE_BOOT: '$(HOST: echo "${NESTED_ENABLE_SECURE_BOOT:-false}")' - # Build and use snapd from current branch - NESTED_BUILD_SNAPD_FROM_CURRENT: '$(HOST: echo "${NESTED_BUILD_SNAPD_FROM_CURRENT:-true}")' - # Download and use an custom image from this url - NESTED_CUSTOM_IMAGE_URL: '$(HOST: echo "${NESTED_CUSTOM_IMAGE_URL:-}")' - # Directory where the images and test assets are stored - NESTED_WORK_DIR: '$(HOST: echo "${NESTED_WORK_DIR:-/tmp/work-dir}")' manual: true warn-timeout: 10m kill-timeout: 60m debug: | #shellcheck source=tests/lib/nested.sh . "$TESTSLIB/nested.sh" - if [ -f "${NESTED_LOGS_DIR}/serial.log" ]; then - cat "${NESTED_LOGS_DIR}/serial.log" - fi + nested_print_serial_log prepare: | #shellcheck source=tests/lib/pkgdb.sh . "$TESTSLIB"/pkgdb.sh @@ -855,19 +865,13 @@ # Install the snapd built dpkg -i "$SPREAD_PATH"/../snapd_*.deb prepare-each: | - #shellcheck source=tests/lib/nested.sh - . "$TESTSLIB/nested.sh" - nested_prepare_env + tests.backup prepare + "$TESTSTOOLS"/nested-state prepare restore-each: | - #shellcheck source=tests/lib/nested.sh - . "$TESTSLIB/nested.sh" - nested_destroy_vm - nested_cleanup_env + "$TESTSTOOLS"/nested-state remove-vm + "$TESTSTOOLS"/nested-state restore + tests.backup restore restore: | - #shellcheck source=tests/lib/nested.sh - . "$TESTSLIB/nested.sh" - nested_cleanup_env - #shellcheck source=tests/lib/pkgdb.sh . "$TESTSLIB"/pkgdb.sh distro_purge_package qemu genisoimage sshpass qemu-kvm cloud-image-utils xz-utils @@ -879,30 +883,16 @@ environment: NESTED_TYPE: "classic" # Channel used to create the nested vm - NESTED_CORE_CHANNEL: '$(HOST: echo "${NESTED_CORE_CHANNEL:-edge}")' - # Channel used to refresh when testing refresh and revert - NESTED_CORE_REFRESH_CHANNEL: '$(HOST: echo "${NESTED_CORE_REFRESH_CHANNEL:-edge}")' - # Use cloud init to make initial system configuration instead of user assertion - NESTED_USE_CLOUD_INIT: '$(HOST: echo "${NESTED_USE_CLOUD_INIT:-true}")' - # Enable kvm in the qemu command line NESTED_ENABLE_KVM: '$(HOST: echo "${NESTED_ENABLE_KVM:-true}")' # Enable tpm in the nested vm in case it is supported NESTED_ENABLE_TPM: '$(HOST: echo "${NESTED_ENABLE_TPM:-false}")' # Enable secure boot in the nested vm in case it is supported NESTED_ENABLE_SECURE_BOOT: '$(HOST: echo "${NESTED_ENABLE_SECURE_BOOT:-false}")' - # Build and use snapd from current branch - NESTED_BUILD_SNAPD_FROM_CURRENT: '$(HOST: echo "${NESTED_BUILD_SNAPD_FROM_CURRENT:-true}")' - # Download and use an custom image from this url - NESTED_CUSTOM_IMAGE_URL: '$(HOST: echo "${NESTED_CUSTOM_IMAGE_URL:-}")' - # Directory where the images and test assets are stored - NESTED_WORK_DIR: '$(HOST: echo "${NESTED_WORK_DIR:-/tmp/work-dir}")' manual: true debug: | #shellcheck source=tests/lib/nested.sh . "$TESTSLIB/nested.sh" - if [ -f "${NESTED_LOGS_DIR}/serial.log" ]; then - cat "${NESTED_LOGS_DIR}/serial.log" - fi + nested_print_serial_log prepare: | #shellcheck source=tests/lib/pkgdb.sh . "$TESTSLIB"/pkgdb.sh @@ -913,22 +903,16 @@ # Install the snapd built dpkg -i "$SPREAD_PATH"/../snapd_*.deb - #shellcheck source=tests/lib/nested.sh - . "$TESTSLIB/nested.sh" - nested_prepare_env - nested_create_classic_vm + "$TESTSTOOLS"/nested-state prepare + "$TESTSTOOLS"/nested-state build-image classic prepare-each: | - #shellcheck source=tests/lib/nested.sh - . "$TESTSLIB/nested.sh" - nested_start_classic_vm + tests.backup prepare + "$TESTSTOOLS"/nested-state create-vm classic restore-each: | - #shellcheck source=tests/lib/nested.sh - . "$TESTSLIB/nested.sh" - nested_destroy_vm + "$TESTSTOOLS"/nested-state remove-vm + tests.backup restore restore: | - #shellcheck source=tests/lib/nested.sh - . "$TESTSLIB/nested.sh" - nested_cleanup_env + "$TESTSTOOLS"/nested-state restore #shellcheck source=tests/lib/pkgdb.sh . "$TESTSLIB"/pkgdb.sh @@ -940,31 +924,17 @@ systems: [ubuntu-16.04-64, ubuntu-18.04-64] environment: NESTED_TYPE: "core" - # Channel used to create the nested vm - NESTED_CORE_CHANNEL: '$(HOST: echo "${NESTED_CORE_CHANNEL:-edge}")' - # Channel used to refresh when testing refresh and revert - NESTED_CORE_REFRESH_CHANNEL: '$(HOST: echo "${NESTED_CORE_REFRESH_CHANNEL:-edge}")' - # Use cloud init to make initial system configuration instead of user assertion - NESTED_USE_CLOUD_INIT: '$(HOST: echo "${NESTED_USE_CLOUD_INIT:-true}")' # Enable kvm in the qemu command line NESTED_ENABLE_KVM: '$(HOST: echo "${NESTED_ENABLE_KVM:-true}")' # Enable tpm in the nested vm in case it is supported NESTED_ENABLE_TPM: '$(HOST: echo "${NESTED_ENABLE_TPM:-false}")' # Enable secure boot in the nested vm in case it is supported NESTED_ENABLE_SECURE_BOOT: '$(HOST: echo "${NESTED_ENABLE_SECURE_BOOT:-false}")' - # Build and use snapd from current branch - NESTED_BUILD_SNAPD_FROM_CURRENT: '$(HOST: echo "${NESTED_BUILD_SNAPD_FROM_CURRENT:-true}")' - # Download and use an custom image from this url - NESTED_CUSTOM_IMAGE_URL: '$(HOST: echo "${NESTED_CUSTOM_IMAGE_URL:-}")' - # Directory where the images and test assets are stored - NESTED_WORK_DIR: '$(HOST: echo "${NESTED_WORK_DIR:-/tmp/work-dir}")' manual: true debug: | #shellcheck source=tests/lib/nested.sh . "$TESTSLIB/nested.sh" - if [ -f "${NESTED_LOGS_DIR}/serial.log" ]; then - cat "${NESTED_LOGS_DIR}/serial.log" - fi + nested_print_serial_log prepare: | #shellcheck source=tests/lib/pkgdb.sh . "$TESTSLIB"/pkgdb.sh @@ -975,23 +945,16 @@ # Install the snapd built dpkg -i "$SPREAD_PATH"/../snapd_*.deb - #shellcheck source=tests/lib/nested.sh - . "$TESTSLIB/nested.sh" - nested_prepare_env - nested_create_core_vm + "$TESTSTOOLS"/nested-state prepare + "$TESTSTOOLS"/nested-state build-image core prepare-each: | - #shellcheck source=tests/lib/nested.sh - . "$TESTSLIB/nested.sh" - nested_start_core_vm - nested_wait_for_snap_command + tests.backup prepare + "$TESTSTOOLS"/nested-state create-vm core restore-each: | - #shellcheck source=tests/lib/nested.sh - . "$TESTSLIB/nested.sh" - nested_destroy_vm + "$TESTSTOOLS"/nested-state remove-vm + tests.backup restore restore: | - #shellcheck source=tests/lib/nested.sh - . "$TESTSLIB/nested.sh" - nested_cleanup_env + "$TESTSTOOLS"/nested-state restore #shellcheck source=tests/lib/pkgdb.sh . "$TESTSLIB"/pkgdb.sh @@ -1003,31 +966,17 @@ systems: [ubuntu-20.04-64] environment: NESTED_TYPE: "core" - # Channel used to create the nested vm - NESTED_CORE_CHANNEL: '$(HOST: echo "${NESTED_CORE_CHANNEL:-edge}")' - # Use cloud init to make initial system configuration instead of user assertion - NESTED_CORE_REFRESH_CHANNEL: '$(HOST: echo "${NESTED_CORE_REFRESH_CHANNEL:-edge}")' - # Use cloud init to make initial system configuration instead of user assertion - NESTED_USE_CLOUD_INIT: '$(HOST: echo "${NESTED_USE_CLOUD_INIT:-true}")' # Enable kvm in the qemu command line - NESTED_ENABLE_KVM: '$(HOST: echo "${NESTED_ENABLE_KVM:-false}")' + NESTED_ENABLE_KVM: '$(HOST: echo "${NESTED_ENABLE_KVM:-true}")' # Enable secure boot in the nested vm in case it is supported NESTED_ENABLE_TPM: '$(HOST: echo "${NESTED_ENABLE_TPM:-true}")' # Enable secure boot in the nested vm in case it is supported NESTED_ENABLE_SECURE_BOOT: '$(HOST: echo "${NESTED_ENABLE_SECURE_BOOT:-true}")' - # Build and use snapd from current branch - NESTED_BUILD_SNAPD_FROM_CURRENT: '$(HOST: echo "${NESTED_BUILD_SNAPD_FROM_CURRENT:-true}")' - # Download and use an custom image from this url - NESTED_CUSTOM_IMAGE_URL: '$(HOST: echo "${NESTED_CUSTOM_IMAGE_URL:-}")' - # Directory where the images and test assets are stored - NESTED_WORK_DIR: '$(HOST: echo "${NESTED_WORK_DIR:-/tmp/work-dir}")' manual: true debug: | #shellcheck source=tests/lib/nested.sh . "$TESTSLIB/nested.sh" - if [ -f "${NESTED_LOGS_DIR}/serial.log" ]; then - cat "${NESTED_LOGS_DIR}/serial.log" - fi + nested_print_serial_log warn-timeout: 10m kill-timeout: 60m prepare: | @@ -1040,18 +989,16 @@ # Install the snapd built dpkg -i "$SPREAD_PATH"/../snapd_*.deb - #shellcheck source=tests/lib/nested.sh - . "$TESTSLIB/nested.sh" - nested_prepare_env - nested_create_core_vm - nested_start_core_vm - nested_wait_for_snap_command + "$TESTSTOOLS"/nested-state prepare + "$TESTSTOOLS"/nested-state build-image core + prepare-each: | + tests.backup prepare + "$TESTSTOOLS"/nested-state create-vm core + restore-each: | + "$TESTSTOOLS"/nested-state remove-vm + tests.backup restore restore: | - #shellcheck source=tests/lib/nested.sh - . "$TESTSLIB/nested.sh" - nested_destroy_vm - nested_cleanup_env - + "$TESTSTOOLS"/nested-state restore #shellcheck source=tests/lib/pkgdb.sh . "$TESTSLIB"/pkgdb.sh distro_purge_package qemu genisoimage sshpass qemu-kvm cloud-image-utils xz-utils diff -Nru snapd-2.47.1+20.10.1build1/store/errors.go snapd-2.48+21.04/store/errors.go --- snapd-2.47.1+20.10.1build1/store/errors.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/store/errors.go 2020-11-19 16:51:02.000000000 +0000 @@ -128,7 +128,7 @@ // SnapActionError conveys errors that were reported on otherwise overall successful snap action (install/refresh) request. type SnapActionError struct { - // NoResults is set if the there were no results in the response + // NoResults is set if there were no results in the response NoResults bool // Refresh errors by snap name. Refresh map[string]error diff -Nru snapd-2.47.1+20.10.1build1/store/store_action_test.go snapd-2.48+21.04/store/store_action_test.go --- snapd-2.47.1+20.10.1build1/store/store_action_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/store/store_action_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -3024,3 +3024,59 @@ c.Assert(results[0].InstanceName(), Equals, "foo-2") c.Assert(results[0].SnapID, Equals, "foo-2-id") } + +func (s *storeActionSuite) TestSnapAction500(c *C) { + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assertRequest(c, r, "POST", snapActionPath) + // check device authorization is set, implicitly checking doRequest was used + w.WriteHeader(500) + })) + + c.Assert(mockServer, NotNil) + defer mockServer.Close() + + mockServerURL, _ := url.Parse(mockServer.URL) + cfg := store.Config{ + StoreBaseURL: mockServerURL, + } + dauthCtx := &testDauthContext{c: c, device: s.device} + sto := store.New(&cfg, dauthCtx) + + results, _, err := sto.SnapAction(s.ctx, nil, []*store.SnapAction{ + { + Action: "install", + InstanceName: "foo", + }, + }, nil, nil, nil) + c.Assert(err, ErrorMatches, `cannot query the store for updates: got unexpected HTTP status code 500 via POST to "http://127\.0\.0\.1:.*/v2/snaps/refresh"`) + c.Check(err, FitsTypeOf, &store.UnexpectedHTTPStatusError{}) + c.Check(results, HasLen, 0) +} + +func (s *storeActionSuite) TestSnapAction400(c *C) { + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assertRequest(c, r, "POST", snapActionPath) + // check device authorization is set, implicitly checking doRequest was used + w.WriteHeader(400) + })) + + c.Assert(mockServer, NotNil) + defer mockServer.Close() + + mockServerURL, _ := url.Parse(mockServer.URL) + cfg := store.Config{ + StoreBaseURL: mockServerURL, + } + dauthCtx := &testDauthContext{c: c, device: s.device} + sto := store.New(&cfg, dauthCtx) + + results, _, err := sto.SnapAction(s.ctx, nil, []*store.SnapAction{ + { + Action: "install", + InstanceName: "foo", + }, + }, nil, nil, nil) + c.Assert(err, ErrorMatches, `cannot query the store for updates: got unexpected HTTP status code 400 via POST to "http://127\.0\.0\.1:.*/v2/snaps/refresh"`) + c.Check(err, FitsTypeOf, &store.UnexpectedHTTPStatusError{}) + c.Check(results, HasLen, 0) +} diff -Nru snapd-2.47.1+20.10.1build1/store/store_asserts.go snapd-2.48+21.04/store/store_asserts.go --- snapd-2.47.1+20.10.1build1/store/store_asserts.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/store/store_asserts.go 2020-11-19 16:51:02.000000000 +0000 @@ -45,10 +45,33 @@ } type assertionSvcError struct { + // v1 error fields + // XXX: remove once switched to v2 API request. Status int `json:"status"` Type string `json:"type"` Title string `json:"title"` Detail string `json:"detail"` + + // v2 error list - the only field included in v2 error response. + // XXX: there is an overlap with searchV2Results (and partially with + // errorListEntry), we could share the definition. + ErrorList []struct { + Code string `json:"code"` + Message string `json:"message"` + } `json:"error-list"` +} + +func (e *assertionSvcError) isNotFound() bool { + return (len(e.ErrorList) > 0 && e.ErrorList[0].Code == "not-found" /* v2 error */) || e.Status == 404 +} + +func (e *assertionSvcError) toError() error { + // is it v2 error? + if len(e.ErrorList) > 0 { + return fmt.Errorf("assertion service error: %q", e.ErrorList[0].Message) + } + // v1 error + return fmt.Errorf("assertion service error: [%s] %q", e.Title, e.Detail) } // Assertion retrieves the assertion for the given type and primary key. @@ -66,7 +89,8 @@ asrt, e = dec.Decode() return e }, func(svcErr *assertionSvcError) error { - if svcErr.Status == 404 { + // error-list indicates v2 error response. + if svcErr.isNotFound() { // best-effort headers, _ := asserts.HeadersFromPrimaryKey(assertType, primaryKey) return &asserts.NotFoundError{ @@ -109,7 +133,8 @@ return e } } - return fmt.Errorf("assertion service error: [%s] %q", svcErr.Title, svcErr.Detail) + // default error handling + return svcErr.toError() } } return e diff -Nru snapd-2.47.1+20.10.1build1/store/store_asserts_test.go snapd-2.48+21.04/store/store_asserts_test.go --- snapd-2.47.1+20.10.1build1/store/store_asserts_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/store/store_asserts_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -185,6 +185,37 @@ }) } +func (s *storeAssertsSuite) TestAssertionNotFoundV2(c *C) { + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // XXX: update to v2 request + assertRequest(c, r, "GET", "/api/v1/snaps/assertions/.*") + c.Check(r.Header.Get("Accept"), Equals, "application/x.ubuntu.assertion") + c.Check(r.URL.Path, Matches, ".*/snap-declaration/16/snapidfoo") + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(404) + io.WriteString(w, `{"error-list":[{"code":"not-found","message":"not found: no ..."}]}`) + })) + + c.Assert(mockServer, NotNil) + defer mockServer.Close() + + mockServerURL, _ := url.Parse(mockServer.URL) + cfg := store.Config{ + AssertionsBaseURL: mockServerURL, + } + sto := store.New(&cfg, nil) + + _, err := sto.Assertion(asserts.SnapDeclarationType, []string{"16", "snapidfoo"}, nil) + c.Check(asserts.IsNotFound(err), Equals, true) + c.Check(err, DeepEquals, &asserts.NotFoundError{ + Type: asserts.SnapDeclarationType, + Headers: map[string]string{ + "series": "16", + "snap-id": "snapidfoo", + }, + }) +} + func (s *storeAssertsSuite) TestAssertion500(c *C) { var n = 0 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { diff -Nru snapd-2.47.1+20.10.1build1/store/store.go snapd-2.48+21.04/store/store.go --- snapd-2.47.1+20.10.1build1/store/store.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/store/store.go 2020-11-19 16:51:02.000000000 +0000 @@ -160,18 +160,39 @@ var ErrTooManyRequests = errors.New("too many requests") -func respToError(resp *http.Response, msg string) error { - if resp.StatusCode == 429 { - return ErrTooManyRequests - } +// UnexpectedHTTPStatusError represents an error where the store +// returned an unexpected HTTP status code, i.e. a status code that +// doesn't represent success nor an expected error condition with +// known handling (e.g. a 404 when instead presence is always +// expected). +type UnexpectedHTTPStatusError struct { + OpSummary string + StatusCode int + Method string + URL *url.URL + OopsID string +} +func (e *UnexpectedHTTPStatusError) Error() string { tpl := "cannot %s: got unexpected HTTP status code %d via %s to %q" - if oops := resp.Header.Get("X-Oops-Id"); oops != "" { + if e.OopsID != "" { tpl += " [%s]" - return fmt.Errorf(tpl, msg, resp.StatusCode, resp.Request.Method, resp.Request.URL, oops) + return fmt.Sprintf(tpl, e.OpSummary, e.StatusCode, e.Method, e.URL, e.OopsID) } + return fmt.Sprintf(tpl, e.OpSummary, e.StatusCode, e.Method, e.URL) +} - return fmt.Errorf(tpl, msg, resp.StatusCode, resp.Request.Method, resp.Request.URL) +func respToError(resp *http.Response, opSummary string) error { + if resp.StatusCode == 429 { + return ErrTooManyRequests + } + return &UnexpectedHTTPStatusError{ + OpSummary: opSummary, + StatusCode: resp.StatusCode, + Method: resp.Request.Method, + URL: resp.Request.URL, + OopsID: resp.Header.Get("X-Oops-Id"), + } } // endpointURL clones a base URL and updates it with optional path and query. diff -Nru snapd-2.47.1+20.10.1build1/strutil/cmdline.go snapd-2.48+21.04/strutil/cmdline.go --- snapd-2.47.1+20.10.1build1/strutil/cmdline.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/strutil/cmdline.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,159 +0,0 @@ -// -*- 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 strutil - -import ( - "bytes" - "fmt" -) - -// KernelCommandLineSplit tries to split the string comprising full or a part -// of a kernel command line into a list of individual arguments. Returns an -// error when the input string is incorrectly formatted. -// -// See https://www.kernel.org/doc/html/latest/admin-guide/kernel-parameters.html for details. -func KernelCommandLineSplit(s string) (out []string, err error) { - const ( - argNone int = iota // initial state - argName // looking at argument name - argAssign // looking at = - argValue // looking at unquoted value - argValueQuoteStart // looking at start of quoted value - argValueQuoted // looking at quoted value - argValueQuoteEnd // looking at end of quoted value - ) - var b bytes.Buffer - var rs = []rune(s) - var last = len(rs) - 1 - var errUnexpectedQuote = fmt.Errorf("unexpected quoting") - var errUnbalancedQUote = fmt.Errorf("unbalanced quoting") - var errUnexpectedArgument = fmt.Errorf("unexpected argument") - var errUnexpectedAssignment = fmt.Errorf("unexpected assignment") - // arguments are: - // - arg - // - arg=value, where value can be any string, spaces are preserve when quoting ".." - var state = argNone - for idx, r := range rs { - maybeSplit := false - switch state { - case argNone: - switch r { - case '"': - return nil, errUnexpectedQuote - case '=': - return nil, errUnexpectedAssignment - case ' ': - maybeSplit = true - default: - state = argName - b.WriteRune(r) - } - case argName: - switch r { - case '"': - return nil, errUnexpectedQuote - case ' ': - maybeSplit = true - state = argNone - case '=': - state = argAssign - fallthrough - default: - b.WriteRune(r) - } - case argAssign: - switch r { - case '=': - return nil, errUnexpectedAssignment - case ' ': - // no value: arg= - maybeSplit = true - state = argNone - case '"': - // arg=".. - state = argValueQuoteStart - b.WriteRune(r) - default: - // arg=v.. - state = argValue - b.WriteRune(r) - } - case argValue: - switch r { - case '"': - // arg=foo" - return nil, errUnexpectedQuote - case ' ': - state = argNone - maybeSplit = true - default: - // arg=value... - b.WriteRune(r) - } - case argValueQuoteStart: - switch r { - case '"': - // closing quote: arg="" - state = argValueQuoteEnd - b.WriteRune(r) - default: - state = argValueQuoted - b.WriteRune(r) - } - case argValueQuoted: - switch r { - case '"': - // closing quote: arg="foo" - state = argValueQuoteEnd - fallthrough - default: - b.WriteRune(r) - } - case argValueQuoteEnd: - switch r { - case ' ': - maybeSplit = true - state = argNone - case '"': - // arg="foo"" - return nil, errUnexpectedQuote - case '=': - // arg="foo"= - return nil, errUnexpectedAssignment - default: - // arg="foo"bar - return nil, errUnexpectedArgument - } - } - if maybeSplit || idx == last { - // split now - if b.Len() != 0 { - out = append(out, b.String()) - b.Reset() - } - } - } - switch state { - case argValueQuoteStart, argValueQuoted: - // ended at arg=" or arg="foo - return nil, errUnbalancedQUote - } - return out, nil -} diff -Nru snapd-2.47.1+20.10.1build1/strutil/cmdline_test.go snapd-2.48+21.04/strutil/cmdline_test.go --- snapd-2.47.1+20.10.1build1/strutil/cmdline_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/strutil/cmdline_test.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,91 +0,0 @@ -// -*- 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 strutil_test - -import ( - . "gopkg.in/check.v1" - - "github.com/snapcore/snapd/strutil" -) - -type cmdlineTestSuite struct{} - -var _ = Suite(&cmdlineTestSuite{}) - -func (s *cmdlineTestSuite) TestSplitKernelCommandLine(c *C) { - for idx, tc := range []struct { - cmd string - exp []string - errStr string - }{ - {cmd: ``, exp: nil}, - {cmd: `foo bar baz`, exp: []string{"foo", "bar", "baz"}}, - {cmd: `foo=" many spaces " bar`, exp: []string{`foo=" many spaces "`, "bar"}}, - {cmd: `foo="1$2"`, exp: []string{`foo="1$2"`}}, - {cmd: `foo=1$2`, exp: []string{`foo=1$2`}}, - {cmd: `foo= bar`, exp: []string{"foo=", "bar"}}, - {cmd: `foo=""`, exp: []string{`foo=""`}}, - {cmd: ` cpu=1,2,3 mem=0x2000;0x4000:$2 `, exp: []string{"cpu=1,2,3", "mem=0x2000;0x4000:$2"}}, - {cmd: "isolcpus=1,2,10-20,100-2000:2/25", exp: []string{"isolcpus=1,2,10-20,100-2000:2/25"}}, - // something more realistic - { - cmd: `BOOT_IMAGE=/vmlinuz-linux root=/dev/mapper/linux-root rw quiet loglevel=3 rd.udev.log_priority=3 vt.global_cursor_default=0 rd.luks.uuid=1a273f76-3118-434b-8597-a3b12a59e017 rd.luks.uuid=775e4582-33c1-423b-ac19-f734e0d5e21c rd.luks.options=discard,timeout=0 root=/dev/mapper/linux-root apparmor=1 security=apparmor`, - exp: []string{ - "BOOT_IMAGE=/vmlinuz-linux", - "root=/dev/mapper/linux-root", - "rw", "quiet", - "loglevel=3", - "rd.udev.log_priority=3", - "vt.global_cursor_default=0", - "rd.luks.uuid=1a273f76-3118-434b-8597-a3b12a59e017", - "rd.luks.uuid=775e4582-33c1-423b-ac19-f734e0d5e21c", - "rd.luks.options=discard,timeout=0", - "root=/dev/mapper/linux-root", - "apparmor=1", - "security=apparmor", - }, - }, - // this is actually ok, eg. rd.luks.options=discard,timeout=0 - {cmd: `a=b=`, exp: []string{"a=b="}}, - // bad quoting, or otherwise malformed command line - {cmd: `foo="1$2`, errStr: "unbalanced quoting"}, - {cmd: `"foo"`, errStr: "unexpected quoting"}, - {cmd: `foo"foo"`, errStr: "unexpected quoting"}, - {cmd: `foo=foo"`, errStr: "unexpected quoting"}, - {cmd: `foo="a""b"`, errStr: "unexpected quoting"}, - {cmd: `foo="a foo="b`, errStr: "unexpected argument"}, - {cmd: `foo="a"="b"`, errStr: "unexpected assignment"}, - {cmd: `=`, errStr: "unexpected assignment"}, - {cmd: `a =`, errStr: "unexpected assignment"}, - {cmd: `="foo"`, errStr: "unexpected assignment"}, - {cmd: `a==`, errStr: "unexpected assignment"}, - {cmd: `foo ==a`, errStr: "unexpected assignment"}, - } { - c.Logf("%v: cmd: %q", idx, tc.cmd) - out, err := strutil.KernelCommandLineSplit(tc.cmd) - if tc.errStr != "" { - c.Assert(err, ErrorMatches, tc.errStr) - c.Check(out, IsNil) - } else { - c.Assert(err, IsNil) - c.Check(out, DeepEquals, tc.exp) - } - } -} diff -Nru snapd-2.47.1+20.10.1build1/strutil/pathiter.go snapd-2.48+21.04/strutil/pathiter.go --- snapd-2.47.1+20.10.1build1/strutil/pathiter.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/strutil/pathiter.go 2020-11-19 16:51:02.000000000 +0000 @@ -135,7 +135,7 @@ return true } -// Rewind returns the iterator the the initial state, allowing the path to be traversed again. +// Rewind returns the iterator to the initial state, allowing the path to be traversed again. func (iter *PathIterator) Rewind() { iter.left = 0 iter.right = 0 diff -Nru snapd-2.47.1+20.10.1build1/systemd/emulation.go snapd-2.48+21.04/systemd/emulation.go --- snapd-2.47.1+20.10.1build1/systemd/emulation.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/systemd/emulation.go 2020-11-19 16:51:02.000000000 +0000 @@ -176,3 +176,11 @@ _, err := systemctlCmd("--root", s.rootDir, "unmask", service) return err } + +func (s *emulation) Mount(what, where string, options ...string) error { + return errNotImplemented +} + +func (s *emulation) Umount(whatOrWhere string) error { + return errNotImplemented +} diff -Nru snapd-2.47.1+20.10.1build1/systemd/systemd.go snapd-2.48+21.04/systemd/systemd.go --- snapd-2.47.1+20.10.1build1/systemd/systemd.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/systemd/systemd.go 2020-11-19 16:51:02.000000000 +0000 @@ -244,6 +244,10 @@ Mask(service string) error // Unmask the given service. Unmask(service string) error + // Mount requests a mount of what under where with options. + Mount(what, where string, options ...string) error + // Umount requests a mount from what or at where to be unmounted. + Umount(whatOrWhere string) error } // A Log is a single entry in the systemd journal @@ -270,8 +274,14 @@ Notify(string) } -// New returns a Systemd that uses the given rootDir -func New(rootDir string, mode InstanceMode, rep reporter) Systemd { +// New returns a Systemd that uses the default root directory and omits +// --root argument when executing systemctl. +func New(mode InstanceMode, rep reporter) Systemd { + return &systemd{mode: mode, reporter: rep} +} + +// NewUnderRoot returns a Systemd that operates on the given rootdir. +func NewUnderRoot(rootDir string, mode InstanceMode, rep reporter) Systemd { return &systemd{rootDir: rootDir, mode: mode, reporter: rep} } @@ -290,7 +300,7 @@ // InstanceMode determines which instance of systemd to control. // // SystemMode refers to the system instance (i.e. pid 1). UserMode -// refers to the the instance launched to manage the user's desktop +// refers to the instance launched to manage the user's desktop // session. GlobalUserMode controls configuration respected by all // user instances on the system. // @@ -353,22 +363,42 @@ } func (s *systemd) Enable(serviceName string) error { - _, err := s.systemctl("--root", s.rootDir, "enable", serviceName) + var err error + if s.rootDir != "" { + _, err = s.systemctl("--root", s.rootDir, "enable", serviceName) + } else { + _, err = s.systemctl("enable", serviceName) + } return err } func (s *systemd) Unmask(serviceName string) error { - _, err := s.systemctl("--root", s.rootDir, "unmask", serviceName) + var err error + if s.rootDir != "" { + _, err = s.systemctl("--root", s.rootDir, "unmask", serviceName) + } else { + _, err = s.systemctl("unmask", serviceName) + } return err } func (s *systemd) Disable(serviceName string) error { - _, err := s.systemctl("--root", s.rootDir, "disable", serviceName) + var err error + if s.rootDir != "" { + _, err = s.systemctl("--root", s.rootDir, "disable", serviceName) + } else { + _, err = s.systemctl("disable", serviceName) + } return err } func (s *systemd) Mask(serviceName string) error { - _, err := s.systemctl("--root", s.rootDir, "mask", serviceName) + var err error + if s.rootDir != "" { + _, err = s.systemctl("--root", s.rootDir, "mask", serviceName) + } else { + _, err = s.systemctl("mask", serviceName) + } return err } @@ -544,7 +574,12 @@ } func (s *systemd) IsEnabled(serviceName string) (bool, error) { - _, err := s.systemctl("--root", s.rootDir, "is-enabled", serviceName) + var err error + if s.rootDir != "" { + _, err = s.systemctl("--root", s.rootDir, "is-enabled", serviceName) + } else { + _, err = s.systemctl("is-enabled", serviceName) + } if err == nil { return true, nil } @@ -561,7 +596,12 @@ if s.mode == GlobalUserMode { panic("cannot call is-active with GlobalUserMode") } - _, err := s.systemctl("--root", s.rootDir, "is-active", serviceName) + var err error + if s.rootDir != "" { + _, err = s.systemctl("--root", s.rootDir, "is-active", serviceName) + } else { + _, err = s.systemctl("is-active", serviceName) + } if err == nil { return true, nil } @@ -874,3 +914,22 @@ _, err := s.systemctl("reload-or-restart", serviceName) return err } + +func (s *systemd) Mount(what, where string, options ...string) error { + args := make([]string, 0, 2+len(options)) + if len(options) > 0 { + args = append(args, options...) + } + args = append(args, what, where) + if output, err := exec.Command("systemd-mount", args...).CombinedOutput(); err != nil { + return osutil.OutputErr(output, err) + } + return nil +} + +func (s *systemd) Umount(whatOrWhere string) error { + if output, err := exec.Command("systemd-mount", "--umount", whatOrWhere).CombinedOutput(); err != nil { + return osutil.OutputErr(output, err) + } + return nil +} diff -Nru snapd-2.47.1+20.10.1build1/systemd/systemd_test.go snapd-2.48+21.04/systemd/systemd_test.go --- snapd-2.47.1+20.10.1build1/systemd/systemd_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/systemd/systemd_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -145,25 +145,25 @@ } func (s *SystemdTestSuite) TestDaemonReload(c *C) { - err := New("", SystemMode, s.rep).DaemonReload() + err := New(SystemMode, s.rep).DaemonReload() c.Assert(err, IsNil) c.Assert(s.argses, DeepEquals, [][]string{{"daemon-reload"}}) } func (s *SystemdTestSuite) TestDaemonReexec(c *C) { - err := New("", SystemMode, s.rep).DaemonReexec() + err := New(SystemMode, s.rep).DaemonReexec() c.Assert(err, IsNil) c.Assert(s.argses, DeepEquals, [][]string{{"daemon-reexec"}}) } func (s *SystemdTestSuite) TestStart(c *C) { - err := New("", SystemMode, s.rep).Start("foo") + err := New(SystemMode, s.rep).Start("foo") c.Assert(err, IsNil) c.Check(s.argses, DeepEquals, [][]string{{"start", "foo"}}) } func (s *SystemdTestSuite) TestStartMany(c *C) { - err := New("", SystemMode, s.rep).Start("foo", "bar", "baz") + err := New(SystemMode, s.rep).Start("foo", "bar", "baz") c.Assert(err, IsNil) c.Check(s.argses, DeepEquals, [][]string{{"start", "foo", "bar", "baz"}}) } @@ -178,7 +178,7 @@ []byte("ActiveState=inactive\n"), } s.errors = []error{nil, nil, nil, nil, &Timeout{}} - err := New("", SystemMode, s.rep).Stop("foo", 1*time.Second) + err := New(SystemMode, s.rep).Stop("foo", 1*time.Second) c.Assert(err, IsNil) c.Assert(s.argses, HasLen, 4) c.Check(s.argses[0], DeepEquals, []string{"stop", "foo"}) @@ -216,7 +216,7 @@ `[1:]), } s.errors = []error{nil} - out, err := New("", SystemMode, s.rep).Status("foo.service", "bar.service", "baz.service", "some.timer", "other.socket") + out, err := New(SystemMode, s.rep).Status("foo.service", "bar.service", "baz.service", "some.timer", "other.socket") c.Assert(err, IsNil) c.Check(out, DeepEquals, []*UnitStatus{ { @@ -266,7 +266,7 @@ `[1:]), } s.errors = []error{nil} - out, err := New("", SystemMode, s.rep).Status("foo.service") + out, err := New(SystemMode, s.rep).Status("foo.service") c.Check(err, ErrorMatches, "cannot get unit status: expected 1 results, got 2") c.Check(out, IsNil) c.Check(s.rep.msgs, IsNil) @@ -283,7 +283,7 @@ `[1:]), } s.errors = []error{nil} - out, err := New("", SystemMode, s.rep).Status("foo.service") + out, err := New(SystemMode, s.rep).Status("foo.service") c.Assert(err, ErrorMatches, `.* bad line "Potatoes" .*`) c.Check(out, IsNil) } @@ -298,7 +298,7 @@ `[1:]), } s.errors = []error{nil} - out, err := New("", SystemMode, s.rep).Status("foo.service") + out, err := New(SystemMode, s.rep).Status("foo.service") c.Assert(err, ErrorMatches, `.* queried status of "foo.service" but got status of "bar.service"`) c.Check(out, IsNil) } @@ -314,7 +314,7 @@ `[1:]), } s.errors = []error{nil} - out, err := New("", SystemMode, s.rep).Status("foo.service") + out, err := New(SystemMode, s.rep).Status("foo.service") c.Assert(err, ErrorMatches, `.* unexpected field "Potatoes" .*`) c.Check(out, IsNil) } @@ -327,7 +327,7 @@ `[1:]), } s.errors = []error{nil} - out, err := New("", SystemMode, s.rep).Status("foo.service") + out, err := New(SystemMode, s.rep).Status("foo.service") c.Assert(err, ErrorMatches, `.* missing UnitFileState, Type .*`) c.Check(out, IsNil) } @@ -340,7 +340,7 @@ `[1:]), } s.errors = []error{nil} - out, err := New("", SystemMode, s.rep).Status("foo.timer") + out, err := New(SystemMode, s.rep).Status("foo.timer") c.Assert(err, ErrorMatches, `.* missing UnitFileState .*`) c.Check(out, IsNil) } @@ -356,7 +356,7 @@ `[1:]), } s.errors = []error{nil} - out, err := New("", SystemMode, s.rep).Status("foo.service") + out, err := New(SystemMode, s.rep).Status("foo.service") c.Assert(err, ErrorMatches, `.* duplicate field "ActiveState" .*`) c.Check(out, IsNil) } @@ -371,7 +371,7 @@ `[1:]), } s.errors = []error{nil} - out, err := New("", SystemMode, s.rep).Status("foo.service") + out, err := New(SystemMode, s.rep).Status("foo.service") c.Assert(err, ErrorMatches, `.* empty field "Id" .*`) c.Check(out, IsNil) } @@ -379,14 +379,20 @@ func (s *SystemdTestSuite) TestStopTimeout(c *C) { restore := MockStopDelays(time.Millisecond, 25*time.Second) defer restore() - err := New("", SystemMode, s.rep).Stop("foo", 10*time.Millisecond) + err := New(SystemMode, s.rep).Stop("foo", 10*time.Millisecond) c.Assert(err, FitsTypeOf, &Timeout{}) c.Assert(len(s.rep.msgs) > 0, Equals, true) c.Check(s.rep.msgs[0], Equals, "Waiting for foo to stop.") } func (s *SystemdTestSuite) TestDisable(c *C) { - err := New("xyzzy", SystemMode, s.rep).Disable("foo") + err := New(SystemMode, s.rep).Disable("foo") + c.Assert(err, IsNil) + c.Check(s.argses, DeepEquals, [][]string{{"disable", "foo"}}) +} + +func (s *SystemdTestSuite) TestUnderRootDisable(c *C) { + err := NewUnderRoot("xyzzy", SystemMode, s.rep).Disable("foo") c.Assert(err, IsNil) c.Check(s.argses, DeepEquals, [][]string{{"--root", "xyzzy", "disable", "foo"}}) } @@ -434,19 +440,37 @@ } func (s *SystemdTestSuite) TestEnable(c *C) { - err := New("xyzzy", SystemMode, s.rep).Enable("foo") + err := New(SystemMode, s.rep).Enable("foo") + c.Assert(err, IsNil) + c.Check(s.argses, DeepEquals, [][]string{{"enable", "foo"}}) +} + +func (s *SystemdTestSuite) TestEnableUnderRoot(c *C) { + err := NewUnderRoot("xyzzy", SystemMode, s.rep).Enable("foo") c.Assert(err, IsNil) c.Check(s.argses, DeepEquals, [][]string{{"--root", "xyzzy", "enable", "foo"}}) } func (s *SystemdTestSuite) TestMask(c *C) { - err := New("xyzzy", SystemMode, s.rep).Mask("foo") + err := New(SystemMode, s.rep).Mask("foo") + c.Assert(err, IsNil) + c.Check(s.argses, DeepEquals, [][]string{{"mask", "foo"}}) +} + +func (s *SystemdTestSuite) TestMaskUnderRoot(c *C) { + err := NewUnderRoot("xyzzy", SystemMode, s.rep).Mask("foo") c.Assert(err, IsNil) c.Check(s.argses, DeepEquals, [][]string{{"--root", "xyzzy", "mask", "foo"}}) } func (s *SystemdTestSuite) TestUnmask(c *C) { - err := New("xyzzy", SystemMode, s.rep).Unmask("foo") + err := New(SystemMode, s.rep).Unmask("foo") + c.Assert(err, IsNil) + c.Check(s.argses, DeepEquals, [][]string{{"unmask", "foo"}}) +} + +func (s *SystemdTestSuite) TestUnmaskUnderRoot(c *C) { + err := NewUnderRoot("xyzzy", SystemMode, s.rep).Unmask("foo") c.Assert(err, IsNil) c.Check(s.argses, DeepEquals, [][]string{{"--root", "xyzzy", "unmask", "foo"}}) } @@ -460,7 +484,7 @@ nil, // for the "start" } s.errors = []error{nil, nil, nil, nil, &Timeout{}} - err := New("", SystemMode, s.rep).Restart("foo", 100*time.Millisecond) + err := New(SystemMode, s.rep).Restart("foo", 100*time.Millisecond) c.Assert(err, IsNil) c.Check(s.argses, HasLen, 3) c.Check(s.argses[0], DeepEquals, []string{"stop", "foo"}) @@ -469,7 +493,7 @@ } func (s *SystemdTestSuite) TestKill(c *C) { - c.Assert(New("", SystemMode, s.rep).Kill("foo", "HUP", ""), IsNil) + c.Assert(New(SystemMode, s.rep).Kill("foo", "HUP", ""), IsNil) c.Check(s.argses, DeepEquals, [][]string{{"kill", "foo", "-s", "HUP", "--kill-who=all"}}) } @@ -481,7 +505,7 @@ func (s *SystemdTestSuite) TestLogErrJctl(c *C) { s.jerrs = []error{&Timeout{}} - reader, err := New("", SystemMode, s.rep).LogReader([]string{"foo"}, 24, false) + reader, err := New(SystemMode, s.rep).LogReader([]string{"foo"}, 24, false) c.Check(err, NotNil) c.Check(reader, IsNil) c.Check(s.jns, DeepEquals, []string{"24"}) @@ -496,7 +520,7 @@ ` s.jouts = [][]byte{[]byte(expected)} - reader, err := New("", SystemMode, s.rep).LogReader([]string{"foo"}, 24, false) + reader, err := New(SystemMode, s.rep).LogReader([]string{"foo"}, 24, false) c.Check(err, IsNil) logs, err := ioutil.ReadAll(reader) c.Assert(err, IsNil) @@ -554,7 +578,7 @@ mockSnapPath := filepath.Join(c.MkDir(), "/var/lib/snappy/snaps/foo_1.0.snap") makeMockFile(c, mockSnapPath) - mountUnitName, err := New(rootDir, SystemMode, nil).AddMountUnitFile("foo", "42", mockSnapPath, "/snap/snapname/123", "squashfs") + mountUnitName, err := NewUnderRoot(rootDir, SystemMode, nil).AddMountUnitFile("foo", "42", mockSnapPath, "/snap/snapname/123", "squashfs") c.Assert(err, IsNil) defer os.Remove(mountUnitName) @@ -587,7 +611,7 @@ // a directory instead of a file produces a different output snapDir := c.MkDir() - mountUnitName, err := New("", SystemMode, nil).AddMountUnitFile("foodir", "x1", snapDir, "/snap/snapname/x1", "squashfs") + mountUnitName, err := New(SystemMode, nil).AddMountUnitFile("foodir", "x1", snapDir, "/snap/snapname/x1", "squashfs") c.Assert(err, IsNil) defer os.Remove(mountUnitName) @@ -609,7 +633,7 @@ c.Assert(s.argses, DeepEquals, [][]string{ {"daemon-reload"}, - {"--root", "", "enable", "snap-snapname-x1.mount"}, + {"enable", "snap-snapname-x1.mount"}, {"start", "snap-snapname-x1.mount"}, }) } @@ -628,7 +652,7 @@ err = ioutil.WriteFile(mockSnapPath, nil, 0644) c.Assert(err, IsNil) - mountUnitName, err := New("", SystemMode, nil).AddMountUnitFile("foo", "42", mockSnapPath, "/snap/snapname/123", "squashfs") + mountUnitName, err := New(SystemMode, nil).AddMountUnitFile("foo", "42", mockSnapPath, "/snap/snapname/123", "squashfs") c.Assert(err, IsNil) defer os.Remove(mountUnitName) @@ -671,7 +695,7 @@ err = ioutil.WriteFile(mockSnapPath, nil, 0644) c.Assert(err, IsNil) - mountUnitName, err := New("", SystemMode, nil).AddMountUnitFile("foo", "x1", mockSnapPath, "/snap/snapname/123", "squashfs") + mountUnitName, err := New(SystemMode, nil).AddMountUnitFile("foo", "x1", mockSnapPath, "/snap/snapname/123", "squashfs") c.Assert(err, IsNil) defer os.Remove(mountUnitName) @@ -710,7 +734,7 @@ err = ioutil.WriteFile(mockSnapPath, nil, 0644) c.Assert(err, IsNil) - mountUnitName, err := New("", SystemMode, nil).AddMountUnitFile("foo", "x1", mockSnapPath, "/snap/snapname/123", "squashfs") + mountUnitName, err := New(SystemMode, nil).AddMountUnitFile("foo", "x1", mockSnapPath, "/snap/snapname/123", "squashfs") c.Assert(err, IsNil) defer os.Remove(mountUnitName) @@ -751,6 +775,19 @@ c.Check(args, DeepEquals, []string{"-o", "json", "--no-pager", "--no-tail", "-u", "foo", "-u", "bar"}) } +func (s *SystemdTestSuite) TestIsActiveUnderRoot(c *C) { + sysErr := &Error{} + // manpage states that systemctl returns exit code 3 for inactive + // services, however we should check any non-0 exit status + sysErr.SetExitCode(1) + sysErr.SetMsg([]byte("inactive\n")) + s.errors = []error{sysErr} + + _, err := NewUnderRoot("xyzzy", SystemMode, s.rep).IsActive("foo") + c.Assert(err, IsNil) + c.Check(s.argses, DeepEquals, [][]string{{"--root", "xyzzy", "is-active", "foo"}}) +} + func (s *SystemdTestSuite) TestIsActiveIsInactive(c *C) { sysErr := &Error{} // manpage states that systemctl returns exit code 3 for inactive @@ -759,10 +796,10 @@ sysErr.SetMsg([]byte("inactive\n")) s.errors = []error{sysErr} - active, err := New("xyzzy", SystemMode, s.rep).IsActive("foo") + active, err := New(SystemMode, s.rep).IsActive("foo") c.Assert(active, Equals, false) c.Assert(err, IsNil) - c.Check(s.argses, DeepEquals, [][]string{{"--root", "xyzzy", "is-active", "foo"}}) + c.Check(s.argses, DeepEquals, [][]string{{"is-active", "foo"}}) } func (s *SystemdTestSuite) TestIsActiveIsFailed(c *C) { @@ -772,19 +809,19 @@ sysErr.SetMsg([]byte("failed\n")) s.errors = []error{sysErr} - active, err := New("xyzzy", SystemMode, s.rep).IsActive("foo") + active, err := New(SystemMode, s.rep).IsActive("foo") c.Assert(active, Equals, false) c.Assert(err, IsNil) - c.Check(s.argses, DeepEquals, [][]string{{"--root", "xyzzy", "is-active", "foo"}}) + c.Check(s.argses, DeepEquals, [][]string{{"is-active", "foo"}}) } func (s *SystemdTestSuite) TestIsActiveIsActive(c *C) { s.errors = []error{nil} - active, err := New("xyzzy", SystemMode, s.rep).IsActive("foo") + active, err := New(SystemMode, s.rep).IsActive("foo") c.Assert(active, Equals, true) c.Assert(err, IsNil) - c.Check(s.argses, DeepEquals, [][]string{{"--root", "xyzzy", "is-active", "foo"}}) + c.Check(s.argses, DeepEquals, [][]string{{"is-active", "foo"}}) } func (s *SystemdTestSuite) TestIsActiveUnexpectedErr(c *C) { @@ -793,7 +830,7 @@ sysErr.SetMsg([]byte("random-failure\n")) s.errors = []error{sysErr} - active, err := New("xyzzy", SystemMode, s.rep).IsActive("foo") + active, err := NewUnderRoot("xyzzy", SystemMode, s.rep).IsActive("foo") c.Assert(active, Equals, false) c.Assert(err, ErrorMatches, ".* failed with exit status 1: random-failure\n") } @@ -814,7 +851,7 @@ mountDir := rootDir + "/snap/foo/42" mountUnit := makeMockMountUnit(c, mountDir) - err := New(rootDir, SystemMode, nil).RemoveMountUnitFile(mountDir) + err := NewUnderRoot(rootDir, SystemMode, nil).RemoveMountUnitFile(mountDir) c.Assert(err, IsNil) // the file is gone @@ -832,7 +869,7 @@ func (s *SystemdTestSuite) testDaemonReloadMutex(c *C, reload func(Systemd) error) { rootDir := dirs.GlobalRootDir - sysd := New(rootDir, SystemMode, nil) + sysd := NewUnderRoot(rootDir, SystemMode, nil) mockSnapPath := filepath.Join(c.MkDir(), "/var/lib/snappy/snaps/foo_1.0.snap") makeMockFile(c, mockSnapPath) @@ -869,7 +906,7 @@ func (s *SystemdTestSuite) TestUserMode(c *C) { rootDir := dirs.GlobalRootDir - sysd := New(rootDir, UserMode, nil) + sysd := NewUnderRoot(rootDir, UserMode, nil) c.Assert(sysd.Enable("foo"), IsNil) c.Check(s.argses[0], DeepEquals, []string{"--user", "--root", rootDir, "enable", "foo"}) @@ -879,7 +916,7 @@ func (s *SystemdTestSuite) TestGlobalUserMode(c *C) { rootDir := dirs.GlobalRootDir - sysd := New(rootDir, GlobalUserMode, nil) + sysd := NewUnderRoot(rootDir, GlobalUserMode, nil) c.Assert(sysd.Enable("foo"), IsNil) c.Check(s.argses[0], DeepEquals, []string{"--user", "--global", "--root", rootDir, "enable", "foo"}) @@ -1086,3 +1123,58 @@ c.Check(s.argses, DeepEquals, [][]string{ {"--root", "/path", "unmask", "foo"}}) } + +func (s *SystemdTestSuite) TestMountHappy(c *C) { + sysd := New(SystemMode, nil) + + cmd := testutil.MockCommand(c, "systemd-mount", "") + defer cmd.Restore() + + c.Assert(sysd.Mount("foo", "bar"), IsNil) + c.Check(cmd.Calls(), DeepEquals, [][]string{ + {"systemd-mount", "foo", "bar"}, + }) + cmd.ForgetCalls() + c.Assert(sysd.Mount("foo", "bar", "-o", "bind"), IsNil) + c.Check(cmd.Calls(), DeepEquals, [][]string{ + {"systemd-mount", "-o", "bind", "foo", "bar"}, + }) +} + +func (s *SystemdTestSuite) TestMountErr(c *C) { + sysd := New(SystemMode, nil) + + cmd := testutil.MockCommand(c, "systemd-mount", `echo "failed"; exit 111`) + defer cmd.Restore() + + err := sysd.Mount("foo", "bar") + c.Assert(err, ErrorMatches, "failed") + c.Check(cmd.Calls(), DeepEquals, [][]string{ + {"systemd-mount", "foo", "bar"}, + }) +} + +func (s *SystemdTestSuite) TestUmountHappy(c *C) { + sysd := New(SystemMode, nil) + + cmd := testutil.MockCommand(c, "systemd-mount", "") + defer cmd.Restore() + + c.Assert(sysd.Umount("bar"), IsNil) + c.Check(cmd.Calls(), DeepEquals, [][]string{ + {"systemd-mount", "--umount", "bar"}, + }) +} + +func (s *SystemdTestSuite) TestUmountErr(c *C) { + sysd := New(SystemMode, nil) + + cmd := testutil.MockCommand(c, "systemd-mount", `echo "failed"; exit 111`) + defer cmd.Restore() + + err := sysd.Umount("bar") + c.Assert(err, ErrorMatches, "failed") + c.Check(cmd.Calls(), DeepEquals, [][]string{ + {"systemd-mount", "--umount", "bar"}, + }) +} diff -Nru snapd-2.47.1+20.10.1build1/tests/bin/NOMATCH snapd-2.48+21.04/tests/bin/NOMATCH --- snapd-2.47.1+20.10.1build1/tests/bin/NOMATCH 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48+21.04/tests/bin/NOMATCH 2020-11-19 16:51:02.000000000 +0000 @@ -0,0 +1,8 @@ +#!/bin/bash +# Source spread-funcs.sh which, at runtime, contains the real definition of +# NOMATCH and execute it. + +# shellcheck source=tests/lib/spread-funcs.sh +. "$TESTSLIB/spread-funcs.sh" + +NOMATCH "$@" diff -Nru snapd-2.47.1+20.10.1build1/tests/bin/os.query snapd-2.48+21.04/tests/bin/os.query --- snapd-2.47.1+20.10.1build1/tests/bin/os.query 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48+21.04/tests/bin/os.query 2020-11-19 16:51:02.000000000 +0000 @@ -0,0 +1,84 @@ +#!/bin/bash + +show_help() { + echo "usage: is-core" + echo " is-core16" + echo " is-core18" + echo " is-core20" + echo " is-classic" + echo " is-trusty" + echo " is-xenial" + echo " is-bionic" + echo " is-focal" + echo "" + echo "Get general information about the current system" +} + +is_core() { + [[ "$SPREAD_SYSTEM" == ubuntu-core-* ]] +} + +is_core16() { + [[ "$SPREAD_SYSTEM" == ubuntu-core-16-* ]] +} + +is_core18() { + [[ "$SPREAD_SYSTEM" == ubuntu-core-18-* ]] +} + +is_core20() { + [[ "$SPREAD_SYSTEM" == ubuntu-core-20-* ]] +} + +is_classic() { + ! is_core +} + +is_trusty() { + grep -qFx 'ID=ubuntu' /etc/os-release && grep -qFx 'VERSION_ID="14.04"' /etc/os-release +} + +is_xenial() { + grep -qFx 'UBUNTU_CODENAME=xenial' /etc/os-release +} + +is_bionic() { + grep -qFx 'UBUNTU_CODENAME=bionic' /etc/os-release +} + +is_focal() { + grep -qFx 'UBUNTU_CODENAME=focal' /etc/os-release +} + +main() { + if [ $# -eq 0 ]; then + show_help + exit 0 + fi + + local subcommand="$1" + local action= + while [ $# -gt 0 ]; do + case "$1" in + -h|--help) + show_help + exit 0 + ;; + *) + action=$(echo "$subcommand" | tr '-' '_') + shift + break + ;; + esac + done + + if [ -z "$(declare -f "$action")" ]; then + echo "os.query: no such command: $subcommand" >&2 + show_help + exit 1 + fi + + "$action" "$@" +} + +main "$@" diff -Nru snapd-2.47.1+20.10.1build1/tests/bin/tests.backup snapd-2.48+21.04/tests/bin/tests.backup --- snapd-2.47.1+20.10.1build1/tests/bin/tests.backup 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48+21.04/tests/bin/tests.backup 2020-11-19 16:51:02.000000000 +0000 @@ -0,0 +1,69 @@ +#!/bin/bash -e +# Tool used to backup/restore a specific directory +# It is used by the test tool to make sure each test +# leaves the test directory as was initially + +show_help() { + echo "usage: tests.backup prepare [PATH]" + echo " tests.backup restore [PATH]" +} + +cmd_prepare() { + local BACKUP_PATH=$1 + + if [ ! -d "$BACKUP_PATH" ]; then + echo "tests.backup: cannot backup $BACKUP_PATH, not a directory" >&2 + exit 1 + fi + tar cf "${BACKUP_PATH}.tar" "$BACKUP_PATH" +} + +cmd_restore() { + local BACKUP_PATH=$1 + if [ -f "${BACKUP_PATH}.tar" ]; then + # Find all the files in the path $BACKUP_PATH and delete them + # This command deletes also the hidden files + find "${BACKUP_PATH}" -maxdepth 1 -mindepth 1 -exec rm -rf {} \; + tar -C/ -xf "${BACKUP_PATH}.tar" + rm "${BACKUP_PATH}.tar" + else + echo "tests.backup: cannot restore ${BACKUP_PATH}.tar, the file does not exist" >&2 + exit 1 + fi +} + +main() { + if [ $# -eq 0 ]; then + show_help + exit 0 + fi + + while [ $# -gt 0 ]; do + case "$1" in + -h|--help) + show_help + exit + ;; + prepare) + local BACKUP_PATH="${2:-$(pwd)}" + cmd_prepare "$BACKUP_PATH" + exit + ;; + restore) + local BACKUP_PATH="${2:-$(pwd)}" + cmd_restore "$BACKUP_PATH" + exit + ;; + -*) + echo "tests.backup: unknown option $1" >&2 + exit 1 + ;; + *) + echo "tests.backup: unknown command $1" >&2 + exit 1 + ;; + esac + done +} + +main "$@" diff -Nru snapd-2.47.1+20.10.1build1/tests/bin/tests.cleanup snapd-2.48+21.04/tests/bin/tests.cleanup --- snapd-2.47.1+20.10.1build1/tests/bin/tests.cleanup 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/bin/tests.cleanup 2020-11-19 16:51:02.000000000 +0000 @@ -3,7 +3,17 @@ show_help() { echo "usage: tests.cleanup prepare" echo " tests.cleanup defer [args]" + echo " tests.cleanup pop" echo " tests.cleanup restore" + echo + echo "COMMANDS:" + echo " prepare: establishes a cleanup stack" + echo " restore: invokes all cleanup commands in reverse order" + echo " defer: pushes a command onto the cleanup stack" + echo " pop: invoke the most recently deferred command, discarding it" + echo + echo "The defer and pop commands can be to establish temporary" + echo "cleanup handler and remove it, in the case of success" } cmd_prepare() { @@ -17,26 +27,43 @@ cmd_defer() { if [ ! -e defer.sh ]; then - echo "tests.cleanup: cannot defer, must call tests.prepare first" >&2 + echo "tests.cleanup: cannot defer, must call tests.cleanup prepare first" >&2 exit 1 fi echo "$*" >> defer.sh } +run_one_cmd() { + CMD="$1" + set +e + sh -ec "$CMD" + RET=$? + set -e + if [ $RET -ne 0 ]; then + echo "tests.cleanup: deferred command \"$CMD\" failed with exit code $RET" + exit $RET + fi +} + +cmd_pop() { + if [ ! -s defer.sh ]; then + echo "tests.cleanup: cannot pop, cleanup stack is empty" >&2 + exit 1 + fi + head -n-1 defer.sh >defer.sh.pop + trap "mv defer.sh.pop defer.sh" EXIT + tail -n-1 defer.sh | while read -r CMD; do + run_one_cmd "$CMD" + done +} + cmd_restore() { if [ ! -e defer.sh ]; then - echo "tests.cleanup: cannot restore, must call tests.prepare first" >&2 + echo "tests.cleanup: cannot restore, must call tests.cleanup prepare first" >&2 exit 1 fi tac defer.sh | while read -r CMD; do - set +e - sh -ec "$CMD" - RET=$? - set -e - if [ $RET -ne 0 ]; then - echo "tests.cleanup: deferred command \"$CMD\" failed with exit code $RET" - exit $RET - fi + run_one_cmd "$CMD" done rm -f defer.sh } @@ -62,6 +89,11 @@ cmd_defer "$@" exit ;; + pop) + shift + cmd_pop "$@" + exit + ;; restore) shift cmd_restore diff -Nru snapd-2.47.1+20.10.1build1/tests/bin/tests.session snapd-2.48+21.04/tests/bin/tests.session --- snapd-2.47.1+20.10.1build1/tests/bin/tests.session 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/bin/tests.session 2020-11-19 16:51:02.000000000 +0000 @@ -363,8 +363,9 @@ trap - INT TERM QUIT # Kill dbus-monitor that otherwise runs until it notices the pipe is no longer -# connected, which happens after a longer while. -kill $dbus_monitor_pid || true +# connected, which happens after a longer while. Redirect stderr to /dev/null +# to avoid upsetting tests which are sensitive to stderr, e.g. tests/main/document-portal-activation +kill $dbus_monitor_pid 2>/dev/null || true wait $dbus_monitor_pid 2>/dev/null || true wait $awk_pid diff -Nru snapd-2.47.1+20.10.1build1/tests/core/basic20/task.yaml snapd-2.48+21.04/tests/core/basic20/task.yaml --- snapd-2.47.1+20.10.1build1/tests/core/basic20/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/core/basic20/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -29,13 +29,24 @@ #shellcheck source=tests/lib/names.sh . "$TESTSLIB"/names.sh - echo "Ensure extracted kernel.efi exists" - test -e /boot/grub/"$kernel_name"*/kernel.efi - test -e /boot/grub/kernel.efi + if [[ "$SPREAD_SYSTEM" = ubuntu-core-20-64 ]]; then + echo "Ensure extracted kernel.efi exists" + test -e /boot/grub/"$kernel_name"*/kernel.efi - echo "Ensure we are using managed boot assets" - MATCH '# Snapd-Boot-Config-Edition: [0-9]+' < /boot/grub/grub.cfg - MATCH '# Snapd-Boot-Config-Edition: [0-9]+' < /run/mnt/ubuntu-seed/EFI/ubuntu/grub.cfg + echo "Ensure kernel.efi is a symlink" + test -L /boot/grub/kernel.efi + + echo "Ensure we are using managed boot assets" + MATCH '# Snapd-Boot-Config-Edition: [0-9]+' < /boot/grub/grub.cfg + MATCH '# Snapd-Boot-Config-Edition: [0-9]+' < /run/mnt/ubuntu-seed/EFI/ubuntu/grub.cfg + else + echo "Ensure extracted {kernel,initrd}.img exists" + test -e /run/mnt/ubuntu-seed/systems/*/kernel/kernel.img + test -e /run/mnt/ubuntu-seed/systems/*/kernel/initrd.img + fi + + echo "Ensure that model was written to ubuntu-boot" + test -e /run/mnt/ubuntu-boot/device/model # ensure that our the-tool (and thus our snap-bootstrap ran) # for external backend the initramfs is not rebuilt diff -Nru snapd-2.47.1+20.10.1build1/tests/core/config-defaults-once/task.yaml snapd-2.48+21.04/tests/core/config-defaults-once/task.yaml --- snapd-2.47.1+20.10.1build1/tests/core/config-defaults-once/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/core/config-defaults-once/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -51,13 +51,11 @@ . "$TESTSLIB"/core-config.sh #shellcheck source=tests/lib/systemd.sh . "$TESTSLIB"/systemd.sh - #shellcheck source=tests/lib/systems.sh - . "$TESTSLIB"/systems.sh # XXX: this should work once it is possible to install snapd on core SNAP=snapd SERVICES="ssh rsyslog" - if is_core18_system; then + if os.query is-core18; then # a core18 already has a snapd snap, verify whether installation of core # does not break the configuration SNAP=core @@ -108,13 +106,10 @@ exit fi - #shellcheck source=tests/lib/systems.sh - . "$TESTSLIB"/systems.sh - # XXX: this should work once it is possible to install snapd on core SNAP=snapd SERVICES="ssh rsyslog" - if is_core18_system; then + if os.query is-core18; then # a core18 already has a snapd snap, verify whether installation of core # does not break the configuration SNAP=core @@ -142,6 +137,6 @@ test ! -e /etc/ssh/sshd_not_to_be_run # Unmask rsyslog service on core18 - if is_core18_system; then + if os.query is-core18; then systemctl unmask rsyslog fi diff -Nru snapd-2.47.1+20.10.1build1/tests/core/custom-device-reg/task.yaml snapd-2.48+21.04/tests/core/custom-device-reg/task.yaml --- snapd-2.47.1+20.10.1build1/tests/core/custom-device-reg/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/core/custom-device-reg/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -13,10 +13,6 @@ fi #shellcheck source=tests/lib/core-config.sh . "$TESTSLIB"/core-config.sh - #shellcheck source=tests/lib/systemd.sh - . "$TESTSLIB"/systemd.sh - #shellcheck source=tests/lib/systems.sh - . "$TESTSLIB"/systems.sh systemctl stop snapd.service snapd.socket clean_snapd_lib @@ -38,7 +34,8 @@ prepare_testrootorg_store # start fake device svc - systemd_create_and_start_unit fakedevicesvc "$(command -v fakedevicesvc) localhost:11029" + #shellcheck disable=SC2148 + systemd-run --unit fakedevicesvc fakedevicesvc localhost:11029 # kick first boot again systemctl start snapd.service snapd.socket @@ -53,11 +50,8 @@ fi #shellcheck source=tests/lib/core-config.sh . "$TESTSLIB"/core-config.sh - #shellcheck source=tests/lib/systemd.sh - . "$TESTSLIB"/systemd.sh - systemctl stop snapd.service snapd.socket - systemd_stop_and_destroy_unit fakedevicesvc + systemctl stop snapd.service snapd.socket fakedevicesvc clean_snapd_lib # Restore pc snap configuration diff -Nru snapd-2.47.1+20.10.1build1/tests/core/custom-device-reg-extras/task.yaml snapd-2.48+21.04/tests/core/custom-device-reg-extras/task.yaml --- snapd-2.47.1+20.10.1build1/tests/core/custom-device-reg-extras/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/core/custom-device-reg-extras/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -14,10 +14,6 @@ fi #shellcheck source=tests/lib/core-config.sh . "$TESTSLIB"/core-config.sh - #shellcheck source=tests/lib/systemd.sh - . "$TESTSLIB"/systemd.sh - #shellcheck source=tests/lib/systems.sh - . "$TESTSLIB"/systems.sh systemctl stop snapd.service snapd.socket clean_snapd_lib @@ -39,7 +35,8 @@ prepare_testrootorg_store # start fake device svc - systemd_create_and_start_unit fakedevicesvc "$(command -v fakedevicesvc) localhost:11029" + #shellcheck disable=SC2148 + systemd-run --unit fakedevicesvc fakedevicesvc localhost:11029 # kick first boot again systemctl start snapd.service snapd.socket @@ -54,10 +51,7 @@ fi #shellcheck source=tests/lib/core-config.sh . "$TESTSLIB"/core-config.sh - #shellcheck source=tests/lib/systemd.sh - . "$TESTSLIB"/systemd.sh - systemctl stop snapd.service snapd.socket - systemd_stop_and_destroy_unit fakedevicesvc + systemctl stop snapd.service snapd.socket fakedevicesvc clean_snapd_lib # Restore pc snap configuration diff -Nru snapd-2.47.1+20.10.1build1/tests/core/device-reg/task.yaml snapd-2.48+21.04/tests/core/device-reg/task.yaml --- snapd-2.47.1+20.10.1build1/tests/core/device-reg/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/core/device-reg/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -5,8 +5,6 @@ execute: | #shellcheck source=tests/lib/names.sh . "$TESTSLIB"/names.sh - #shellcheck source=tests/lib/systems.sh - . "$TESTSLIB"/systems.sh echo "Wait for first boot to be done" while ! snap changes | grep -q "Done.*Initialize system state"; do sleep 1; done diff -Nru snapd-2.47.1+20.10.1build1/tests/core/enable-disable-units-gpio/task.yaml snapd-2.48+21.04/tests/core/enable-disable-units-gpio/task.yaml --- snapd-2.47.1+20.10.1build1/tests/core/enable-disable-units-gpio/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/core/enable-disable-units-gpio/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -24,12 +24,10 @@ # shellcheck source=tests/lib/systemd.sh . "$TESTSLIB/systemd.sh" echo "Create/enable fake gpio" - systemd_create_and_start_persistent_unit fake-gpio "$TESTSLIB/fakegpio/fake-gpio.py" "[Unit]\\nBefore=snap.core.interface.gpio-100.service\\n[Service]\\nType=notify" + systemd_create_and_start_unit fake-gpio "$TESTSLIB/fakegpio/fake-gpio.py" "[Unit]\\nBefore=snap.core.interface.gpio-100.service\\n[Service]\\nType=notify" echo "Given a snap declaring a plug on gpio is installed" - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh - install_local gpio-consumer + "$TESTSTOOLS"/snaps-state install-local gpio-consumer echo "And the gpio plug is connected" snap connect gpio-consumer:gpio :gpio-pin @@ -45,7 +43,7 @@ # shellcheck source=tests/lib/systemd.sh . "$TESTSLIB/systemd.sh" - system_stop_and_remove_persistent_unit fake-gpio + systemd_stop_and_remove_unit fake-gpio umount /sys/class/gpio || true execute: | diff -Nru snapd-2.47.1+20.10.1build1/tests/core/fsck-on-boot/task.yaml snapd-2.48+21.04/tests/core/fsck-on-boot/task.yaml --- snapd-2.47.1+20.10.1build1/tests/core/fsck-on-boot/task.yaml 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48+21.04/tests/core/fsck-on-boot/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -0,0 +1,87 @@ +summary: the boot base provides essential fsck programs +details: | + Snapd uses vfat on certain essential boot partitions, due to external + requirements imposed by the bootloader architecture. This test verifies that + the boot process is capable of detecting unclean vfat and fixing it before + such file system is mounted. This is an essential property to ensure + longevity of devices that rely on write to vfat to operate. +prepare: | + tests.cleanup prepare +execute: | + unmount_vfat() { + if os.query is-core16; then + # Refer to the core 16 PC gadget for details: + # https://github.com/snapcore/pc-amd64-gadget/blob/16/gadget.yaml + umount /boot/efi + umount /boot/grub + elif os.query is-core18; then + # Refer to the core 18 PC gadget for details: + # https://github.com/snapcore/pc-amd64-gadget/blob/18/gadget.yaml + umount /boot/efi + umount /boot/grub + elif os.query is-core20; then + # TODO:UC20 The property of having to keep a mounted vfat at all time + # is not the most fortunate. Any power loss will result in a dirty + # filesystem. Could ubuntu-seed be re-mounted read-only at some point + # during the start-up process? Power loss on read-only vfat is + # harmless in comparison. + + # The snapd snap from the recovery system will only be mounted if we are + # on the first boot - subsequent boots do not mount it, because we will + # unset RecoverySystem in the modeenv, and it's not necessary for + # seeding anymore + if mountpoint /run/mnt/snapd >/dev/null; then + umount /run/mnt/ubuntu-seed/systems/*/snaps/snapd_*.snap + fi + umount /var/lib/snapd/seed + umount /run/mnt/ubuntu-seed + else + echo "Please adjust the test to support this core system" + false + fi + } + + case "$SPREAD_REBOOT" in + 0) + echo "We can corrupt the boot partition" + # FAT uses a specific byte to effectively indicate that the file system is + # dirty. The precise details as to how this byte is used by each system vary, + # but Linux sets it on a non-read-only mount, and clears it on unmount. We + # can set it manually, verify it when the image is mounted and observe fsck + # clearing it. Note that larger block devices use FAT32 and the offset + # differs. FAT12 and FAT16 uses 37 while FAT32 uses 65. + unmount_vfat + # Use offset 65 as FAT32 kicks in for devices larger than 32MB + printf "\x01" > one + tests.cleanup defer rm -f one + dd if=one of=/dev/sda2 seek=65 bs=1 count=1 conv=notrunc + tests.cleanup pop + + # Reboot to give the early boot process a chance to fix the corruption. + REBOOT + ;; + 1) + echo "On the next boot, we should not see the dirty flag anymore" + # Note that we cannot read the dirty byte from the filesystem as it is + # automatically set by the kernel when vfat is mounted. We must resort + # to observing the kernel ring buffer. Should this message ever change, the + # sister fsck-vfat test does a controlled experiment in mounting a dirty + # vfat, to ensure that we are aware of such changes. + dmesg -c > dmesg-on-boot.log + NOMATCH "Volume was not properly unmounted. Some data may be corrupt. Please run fsck." < dmesg-on-boot.log + + # Unmount vfat again and read the dirty flag manually. The kernel doesn't + # clean the dirty flag on unmount, if it was present on mount. This method is + # less sensitive to kernel log messages being preserved in the early boot + # chain. + unmount_vfat + cat /proc/self/mountinfo >boot1-after-umount.log + dd if=/dev/sda2 of=dirty skip=65 bs=1 count=1 conv=notrunc + test "$(od -t x1 -A n dirty)" = " 00" # NOTE: the leading space is relevant + + # Reboot to restore mount points. + REBOOT + ;; + esac +restore: | + tests.cleanup restore diff -Nru snapd-2.47.1+20.10.1build1/tests/core/fsck-vfat/task.yaml snapd-2.48+21.04/tests/core/fsck-vfat/task.yaml --- snapd-2.47.1+20.10.1build1/tests/core/fsck-vfat/task.yaml 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48+21.04/tests/core/fsck-vfat/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -0,0 +1,67 @@ +summary: the boot base provides essential fsck programs +details: | + Snapd uses vfat on certain essential boot partitions, due to external + requirements imposed by the bootloader architecture. This test verifies + that fsck.vfat is shipped in the image, and that it can correctly clean + the dirty bit that is artificially set by this test. + + A separate test examines how fsck is automatically invoked during the boot + process. This is not verified here. +prepare: | + tests.cleanup prepare +execute: | + echo "Essential fsck programs are in the boot base" + test -n "$(command -v fsck.vfat)" + test -n "$(command -v fsck.fat)" + test -n "$(command -v fsck.ext2)" + test -n "$(command -v fsck.ext3)" + test -n "$(command -v fsck.ext4)" + + echo "mkfs.vfat can create a FAT 12 filesystem" + dd if=/dev/zero of=fat.img bs=1M count=1 + mkfs.vfat fat.img > mkfs.vfat.log + + echo "fsck.fat reports all-ok on such filesystem" + fsck.vfat fat.img > fsck.vfat.vanilla.log + MATCH 'fat.img: 0 files, 0/502 clusters' one + tests.cleanup defer rm -f one + dd if=one of=fat.img seek=37 bs=1 count=1 conv=notrunc + + echo "Mounting dirty FAT generated a kernel message" + mount fat.img /mnt + tests.cleanup defer umount /mnt + # If this ever fails because the kernel log message has changed, please + # adjust the fsck-on-boot tests as well. It relies on absence of this exact + # message. + dmesg -c | MATCH "Volume was not properly unmounted. Some data may be corrupt. Please run fsck." + tests.cleanup pop # unmount + + echo "fsck.fat can fix such corruption" + set +e + fsck.vfat -v -a fat.img > fsck.vfat.dirty.log + retval=$? + set -e + test "$retval" -eq 1 # see fsck.vfat(8) for details + MATCH '0x25: Dirty bit is set. Fs was not properly unmounted and some data may be corrupt.' < fsck.vfat.dirty.log + MATCH 'Automatically removing dirty bit.' < fsck.vfat.dirty.log + + echo "fsck.fat reports the file system as clean" + fsck.vfat -v -a fat.img > fsck.vfat.cleaned.log + not MATCH '0x25: Dirty bit is set. Fs was not properly unmounted and some data may be corrupt.' < fsck.vfat.cleaned.log + + echo "Cleaned FAT mounts without warnings" + mount fat.img /mnt + tests.cleanup defer umount /mnt + dmesg -c | not MATCH "Volume was not properly unmounted. Some data may be corrupt. Please run fsck." + tests.cleanup pop # unmount +restore: | + tests.cleanup restore diff -Nru snapd-2.47.1+20.10.1build1/tests/core/gadget-config-defaults/task.yaml snapd-2.48+21.04/tests/core/gadget-config-defaults/task.yaml --- snapd-2.47.1+20.10.1build1/tests/core/gadget-config-defaults/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/core/gadget-config-defaults/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -24,10 +24,8 @@ . "$TESTSLIB"/core-config.sh #shellcheck source=tests/lib/systemd.sh . "$TESTSLIB"/systemd.sh - #shellcheck source=tests/lib/systems.sh - . "$TESTSLIB"/systems.sh - if [ "$SERVICE" = "rsyslog" ] && is_core18_system; then + if [ "$SERVICE" = "rsyslog" ] && os.query is-core18; then echo "The service to test does not exist in the core18 system, skipping..." touch "${SERVICE}.skip" exit @@ -49,7 +47,7 @@ # test-snapd-with-configure TEST_SNAP_ID=jHxWQxtGqu7tHwiq7F8Ojk5qazcEeslT - if is_core18_system; then + if os.query is-core18; then # test-snapd-with-configure-core18 TEST_SNAP_ID=jHxWQxtGqu7tHwiq7F8Ojk5qazcEeslT fi @@ -57,7 +55,7 @@ # test-snapd-with-configure TEST_SNAP_ID=aLcJorEJZgJNUGL2GMb3WR9SoVyHUNAd - if is_core18_system; then + if os.query is-core18; then # test-snapd-with-configure-core18 TEST_SNAP_ID=BzMG26hwO2ccNBzV5BxK4DZgulJ2AXsa fi @@ -100,8 +98,6 @@ . "$TESTSLIB"/core-config.sh #shellcheck source=tests/lib/systemd.sh . "$TESTSLIB"/systemd.sh - #shellcheck source=tests/lib/systems.sh - . "$TESTSLIB"/systems.sh SUFFIX="$(get_test_snap_suffix)" @@ -156,8 +152,6 @@ fi #shellcheck source=tests/lib/core-config.sh . "$TESTSLIB"/core-config.sh - #shellcheck source=tests/lib/systems.sh - . "$TESTSLIB"/systems.sh SUFFIX="$(get_test_snap_suffix)" diff -Nru snapd-2.47.1+20.10.1build1/tests/core/gadget-config-defaults-vitality/task.yaml snapd-2.48+21.04/tests/core/gadget-config-defaults-vitality/task.yaml --- snapd-2.47.1+20.10.1build1/tests/core/gadget-config-defaults-vitality/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/core/gadget-config-defaults-vitality/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -19,8 +19,6 @@ . "$TESTSLIB"/core-config.sh #shellcheck source=tests/lib/systemd.sh . "$TESTSLIB"/systemd.sh - #shellcheck source=tests/lib/systems.sh - . "$TESTSLIB"/systems.sh # Stop snapd and remove existing state, modify and repack gadget # snap and provide developer assertions in order to force first @@ -77,8 +75,6 @@ fi #shellcheck source=tests/lib/core-config.sh . "$TESTSLIB"/core-config.sh - #shellcheck source=tests/lib/systems.sh - . "$TESTSLIB"/systems.sh SUFFIX="$(get_test_snap_suffix)" @@ -122,8 +118,6 @@ fi #shellcheck source=tests/lib/core-config.sh . "$TESTSLIB"/core-config.sh - #shellcheck source=tests/lib/systems.sh - . "$TESTSLIB"/systems.sh SUFFIX="$(get_test_snap_suffix)" diff -Nru snapd-2.47.1+20.10.1build1/tests/core/gadget-update-pc/task.yaml snapd-2.48+21.04/tests/core/gadget-update-pc/task.yaml --- snapd-2.47.1+20.10.1build1/tests/core/gadget-update-pc/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/core/gadget-update-pc/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -35,9 +35,6 @@ . "$TESTSLIB"/store.sh setup_fake_store "$BLOB_DIR" - #shellcheck source=tests/lib/systems.sh - . "$TESTSLIB"/systems.sh - cp /var/lib/snapd/snaps/pc_*.snap gadget.snap unsquashfs -d pc-snap gadget.snap @@ -59,7 +56,7 @@ cp pc-snap/meta/gadget.yaml gadget.yaml.orig system_seed="" - if is_core20_system ; then + if os.query is-core20 ; then system_seed="--system-seed" fi @@ -68,7 +65,7 @@ echo 'this is foo-x2' > foo-x2.img cp foo-x2.img pc-snap/foo.img echo 'this is foo.cfg' > pc-snap/foo.cfg - if is_core20_system; then + if os.query is-core20; then echo 'this is foo-seed.cfg' > pc-snap/foo-seed.cfg fi sed -i -e 's/^version: \(.*\)-1/version: \1-2/' pc-snap/meta/snap.yaml @@ -82,7 +79,7 @@ echo 'this is updated foo-x3' > foo-x3.img cp foo-x3.img pc-snap/foo.img echo 'this is updated foo.cfg' > pc-snap/foo.cfg - if is_core20_system; then + if os.query is-core20; then echo 'this is updated foo-seed.cfg' > pc-snap/foo-seed.cfg fi echo 'this is bar.cfg' > pc-snap/bar.cfg @@ -134,15 +131,13 @@ #shellcheck source=tests/lib/store.sh . "$TESTSLIB"/store.sh - #shellcheck source=tests/lib/systems.sh - . "$TESTSLIB"/systems.sh # XXX: the test hardcodes a bunch of locations # - 'BIOS Boot' and 'EFI System' are modified during the update # - 'EFI System' is mounted at /boot/efi bootdir=/boot/efi - if is_core20_system; then + if os.query is-core20; then # /boot/efi is not mounted on UC20, so use the /run/mnt hierarchy bootdir=/run/mnt/ubuntu-boot fi @@ -173,7 +168,7 @@ dd if='/dev/disk/by-partlabel/BIOS\x20Boot' skip="$szimg" bs=1 count="$szfoo" of=foo-written.img test "$(cat foo-written.img)" = 'this is foo-x2' - if is_core20_system; then + if os.query is-core20; then # a filesystem structure entry was copied to the right place test "$(cat /run/mnt/ubuntu-seed/foo-seed.cfg)" = 'this is foo-seed.cfg' @@ -210,7 +205,7 @@ dd if='/dev/disk/by-partlabel/BIOS\x20Boot' skip="$szimg" bs=1 count="$szfoo" of=foo-updated-written.img test "$(cat foo-updated-written.img)" = 'this is updated foo-x3' - if is_core20_system; then + if os.query is-core20; then # a filesystem structure entry was copied to the right place test "$(cat /run/mnt/ubuntu-seed/foo-seed.cfg)" = 'this is updated foo-seed.cfg' diff -Nru snapd-2.47.1+20.10.1build1/tests/core/iio/task.yaml snapd-2.48+21.04/tests/core/iio/task.yaml --- snapd-2.47.1+20.10.1build1/tests/core/iio/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/core/iio/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -22,9 +22,7 @@ echo "iio-0" > /dev/iio:device0 echo "Given a snap declaring a plug on iio is installed" - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh - install_local iio-consumer + "$TESTSTOOLS"/snaps-state install-local iio-consumer echo "And the iio plug is connected" snap connect iio-consumer:iio core:iio0 diff -Nru snapd-2.47.1+20.10.1build1/tests/core/kernel-ver/task.yaml snapd-2.48+21.04/tests/core/kernel-ver/task.yaml --- snapd-2.47.1+20.10.1build1/tests/core/kernel-ver/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/core/kernel-ver/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -4,15 +4,12 @@ systems: [ubuntu-core-*-64] execute: | - # shellcheck source=tests/lib/systems.sh - . "$TESTSLIB/systems.sh" - echo "Check kernel version" - if is_core16_system; then + if os.query is-core16; then VER="^4.4" - elif is_core18_system; then + elif os.query is-core18; then VER="^4.15" - elif is_core20_system; then + elif os.query is-core20; then VER="^5.4" fi uname -r | MATCH $VER diff -Nru snapd-2.47.1+20.10.1build1/tests/core/netplan/task.yaml snapd-2.48+21.04/tests/core/netplan/task.yaml --- snapd-2.47.1+20.10.1build1/tests/core/netplan/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/core/netplan/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -15,9 +15,6 @@ snap install test-snapd-netplan-apply --edge execute: | - # shellcheck source=tests/lib/systems.sh - . "$TESTSLIB/systems.sh" - echo "The interface is disconnected by default" snap connections test-snapd-netplan-apply | MATCH 'network-setup-control +test-snapd-netplan-apply:network-setup-control +- +-' @@ -54,7 +51,7 @@ echo "Ensure that the number of network restarts is greater after netplan apply was run" [ "$stopped_after" -gt "$stopped_before" ] && [ "$started_after" -gt "$started_before" ] - if is_core16_system; then + if os.query is-core16; then echo "Skipping Ubuntu Core 16 which does not have Info D-Bus method" exit 0 fi diff -Nru snapd-2.47.1+20.10.1build1/tests/core/os-release/task.yaml snapd-2.48+21.04/tests/core/os-release/task.yaml --- snapd-2.47.1+20.10.1build1/tests/core/os-release/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/core/os-release/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -4,14 +4,11 @@ cat /etc/lsb-release execute: | - #shellcheck source=tests/lib/systems.sh - . "$TESTSLIB"/systems.sh - - if is_core16_system; then + if os.query is-core16; then MATCH "DISTRIB_RELEASE=16" < /etc/lsb-release - elif is_core18_system; then + elif os.query is-core18; then MATCH "DISTRIB_RELEASE=18" < /etc/lsb-release - elif is_core20_system; then + elif os.query is-core20; then MATCH "DISTRIB_RELEASE=20" < /etc/lsb-release else echo "Unknown Ubuntu Core system!" diff -Nru snapd-2.47.1+20.10.1build1/tests/core/reboot/task.yaml snapd-2.48+21.04/tests/core/reboot/task.yaml --- snapd-2.47.1+20.10.1build1/tests/core/reboot/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/core/reboot/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -4,10 +4,8 @@ priority: 100 prepare: | - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh - install_local test-snapd-tools - install_local test-snapd-service + "$TESTSTOOLS"/snaps-state install-local test-snapd-tools + "$TESTSTOOLS"/snaps-state install-local test-snapd-service execute: | echo "Ensure snaps are (still) there." diff -Nru snapd-2.47.1+20.10.1build1/tests/core/remodel/task.yaml snapd-2.48+21.04/tests/core/remodel/task.yaml --- snapd-2.47.1+20.10.1build1/tests/core/remodel/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/core/remodel/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -13,8 +13,6 @@ . "$TESTSLIB"/core-config.sh #shellcheck source=tests/lib/systemd.sh . "$TESTSLIB"/systemd.sh - #shellcheck source=tests/lib/systems.sh - . "$TESTSLIB"/systems.sh systemctl stop snapd.service snapd.socket clean_snapd_lib diff -Nru snapd-2.47.1+20.10.1build1/tests/core/remove/task.yaml snapd-2.48+21.04/tests/core/remove/task.yaml --- snapd-2.47.1+20.10.1build1/tests/core/remove/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/core/remove/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -4,18 +4,15 @@ systems: [-ubuntu-core-16-*] execute: | - # shellcheck source=tests/lib/systems.sh - . "$TESTSLIB/systems.sh" - echo "Ensure snapd cannot be removed" if snap remove --purge snapd; then echo "The snapd snap should not be removable" exit 1 fi - if is_core18_system; then + if os.query is-core18; then base=core18 - elif is_core20_system; then + elif os.query is-core20; then base=core20 fi echo "Ensure $base cannot be removed" diff -Nru snapd-2.47.1+20.10.1build1/tests/core/seed-base-symlinks/task.yaml snapd-2.48+21.04/tests/core/seed-base-symlinks/task.yaml --- snapd-2.47.1+20.10.1build1/tests/core/seed-base-symlinks/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/core/seed-base-symlinks/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -10,7 +10,7 @@ . "$TESTSLIB/systems.sh" TARGET_SNAP=core - if is_core18_system; then + if os.query is-core18; then TARGET_SNAP=core18 fi diff -Nru snapd-2.47.1+20.10.1build1/tests/core/snap-auto-mount/task.yaml snapd-2.48+21.04/tests/core/snap-auto-mount/task.yaml --- snapd-2.47.1+20.10.1build1/tests/core/snap-auto-mount/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/core/snap-auto-mount/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -7,8 +7,6 @@ echo "This test needs test keys to be trusted" exit fi - # shellcheck source=tests/lib/systems.sh - . "$TESTSLIB/systems.sh" echo "Install dmsetup" snap install --devmode --edge dmsetup @@ -26,7 +24,7 @@ # We use different filesystems to cover both: fat and ext. fat is the most # common fs used and we also use ext3 because fat is not available on ubuntu core 18 - if is_core18_system; then + if os.query is-core18; then mkfs.ext3 /dev/ram0 else mkfs.vfat /dev/ram0 diff -Nru snapd-2.47.1+20.10.1build1/tests/core/snap-debug-bootvars/task.yaml snapd-2.48+21.04/tests/core/snap-debug-bootvars/task.yaml --- snapd-2.47.1+20.10.1build1/tests/core/snap-debug-bootvars/task.yaml 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48+21.04/tests/core/snap-debug-bootvars/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -0,0 +1,38 @@ +summary: Ensure `snap debug bootvars` command works + +debug: | + cat default.out || true + cat uc20.out || true + cat run.out || true + cat recovery.out || true + +restore: | + rm -f default.out uc20.out run.out recovery.out + +execute: | + # does not outright fail + snap debug boot-vars > default.out + + if os.query is-core20; then + # boot-vars default output is for the run mode bootloader, make sure its + # output looks sane (though we don't expect any of the variables to be + # set) + MATCH 'kernel_status=$' < default.out + + snap debug boot-vars --uc20 > uc20.out + snap debug boot-vars --root-dir /run/mnt/ubuntu-boot > run.out + # the no-parameters output and explicit --uc20 should be the same + diff -up default.out uc20.out + # default shows a run mode bootloader variables, so the output shall be + # identical again + diff -up default.out run.out + + # try the recovery bootloader now + snap debug boot-vars --root-dir /run/mnt/ubuntu-seed > recovery.out + MATCH 'snapd_recovery_mode=run' < recovery.out + else + MATCH 'snap_core=core.*\.snap' < default.out + MATCH 'snap_kernel=pc-kernel.*\.snap' < default.out + # relevant snaps are not being updated, so snap_mode is unset + MATCH 'snap_mode=$' < default.out + fi diff -Nru snapd-2.47.1+20.10.1build1/tests/core/snapd-failover/task.yaml snapd-2.48+21.04/tests/core/snapd-failover/task.yaml --- snapd-2.47.1+20.10.1build1/tests/core/snapd-failover/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/core/snapd-failover/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -1,9 +1,6 @@ summary: Check that snapd failure handling works prepare: | - # shellcheck source=tests/lib/systems.sh - . "$TESTSLIB/systems.sh" - # shellcheck source=tests/lib/snaps.sh . "$TESTSLIB/snaps.sh" @@ -11,7 +8,7 @@ # test because it by default uses the core snap # there is a different test for the transition between the core snap and the # snapd snap - if is_core16_system; then + if os.query is-core16; then # rebuild the core snap into the snapd snap and install it repack_installed_core_snap_into_snapd_snap snap install --dangerous snapd-from-core.snap @@ -21,13 +18,10 @@ ls -l /snap/snapd/ debug: | - # shellcheck source=tests/lib/systems.sh - . "$TESTSLIB/systems.sh" - # dump failure data journalctl -u snapd.failure.service journalctl -u snapd.socket || true - if is_core16_system; then + if os.query is-core16; then # might be useful to know what's up with the core snap too on uc16 ls -l /snap/core/ fi @@ -39,10 +33,7 @@ /snap/snapd/x1/usr/bin/snap debug state --change="$(/snap/snapd/x1/usr/bin/snap debug state /var/lib/snapd/state.json|tail -n1|awk '{print $1}')" /var/lib/snapd/state.json || true restore: | - # shellcheck source=tests/lib/systems.sh - . "$TESTSLIB/systems.sh" - - if is_core16_system; then + if os.query is-core16; then echo "ensuring we reverted fully to core snap system" not test -d /snap/snapd # cleanup the snapd-from-core snap we built @@ -56,9 +47,6 @@ fi execute: | - # shellcheck source=tests/lib/systems.sh - . "$TESTSLIB/systems.sh" - if [ "$SPREAD_REBOOT" = 0 ]; then echo "Testing failover handling of the snapd snap" @@ -141,7 +129,7 @@ # do that restoration here # TODO: move this to restore section when spread is fixed # see https://github.com/snapcore/spread/pull/85 - if is_core16_system; then + if os.query is-core16; then echo "Manually uninstall the snapd snap on UC16" systemctl stop snapd.service snapd.socket snapd.autoimport.service snapd.snap-repair.service snapd.snap-repair.timer umount "/snap/snapd/$(readlink /snap/snapd/current)" @@ -163,7 +151,7 @@ else # "$SPREAD_REBOOT" != 0 # technically this check is unnecessary because we only reboot during # the test's execution on uc16, but just be extra safe - if is_core16_system; then + if os.query is-core16; then # now remove the snapd snap since we booted with the core snap snap list # this only succeeds because we reverted back to having the core diff -Nru snapd-2.47.1+20.10.1build1/tests/core/snap-set-core-config/task.yaml snapd-2.48+21.04/tests/core/snap-set-core-config/task.yaml --- snapd-2.47.1+20.10.1build1/tests/core/snap-set-core-config/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/core/snap-set-core-config/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -10,11 +10,10 @@ if [ $rc = 4 ]; then # systemctl(1) exit code 4: no such unit - #shellcheck source=tests/lib/systemd.sh - . "$TESTSLIB"/systemd.sh - # start fake rsyslog service - systemd_create_and_start_unit rsyslog "/bin/sleep 2h" + printf '[Unit]\nDescription=test %s\n[Service]\nType=simple\nExecStart=%s\n' "${SPREAD_JOB:-unknown}" "/bin/sleep 2h" > /run/systemd/system/rsyslog.service + systemctl daemon-reload + systemctl start rsyslog # create a flag to indicate the ryslog service is fake touch rsyslog.fake @@ -22,9 +21,9 @@ restore: | if [ -f rsyslog.fake ]; then - #shellcheck source=tests/lib/systemd.sh - . "$TESTSLIB"/systemd.sh - systemd_stop_and_destroy_unit rsyslog + systemctl stop rsyslog + rm /run/systemd/system/rsyslog.service + systemctl daemon-reload else systemctl enable rsyslog.service systemctl start rsyslog.service diff -Nru snapd-2.47.1+20.10.1build1/tests/core/system-snap-refresh/task.yaml snapd-2.48+21.04/tests/core/system-snap-refresh/task.yaml --- snapd-2.47.1+20.10.1build1/tests/core/system-snap-refresh/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/core/system-snap-refresh/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -8,12 +8,10 @@ systems: [ubuntu-core-*] restore: | - #shellcheck source=tests/lib/systems.sh - . "$TESTSLIB"/systems.sh TARGET_SNAP_NAME=core - if is_core18_system; then + if os.query is-core18; then TARGET_SNAP_NAME=core18 - elif is_core20_system; then + elif os.query is-core20; then TARGET_SNAP_NAME=core20 fi @@ -23,13 +21,10 @@ fi execute: | - #shellcheck source=tests/lib/systems.sh - . "$TESTSLIB"/systems.sh - TARGET_SNAP_NAME=core - if is_core18_system; then + if os.query is-core18; then TARGET_SNAP_NAME=core18 - elif is_core20_system; then + elif os.query is-core20; then TARGET_SNAP_NAME=core20 fi diff -Nru snapd-2.47.1+20.10.1build1/tests/core/upgrade/task.yaml snapd-2.48+21.04/tests/core/upgrade/task.yaml --- snapd-2.47.1+20.10.1build1/tests/core/upgrade/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/core/upgrade/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -27,11 +27,8 @@ snap remove core --revision=x3 prepare: | - #shellcheck source=tests/lib/systems.sh - . "$TESTSLIB"/systems.sh - TARGET_SNAP=core - if is_core18_system; then + if os.query is-core18; then TARGET_SNAP=core18 fi @@ -41,11 +38,9 @@ execute: | #shellcheck source=tests/lib/boot.sh . "$TESTSLIB"/boot.sh - #shellcheck source=tests/lib/systems.sh - . "$TESTSLIB"/systems.sh TARGET_SNAP=core - if is_core18_system; then + if os.query is-core18; then TARGET_SNAP=core18 fi diff -Nru snapd-2.47.1+20.10.1build1/tests/lib/cla_check.py snapd-2.48+21.04/tests/lib/cla_check.py --- snapd-2.47.1+20.10.1build1/tests/lib/cla_check.py 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/lib/cla_check.py 2020-11-19 16:51:02.000000000 +0000 @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # -*- coding: utf-8 -*- from __future__ import print_function @@ -14,9 +14,11 @@ try: from launchpadlib.launchpad import Launchpad except ImportError: - sys.exit("Install launchpadlib: sudo apt install python-launchpadlib") + sys.exit( + "Install launchpadlib: sudo apt install python-launchpadlib python3-launchpadlib" + ) -shortlog_email_rx = re.compile("^\s*\d+\s+.*<(\S+)>$", re.M) +shortlog_email_rx = re.compile(r"^\s*\d+\s+.*<(\S+)>$", re.M) is_travis = os.getenv("TRAVIS", "") == "true" @@ -24,7 +26,7 @@ def get_emails_for_range(r): - output = check_output(["git", "shortlog", "-se", r]) + output = check_output(["git", "shortlog", "-se", r]).decode("utf-8") return set(m.group(1) for m in shortlog_email_rx.finditer(output)) @@ -42,15 +44,16 @@ clear = "" if is_travis: - fold_start = 'travis_fold:start:{{tag}}\r{}{}{{message}}{}'.format( - clear, yellow, reset) - fold_end = 'travis_fold:end:{{tag}}\r{}'.format(clear) + fold_start = "travis_fold:start:{{tag}}\r{}{}{{message}}{}".format( + clear, yellow, reset + ) + fold_end = "travis_fold:end:{{tag}}\r{}".format(clear) elif is_github_actions: - fold_start = '::group::{message}' - fold_end = '::endgroup::' + fold_start = "::group::{message}" + fold_end = "::endgroup::" else: - fold_start = '{}{{message}}{}'.format(yellow, reset) - fold_end = '' + fold_start = "{}{{message}}{}".format(yellow, reset) + fold_end = "" def static_email_check(email, master_emails, width): @@ -114,11 +117,12 @@ def main(): - parser = argparse.ArgumentParser(description='') - parser.add_argument('commit_range', - help='Commit range in format ..') + parser = argparse.ArgumentParser(description="") + parser.add_argument( + "commit_range", help="Commit range in format .." + ) opts = parser.parse_args() - master, proposed = opts.commit_range.split('..') + master, _ = opts.commit_range.split("..") print_checkout_info(opts.commit_range) emails = get_emails_for_range(opts.commit_range) if len(emails) == 0: diff -Nru snapd-2.47.1+20.10.1build1/tests/lib/cloud-init-seeds/attacker-user/meta-data snapd-2.48+21.04/tests/lib/cloud-init-seeds/attacker-user/meta-data --- snapd-2.47.1+20.10.1build1/tests/lib/cloud-init-seeds/attacker-user/meta-data 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48+21.04/tests/lib/cloud-init-seeds/attacker-user/meta-data 2020-11-19 16:51:02.000000000 +0000 @@ -0,0 +1,2 @@ +instance-id: iid-local02 +local-hostname: cloudimg diff -Nru snapd-2.47.1+20.10.1build1/tests/lib/cloud-init-seeds/attacker-user/user-data snapd-2.48+21.04/tests/lib/cloud-init-seeds/attacker-user/user-data --- snapd-2.47.1+20.10.1build1/tests/lib/cloud-init-seeds/attacker-user/user-data 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48+21.04/tests/lib/cloud-init-seeds/attacker-user/user-data 2020-11-19 16:51:02.000000000 +0000 @@ -0,0 +1,7 @@ +#cloud-config +users: + - default + - name: attacker-user + sudo: ALL=(ALL) NOPASSWD:ALL + lock_passwd: false + passwd: $6$rounds=4096$PCrfo.ggdf4ubP$REjyaoY2tUWH2vjFJjvLs3rDxVTszGR9P7mhH9sHb2MsELfc53uV/v15jDDOJU/9WInfjjTKJPlD5URhX5Mix0 diff -Nru snapd-2.47.1+20.10.1build1/tests/lib/cloud-init-seeds/normal-user/meta-data snapd-2.48+21.04/tests/lib/cloud-init-seeds/normal-user/meta-data --- snapd-2.47.1+20.10.1build1/tests/lib/cloud-init-seeds/normal-user/meta-data 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48+21.04/tests/lib/cloud-init-seeds/normal-user/meta-data 2020-11-19 16:51:02.000000000 +0000 @@ -0,0 +1,2 @@ +instance-id: iid-local01 +local-hostname: cloudimg diff -Nru snapd-2.47.1+20.10.1build1/tests/lib/cloud-init-seeds/normal-user/user-data snapd-2.48+21.04/tests/lib/cloud-init-seeds/normal-user/user-data --- snapd-2.47.1+20.10.1build1/tests/lib/cloud-init-seeds/normal-user/user-data 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48+21.04/tests/lib/cloud-init-seeds/normal-user/user-data 2020-11-19 16:51:02.000000000 +0000 @@ -0,0 +1,7 @@ +#cloud-config +users: + - default + - name: normal-user + sudo: ALL=(ALL) NOPASSWD:ALL + lock_passwd: false + passwd: $6$rounds=4096$PCrfo.ggdf4ubP$REjyaoY2tUWH2vjFJjvLs3rDxVTszGR9P7mhH9sHb2MsELfc53uV/v15jDDOJU/9WInfjjTKJPlD5URhX5Mix0 diff -Nru snapd-2.47.1+20.10.1build1/tests/lib/core-config.sh snapd-2.48+21.04/tests/lib/core-config.sh --- snapd-2.47.1+20.10.1build1/tests/lib/core-config.sh 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/lib/core-config.sh 2020-11-19 16:51:02.000000000 +0000 @@ -1,8 +1,5 @@ #!/bin/bash -#shellcheck source=tests/lib/systems.sh -. "$TESTSLIB"/systems.sh - clean_snapd_lib() { rm -rf /var/lib/snapd/assertions/* rm -rf /var/lib/snapd/device @@ -81,9 +78,9 @@ get_test_model(){ local MODEL_NAME=$1 - if is_core18_system; then + if os.query is-core18; then echo "${MODEL_NAME}-18.model" - elif is_core20_system; then + elif os.query is-core20; then echo "${MODEL_NAME}-20.model" else echo "${MODEL_NAME}.model" @@ -91,9 +88,9 @@ } get_test_snap_suffix(){ - if is_core18_system; then + if os.query is-core18; then echo "-core18" - elif is_core20_system; then + elif os.query is-core20; then echo "-core20" fi } diff -Nru snapd-2.47.1+20.10.1build1/tests/lib/ensure_ubuntu_save.py snapd-2.48+21.04/tests/lib/ensure_ubuntu_save.py --- snapd-2.47.1+20.10.1build1/tests/lib/ensure_ubuntu_save.py 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48+21.04/tests/lib/ensure_ubuntu_save.py 2020-11-19 16:51:02.000000000 +0000 @@ -0,0 +1,50 @@ +#!/usr/bin/env python3 + +import argparse +import logging +import yaml +import sys + + +def parse_arguments(): + parser = argparse.ArgumentParser(description="ensure pc gadget has ubuntu-save") + parser.add_argument( + "gadgetyaml", type=argparse.FileType("r"), help="path to gadget.yaml input file" + ) + return parser.parse_args() + + +def main(opts): + gadget_yaml = yaml.safe_load(opts.gadgetyaml) + + structs = gadget_yaml["volumes"]["pc"]["structure"] + save_idx = -1 + for idx, s in enumerate(structs): + role = s.get("role", "") + if role == "system-save": + logging.info("system-save structure already present") + # already has ubuntu-save + return + if role == "system-data": + # ubuntu-save precedes ubuntu-data + save_idx = idx + break + if save_idx == -1: + raise RuntimeError("cannot find a suitable place to insert ubuntu-save") + + ubuntu_save = { + "name": "ubuntu-save", + "role": "system-save", + # TODO:UC20: update when pc-amd64-gadget changes + "size": "16M", + "filesystem": "ext4", + "type": "83,0FC63DAF-8483-4772-8E79-3D69D8477DE4", + } + structs.insert(save_idx, ubuntu_save) + yaml.dump(gadget_yaml, stream=sys.stdout, indent=2, default_flow_style=False) + + +if __name__ == "__main__": + logging.basicConfig(level=logging.DEBUG) + opts = parse_arguments() + main(opts) diff -Nru snapd-2.47.1+20.10.1build1/tests/lib/nested.sh snapd-2.48+21.04/tests/lib/nested.sh --- snapd-2.47.1+20.10.1build1/tests/lib/nested.sh 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/lib/nested.sh 2020-11-19 16:51:02.000000000 +0000 @@ -3,9 +3,6 @@ # shellcheck source=tests/lib/systemd.sh . "$TESTSLIB"/systemd.sh -# shellcheck source=tests/lib/systems.sh -. "$TESTSLIB"/systems.sh - # shellcheck source=tests/lib/store.sh . "$TESTSLIB"/store.sh @@ -45,11 +42,10 @@ nested_wait_for_reboot() { local initial_boot_id="$1" - local retry wait last_boot_id - retry=150 - wait=5 + local last_boot_id="$initial_boot_id" + local retry=150 + local wait=5 - last_boot_id="" while [ $retry -ge 0 ]; do retry=$(( retry - 1 )) # The get_boot_id could fail because the connection is broken due to the reboot @@ -63,6 +59,20 @@ [ "$last_boot_id" != "$initial_boot_id" ] } +nested_uc20_transition_to_system_mode() { + local recovery_system="$1" + local mode="$2" + local current_boot_id + current_boot_id=$(nested_get_boot_id) + nested_exec "sudo snap reboot --$mode $recovery_system" + nested_wait_for_reboot "$current_boot_id" + + # verify we are now in the requested mode + if ! nested_exec "cat /proc/cmdline" | MATCH "snapd_recovery_mode=$mode"; then + return 1 + fi +} + nested_retry_while_success() { local retry="$1" local wait="$2" @@ -254,15 +264,15 @@ } nested_is_core_20_system() { - is_focal_system + os.query is-focal } nested_is_core_18_system() { - is_bionic_system + os.query is-bionic } nested_is_core_16_system() { - is_xenial_system + os.query is-xenial } nested_refresh_to_new_core() { @@ -302,19 +312,18 @@ } nested_secboot_sign_file() { - local DIR="$1" + local FILE="$1" local KEY="$2" local CERT="$3" - local FILE="$4" - sbattach --remove "$DIR"/"$FILE" - sbsign --key "$KEY" --cert "$CERT" --output "$DIR"/"$FILE" "$DIR"/"$FILE" + sbattach --remove "$FILE" + sbsign --key "$KEY" --cert "$CERT" --output "$FILE" "$FILE" } nested_secboot_sign_gadget() { local GADGET_DIR="$1" local KEY="$2" local CERT="$3" - nested_secboot_sign_file "$GADGET_DIR" "$KEY" "$CERT" "shim.efi.signed" + nested_secboot_sign_file "$GADGET_DIR/shim.efi.signed" "$KEY" "$CERT" } nested_prepare_env() { @@ -413,6 +422,16 @@ esac } +nested_ensure_ubuntu_save() { + local GADGET_DIR="$1" + "$TESTSLIB"/ensure_ubuntu_save.py "$GADGET_DIR"/meta/gadget.yaml > /tmp/gadget-with-save.yaml + if [ "$(cat /tmp/gadget-with-save.yaml)" != "" ]; then + mv /tmp/gadget-with-save.yaml "$GADGET_DIR"/meta/gadget.yaml + else + rm -f /tmp/gadget-with-save.yaml + fi +} + nested_create_core_vm() { # shellcheck source=tests/lib/prepare.sh . "$TESTSLIB"/prepare.sh @@ -449,6 +468,16 @@ repack_snapd_deb_into_snapd_snap "$NESTED_ASSETS_DIR" EXTRA_FUNDAMENTAL="$EXTRA_FUNDAMENTAL --snap $NESTED_ASSETS_DIR/snapd-from-deb.snap" + snap download --channel="$CORE_CHANNEL" --basename=core18 core18 + repack_core_snap_with_tweaks "core18.snap" "new-core18.snap" + EXTRA_FUNDAMENTAL="$EXTRA_FUNDAMENTAL --snap $PWD/new-core18.snap" + + repack_core_snap_with_tweaks "core18.snap" "new-core18.snap" + + if [ "$NESTED_SIGN_SNAPS_FAKESTORE" = "true" ]; then + make_snap_installable_with_id "$NESTED_FAKESTORE_BLOB_DIR" "$PWD/new-core18.snap" "CSO04Jhav2yK0uz97cr0ipQRyqg0qQL6" + fi + elif nested_is_core_20_system; then snap download --basename=pc-kernel --channel="20/edge" pc-kernel uc20_build_initramfs_kernel_snap "$PWD/pc-kernel.snap" "$NESTED_ASSETS_DIR" @@ -482,6 +511,10 @@ snap download --basename=pc --channel="20/edge" pc unsquashfs -d pc-gadget pc.snap nested_secboot_sign_gadget pc-gadget "$SNAKEOIL_KEY" "$SNAKEOIL_CERT" + # TODO:UC20: until https://github.com/snapcore/pc-amd64-gadget/pull/51/ + # lands there is no ubuntu-save in the gadget, make sure we have one + nested_ensure_ubuntu_save pc-gadget + # also make logging persistent for easier debugging of # test failures, otherwise we have no way to see what # happened during a failed nested VM boot where we @@ -515,7 +548,7 @@ # which channel? snap download --channel="$CORE_CHANNEL" --basename=core20 core20 - repack_core20_snap_with_tweaks "core20.snap" "new-core20.snap" + repack_core_snap_with_tweaks "core20.snap" "new-core20.snap" EXTRA_FUNDAMENTAL="$EXTRA_FUNDAMENTAL --snap $PWD/new-core20.snap" # sign the snapd snap with fakestore if requested @@ -683,11 +716,44 @@ kpartx -d "$IMAGE" } +nested_save_serial_log() { + if [ -f "${NESTED_LOGS_DIR}/serial.log" ]; then + for i in $(seq 1 9); do + if [ ! -f "${NESTED_LOGS_DIR}/serial.log.${i}" ]; then + cp "${NESTED_LOGS_DIR}/serial.log" "${NESTED_LOGS_DIR}/serial.log.${i}" + break + fi + done + # make sure we start with clean log file + echo > "${NESTED_LOGS_DIR}/serial.log" + fi +} + +nested_print_serial_log() { + if [ -f "${NESTED_LOGS_DIR}/serial.log.1" ]; then + # here we disable SC2045 because previously it is checked there is at least + # 1 file which matches. In this case ls command is needed because it is important + # to get the list in reverse order. + # shellcheck disable=SC2045 + for logfile in $(ls "${NESTED_LOGS_DIR}"/serial.log.*); do + cat "$logfile" + done + fi + if [ -f "${NESTED_LOGS_DIR}/serial.log" ]; then + cat "${NESTED_LOGS_DIR}/serial.log" + fi +} + nested_force_stop_vm() { systemctl stop nested-vm } nested_force_start_vm() { + # if the nested-vm is using a swtpm, we need to wait until the file exists + # because the file disappears temporarily after qemu exits + if systemctl show nested-vm -p ExecStart | grep -q swtpm-mvo; then + retry -n 10 --wait 1 test -S /var/snap/swtpm-mvo/current/swtpm-sock + fi systemctl start nested-vm } @@ -735,6 +801,9 @@ PARAM_SERIAL="-chardev socket,telnet,host=localhost,server,port=7777,nowait,id=char0,logfile=${NESTED_LOGS_DIR}/serial.log,logappend=on -serial chardev:char0" fi + # save logs from previous runs + nested_save_serial_log + # Set kvm attribute local ATTR_KVM ATTR_KVM="" @@ -742,9 +811,6 @@ ATTR_KVM=",accel=kvm" # CPU can be defined just when kvm is enabled PARAM_CPU="-cpu host" - # Increase the number of cpus used once the issue related to kvm and ovmf is fixed - # https://bugs.launchpad.net/ubuntu/+source/kvm/+bug/1872803 - PARAM_SMP="-smp 1" fi local PARAM_MACHINE @@ -908,17 +974,24 @@ else # Start the nested core vm nested_start_core_vm_unit "$CURRENT_IMAGE" - fi + fi } nested_shutdown() { + # we sometimes have bugs in nested vm's where files that were successfully + # written become empty all of a sudden, so doing a sync here in the VM, and + # another one in the host when done probably helps to avoid that, and at + # least can't hurt anything + nested_exec "sync" nested_exec "sudo shutdown now" || true nested_wait_for_no_ssh nested_force_stop_vm wait_for_service "$NESTED_VM" inactive + sync } nested_start() { + nested_save_serial_log nested_force_start_vm wait_for_service "$NESTED_VM" active nested_wait_for_ssh @@ -971,19 +1044,20 @@ PARAM_SMP="-smp 1" # use only 2G of RAM for qemu-nested if [ "$SPREAD_BACKEND" = "google-nested" ]; then - PARAM_MEM="-m 4096" + PARAM_MEM="${NESTED_PARAM_MEM:--m 4096}" elif [ "$SPREAD_BACKEND" = "qemu-nested" ]; then - PARAM_MEM="-m 2048" + PARAM_MEM="${NESTED_PARAM_MEM:--m 2048}" else echo "unknown spread backend $SPREAD_BACKEND" exit 1 fi - local PARAM_DISPLAY PARAM_NETWORK PARAM_MONITOR PARAM_USB PARAM_CPU PARAM_RANDOM PARAM_SNAPSHOT + local PARAM_DISPLAY PARAM_NETWORK PARAM_MONITOR PARAM_USB PARAM_CPU PARAM_CD PARAM_RANDOM PARAM_SNAPSHOT PARAM_DISPLAY="-nographic" PARAM_NETWORK="-net nic,model=virtio -net user,hostfwd=tcp::$NESTED_SSH_PORT-:22" PARAM_MONITOR="-monitor tcp:127.0.0.1:$NESTED_MON_PORT,server,nowait" PARAM_USB="-usb" PARAM_CPU="" + PARAM_CD="${NESTED_PARAM_CD:-}" PARAM_RANDOM="-object rng-random,id=rng0,filename=/dev/urandom -device virtio-rng-pci,rng=rng0" PARAM_SNAPSHOT="-snapshot" @@ -1025,8 +1099,9 @@ # ensure we have a log dir mkdir -p "$NESTED_LOGS_DIR" - # make sure we start with clean log file - echo > "${NESTED_LOGS_DIR}/serial.log" + # save logs from previous runs + nested_save_serial_log + # Systemd unit is created, it is important to respect the qemu parameters order systemd_create_and_start_unit "$NESTED_VM" "${QEMU} \ ${PARAM_SMP} \ @@ -1043,13 +1118,14 @@ ${PARAM_SEED} \ ${PARAM_SERIAL} \ ${PARAM_MONITOR} \ - ${PARAM_USB} " + ${PARAM_USB} \ + ${PARAM_CD} " nested_wait_for_ssh } nested_destroy_vm() { - systemd_stop_and_destroy_unit "$NESTED_VM" + systemd_stop_and_remove_unit "$NESTED_VM" local CURRENT_IMAGE CURRENT_IMAGE="$NESTED_IMAGES_DIR/$(nested_get_current_image_name)" @@ -1120,3 +1196,17 @@ echo "$NESTED_WORK_DIR/spread" fi } + +nested_build_seed_cdrom() { + local SEED_DIR="$1" + local SEED_NAME="$2" + local LABEL="$3" + + shift 3 + + local ORIG_DIR=$PWD + + pushd "$SEED_DIR" || return 1 + genisoimage -output "$ORIG_DIR/$SEED_NAME" -volid "$LABEL" -joliet -rock "$@" + popd || return 1 +} diff -Nru snapd-2.47.1+20.10.1build1/tests/lib/network.sh snapd-2.48+21.04/tests/lib/network.sh --- snapd-2.47.1+20.10.1build1/tests/lib/network.sh 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/lib/network.sh 2020-11-19 16:51:02.000000000 +0000 @@ -21,14 +21,8 @@ make_network_service() { SERVICE_NAME="$1" - SERVICE_FILE="$2" - PORT="$3" + PORT="$2" - #shellcheck source=tests/lib/systemd.sh - . "$TESTSLIB"/systemd.sh - - printf '#!/bin/sh -e\nwhile true; do printf '\''HTTP/1.1 200 OK\\n\\nok\\n'\'' | nc -l -p %s -w 1; done' "$PORT" > "$SERVICE_FILE" - chmod a+x "$SERVICE_FILE" - systemd_create_and_start_unit "$SERVICE_NAME" "$(readlink -f "$SERVICE_FILE")" + systemd-run --unit "$SERVICE_NAME" sh -c "while true; do printf 'HTTP/1.1 200 OK\\n\\nok\\n' | nc -l -p $PORT -w 1; done" wait_listen_port "$PORT" } diff -Nru snapd-2.47.1+20.10.1build1/tests/lib/pkgdb.sh snapd-2.48+21.04/tests/lib/pkgdb.sh --- snapd-2.47.1+20.10.1build1/tests/lib/pkgdb.sh 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/lib/pkgdb.sh 2020-11-19 16:51:02.000000000 +0000 @@ -520,6 +520,12 @@ fi fi + if [[ "$SPREAD_SYSTEM" == opensuse-tumbleweed-* ]]; then + # Package installation applies vendor presets only, which leaves + # snapd.apparmor disabled. + systemctl enable --now snapd.apparmor.service + fi + # On some distributions the snapd.socket is not yet automatically # enabled as we don't have a systemd present configuration approved # by the distribution for it in place yet. diff -Nru snapd-2.47.1+20.10.1build1/tests/lib/prepare-restore.sh snapd-2.48+21.04/tests/lib/prepare-restore.sh --- snapd-2.47.1+20.10.1build1/tests/lib/prepare-restore.sh 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/lib/prepare-restore.sh 2020-11-19 16:51:02.000000000 +0000 @@ -25,9 +25,6 @@ # shellcheck source=tests/lib/state.sh . "$TESTSLIB/state.sh" -# shellcheck source=tests/lib/systems.sh -. "$TESTSLIB/systems.sh" - ### ### Utility functions reused below. @@ -67,6 +64,10 @@ fi unset owner + # Add a new line first to prevent an error which happens when + # the file has not new line, and we see this: + # syntax error, unexpected WORD, expecting END or ':' or '\n' + echo >> /etc/sudoers echo 'test ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers chown test.test -R "$SPREAD_PATH" @@ -544,7 +545,7 @@ # On core systems, the journal service is configured once the final core system # is created and booted what is done during the first test suite preparation - if is_classic_system; then + if os.query is-classic; then # shellcheck source=tests/lib/prepare.sh . "$TESTSLIB"/prepare.sh disable_journald_rate_limiting @@ -562,7 +563,7 @@ prepare_suite() { # shellcheck source=tests/lib/prepare.sh . "$TESTSLIB"/prepare.sh - if is_core_system; then + if os.query is-core; then prepare_ubuntu_core else prepare_classic @@ -584,7 +585,7 @@ local variant="$1" # back test directory to be restored during the restore - tar cf "${PWD}.tar" "$PWD" + tests.backup prepare # WORKAROUND for memleak https://github.com/systemd/systemd/issues/11502 if [[ "$SPREAD_SYSTEM" == debian-sid* ]]; then @@ -612,7 +613,7 @@ # shellcheck source=tests/lib/prepare.sh . "$TESTSLIB"/prepare.sh - if is_classic_system; then + if os.query is-classic; then prepare_each_classic fi fi @@ -638,11 +639,7 @@ rm -f "$RUNTIME_STATE_PATH/audit-stamp" # restore test directory saved during prepare - if [ -f "${PWD}.tar" ]; then - rm -rf "$PWD" - tar -C/ -xf "${PWD}.tar" - rm -rf "${PWD}.tar" - fi + tests.backup restore if [[ "$variant" = full && "$PROFILE_SNAPS" = 1 ]]; then echo "Save snaps profiler log" @@ -682,7 +679,7 @@ restore_suite() { # shellcheck source=tests/lib/reset.sh "$TESTSLIB"/reset.sh --store - if is_classic_system; then + if os.query is-classic; then # shellcheck source=tests/lib/pkgdb.sh . "$TESTSLIB"/pkgdb.sh distro_purge_package snapd diff -Nru snapd-2.47.1+20.10.1build1/tests/lib/prepare.sh snapd-2.48+21.04/tests/lib/prepare.sh --- snapd-2.47.1+20.10.1build1/tests/lib/prepare.sh 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/lib/prepare.sh 2020-11-19 16:51:02.000000000 +0000 @@ -12,8 +12,6 @@ . "$TESTSLIB/boot.sh" # shellcheck source=tests/lib/state.sh . "$TESTSLIB/state.sh" -# shellcheck source=tests/lib/systems.sh -. "$TESTSLIB/systems.sh" disable_kernel_rate_limiting() { @@ -55,10 +53,10 @@ return fi - if is_core18_system; then + if os.query is-core18; then snap install --devmode jq-core18 snap alias jq-core18.jq jq - elif is_core20_system; then + elif os.query is-core20; then snap install --devmode --edge jq-core20 snap alias jq-core20.jq jq else @@ -340,17 +338,17 @@ rm -f "$UNPACK_DIR"/etc/apparmor.d/* dpkg-deb -x "$SPREAD_PATH"/../snapd_*.deb "$UNPACK_DIR" - cp /usr/lib/snapd/info "$UNPACK_DIR"/usr/lib/ + cp /usr/lib/snapd/info "$UNPACK_DIR"/usr/lib/snapd snap pack "$UNPACK_DIR" "$TARGET" rm -rf "$UNPACK_DIR" } -repack_core20_snap_with_tweaks() { - local CORE20SNAP="$1" +repack_core_snap_with_tweaks() { + local CORESNAP="$1" local TARGET="$2" - local UNPACK_DIR="/tmp/core20-unpack" - unsquashfs -no-progress -d "$UNPACK_DIR" "$CORE20SNAP" + local UNPACK_DIR="/tmp/core-unpack" + unsquashfs -no-progress -d "$UNPACK_DIR" "$CORESNAP" mkdir -p "$UNPACK_DIR"/etc/systemd/journald.conf.d cat < "$UNPACK_DIR"/etc/systemd/journald.conf.d/to-console.conf @@ -385,7 +383,7 @@ rm -f "$UNPACK_DIR"/etc/apparmor.d/* dpkg-deb -x "$SPREAD_PATH"/../snapd_*.deb "$UNPACK_DIR" - cp /usr/lib/snapd/info "$UNPACK_DIR"/usr/lib/ + cp /usr/lib/snapd/info "$UNPACK_DIR"/usr/lib/snapd # now install a unit that sets up enough so that we can connect cat > "$UNPACK_DIR"/lib/systemd/system/snapd.spread-tests-run-mode-tweaks.service <<'EOF' @@ -509,22 +507,6 @@ echo "if test -d /run/mnt/data/system-data; then touch /run/mnt/data/system-data/the-tool-ran; fi" >> \ "$skeletondir/main/usr/lib/the-tool" - - # patch the initramfs to go back to isolating the initrd units and to - # not specify to mount /run/mnt/ubuntu-boot via fstab, these were all - # done as interim changes until snap-bootstrap took control of things, - # now that snap-bootstrap is in control, we don't want those hacks, but - # we still want accurate test results so drop those hacks to let - # snap-bootstrap do everything for the spread run - - # TODO:UC20: drop these patches when the associated changes have landed - # upstream - rm -rf "$skeletondir/main/usr/lib/systemd/system/initrd-cleanup.service.d/core-override.conf" - sed -i "$skeletondir/main/usr/lib/systemd/system/populate-writable.service" \ - -e "s@ExecStartPost=/usr/bin/systemctl --no-block start initrd.target@ExecStartPost=/usr/bin/systemctl --no-block isolate initrd.target@" - sed -i "$skeletondir/main/usr/lib/the-modeenv" \ - -e "s@echo 'LABEL=ubuntu-boot /run/mnt/ubuntu-boot auto defaults 0 0' >> /run/image.fstab@echo not doing anything@" - if [ "$injectKernelPanic" = "true" ]; then # add a kernel panic to the end of the-tool execution echo "echo 'forcibly panicing'; echo c > /proc/sysrq-trigger" >> "$skeletondir/main/usr/lib/the-tool" @@ -597,6 +579,7 @@ ) snap pack repacked-kernel "$TARGET" + rm -rf repacked-kernel } @@ -734,15 +717,15 @@ snap wait system seed.loaded # download the snapd snap for all uc systems except uc16 - if ! is_core16_system; then + if ! os.query is-core16; then snap download "--channel=${SNAPD_CHANNEL}" snapd fi # we cannot use "names.sh" here because no snaps are installed yet core_name="core" - if is_core18_system; then + if os.query is-core18; then core_name="core18" - elif is_core20_system; then + elif os.query is-core20; then core_name="core20" fi # XXX: we get "error: too early for operation, device not yet @@ -754,7 +737,7 @@ snap model --verbose # remove the above debug lines once the mentioned bug is fixed snap install "--channel=${CORE_CHANNEL}" "$core_name" - if is_core16_system || is_core18_system; then + if os.query is-core16 || os.query is-core18; then UNPACK_DIR="/tmp/$core_name-snap" unsquashfs -no-progress -d "$UNPACK_DIR" /var/lib/snapd/snaps/${core_name}_*.snap fi @@ -774,12 +757,12 @@ cp /usr/bin/snap "$IMAGE_HOME" export UBUNTU_IMAGE_SNAP_CMD="$IMAGE_HOME/snap" - if is_core18_system; then + if os.query is-core18; then repack_snapd_snap_with_deb_content "$IMAGE_HOME" # FIXME: fetch directly once its in the assertion service cp "$TESTSLIB/assertions/ubuntu-core-18-amd64.model" "$IMAGE_HOME/pc.model" IMAGE=core18-amd64.img - elif is_core20_system; then + elif os.query is-core20; then repack_snapd_snap_with_deb_content_and_run_mode_firstboot_tweaks "$IMAGE_HOME" cp "$TESTSLIB/assertions/ubuntu-core-20-amd64.model" "$IMAGE_HOME/pc.model" IMAGE=core20-amd64.img @@ -837,7 +820,7 @@ IMAGE_CHANNEL="$GADGET_CHANNEL" fi - if is_core20_system; then + if os.query is-core20; then snap download --basename=pc-kernel --channel="20/$KERNEL_CHANNEL" pc-kernel # make sure we have the snap test -e pc-kernel.snap @@ -852,7 +835,7 @@ # on core18 we need to use the modified snapd snap and on core16 # it is the modified core that contains our freshly build snapd - if is_core18_system || is_core20_system; then + if os.query is-core18 || os.query is-core20; then extra_snap=("$IMAGE_HOME"/snapd_*.snap) else extra_snap=("$IMAGE_HOME"/core_*.snap) @@ -871,7 +854,7 @@ --output "$IMAGE_HOME/$IMAGE" rm -f ./pc-kernel_*.{snap,assert} ./pc_*.{snap,assert} ./snapd_*.{snap,assert} - if is_core20_system; then + if os.query is-core20; then # (ab)use ubuntu-seed LOOP_PARTITION=2 else @@ -881,7 +864,7 @@ # expand the uc16 and uc18 images a little bit (400M) as it currently will # run out of space easily from local spread runs if there are extra files in # the project not included in the git ignore and spread ignore, etc. - if ! is_core20_system; then + if ! os.query is-core20; then # grow the image by 400M truncate --size=+400M "$IMAGE_HOME/$IMAGE" # fix the GPT table because old versions of parted complain about this @@ -905,7 +888,7 @@ dev=$(basename "$devloop") # resize the 2nd partition from that loop device to fix the size - if ! is_core20_system; then + if ! os.query is-core20; then resize2fs -p "/dev/mapper/${dev}p${LOOP_PARTITION}" fi @@ -918,7 +901,7 @@ # - built debs # - golang archive files and built packages dir # - govendor .cache directory and the binary, - if is_core16_system || is_core18_system; then + if os.query is-core16 || os.query is-core18; then # we need to include "core" here because -C option says to ignore # files the way CVS(?!) does, so it ignores files named "core" which # are core dumps, but we have a test suite named "core", so including @@ -932,7 +915,7 @@ --exclude /gopath/pkg/ \ --include core/ \ /home/gopath /mnt/user-data/ - elif is_core20_system; then + elif os.query is-core20; then # prepare passwd for run-mode-overlay-data mkdir -p /root/test-etc mkdir -p /var/lib/extrausers @@ -963,7 +946,7 @@ fi # now modify the image writable partition - only possible on uc16 / uc18 - if is_core16_system || is_core18_system; then + if os.query is-core16 || os.query is-core18; then # modify the writable partition of "core" so that we have the # test user setup_core_for_testing_by_modify_writable "$UNPACK_DIR" @@ -994,7 +977,7 @@ chmod +x "$IMAGE_HOME/reflash.sh" DEVPREFIX="" - if is_core20_system; then + if os.query is-core20; then DEVPREFIX="/boot" fi # extract ROOT from /proc/cmdline @@ -1051,7 +1034,7 @@ done echo "Ensure the snapd snap is available" - if is_core18_system || is_core20_system; then + if os.query is-core18 || os.query is-core20; then if ! snap list snapd; then echo "snapd snap on core18 is missing" snap list @@ -1062,9 +1045,9 @@ echo "Ensure rsync is available" if ! command -v rsync; then rsync_snap="test-snapd-rsync" - if is_core18_system; then + if os.query is-core18; then rsync_snap="test-snapd-rsync-core18" - elif is_core20_system; then + elif os.query is-core20; then rsync_snap="test-snapd-rsync-core20" fi snap install --devmode --edge "$rsync_snap" @@ -1077,21 +1060,21 @@ echo "Ensure the core snap is cached" # Cache snaps - if is_core18_system || is_core20_system; then + if os.query is-core18 || os.query is-core20; then if snap list core >& /dev/null; then echo "core snap on core18 should not be installed yet" snap list exit 1 fi cache_snaps core - if is_core18_system; then + if os.query is-core18; then cache_snaps test-snapd-sh-core18 fi fi echo "Cache the snaps profiler snap" if [ "$PROFILE_SNAPS" = 1 ]; then - if is_core18_system; then + if os.query is-core18; then cache_snaps test-snapd-profiler-core18 else cache_snaps test-snapd-profiler diff -Nru snapd-2.47.1+20.10.1build1/tests/lib/reset.sh snapd-2.48+21.04/tests/lib/reset.sh --- snapd-2.47.1+20.10.1build1/tests/lib/reset.sh 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/lib/reset.sh 2020-11-19 16:51:02.000000000 +0000 @@ -8,8 +8,6 @@ # shellcheck source=tests/lib/systemd.sh . "$TESTSLIB/systemd.sh" -#shellcheck source=tests/lib/systems.sh -. "$TESTSLIB"/systems.sh reset_classic() { # Reload all service units as in some situations the unit might @@ -176,7 +174,7 @@ # When the variable REUSE_SNAPD is set to 1, we don't remove and purge snapd. # In that case we just cleanup the environment by removing installed snaps as # it is done for core systems. -if is_core_system || [ "$REUSE_SNAPD" = 1 ]; then +if os.query is-core || [ "$REUSE_SNAPD" = 1 ]; then reset_all_snap "$@" else reset_classic "$@" diff -Nru snapd-2.47.1+20.10.1build1/tests/lib/snaps/test-snapd-policy-app-consumer/meta/snap.yaml snapd-2.48+21.04/tests/lib/snaps/test-snapd-policy-app-consumer/meta/snap.yaml --- snapd-2.47.1+20.10.1build1/tests/lib/snaps/test-snapd-policy-app-consumer/meta/snap.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/lib/snaps/test-snapd-policy-app-consumer/meta/snap.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -326,6 +326,9 @@ process-control: command: bin/run plugs: [ process-control ] + ptp: + command: bin/run + plugs: [ ptp ] pulseaudio: command: bin/run plugs: [ pulseaudio ] diff -Nru snapd-2.47.1+20.10.1build1/tests/lib/snaps.sh snapd-2.48+21.04/tests/lib/snaps.sh --- snapd-2.47.1+20.10.1build1/tests/lib/snaps.sh 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/lib/snaps.sh 2020-11-19 16:51:02.000000000 +0000 @@ -2,20 +2,26 @@ make_snap() { local SNAP_NAME="$1" - local SNAP_DIR="$TESTSLIB/snaps/${SNAP_NAME}" - if [ $# -gt 1 ]; then - SNAP_DIR="$2" + local SNAP_DIR="${2:-$TESTSLIB/snaps/${SNAP_NAME}}" + local SNAP_VERSION="${3:-1.0}" + + local META_FILE META_NAME SNAP_FILE + META_FILE="$SNAP_DIR/meta/snap.yaml" + if [ ! -f "$META_FILE" ]; then + echo "snap.yaml file not found for $SNAP_NAME snap" + return 1 fi - local SNAP_FILE="${SNAP_DIR}/${SNAP_NAME}_1.0_all.snap" + META_NAME="$(grep '^name:' "$META_FILE" | awk '{ print $2 }' | tr -d ' ')" + SNAP_FILE="${SNAP_DIR}/${META_NAME}_${SNAP_VERSION}_all.snap" # assigned in a separate step to avoid hiding a failure if [ ! -f "$SNAP_FILE" ]; then - snap pack "$SNAP_DIR" "$SNAP_DIR" >/dev/null || return 1 + snap pack "$SNAP_DIR" "$SNAP_DIR" >/dev/null fi # echo the snap name if [ -f "$SNAP_FILE" ]; then echo "$SNAP_FILE" else - find "$SNAP_DIR" -name '*.snap' | head -n1 + find "$SNAP_DIR" -name "${META_NAME}_*.snap"| head -n1 fi } @@ -97,6 +103,9 @@ # repack into the target dir specified snap pack --filename=snapd-from-deb.snap snapd-unpacked "$1" + + # cleanup + rm -rf snapd-unpacked } # repack_snapd_deb_into_core_snap will re-pack a core snap using the assets @@ -112,6 +121,9 @@ # repack into the target dir specified snap pack --filename=core-from-snapd-deb.snap core-unpacked "$1" + + # cleanup + rm -rf core-unpacked } # repack_installed_core_snap_into_snapd_snap will re-pack the core snap as the snapd snap, diff -Nru snapd-2.47.1+20.10.1build1/tests/lib/state.sh snapd-2.48+21.04/tests/lib/state.sh --- snapd-2.47.1+20.10.1build1/tests/lib/state.sh 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/lib/state.sh 2020-11-19 16:51:02.000000000 +0000 @@ -14,8 +14,6 @@ # shellcheck source=tests/lib/systemd.sh . "$TESTSLIB/systemd.sh" -# shellcheck source=tests/lib/systems.sh -. "$TESTSLIB/systems.sh" delete_snapd_state() { rm -rf "$SNAPD_STATE_PATH" @@ -26,9 +24,9 @@ } is_snapd_state_saved() { - if is_core_system && [ -d "$SNAPD_STATE_PATH"/snapd-lib ]; then + if os.query is-core && [ -d "$SNAPD_STATE_PATH"/snapd-lib ]; then return 0 - elif is_classic_system && [ -f "$SNAPD_STATE_FILE" ]; then + elif os.query is-classic && [ -f "$SNAPD_STATE_FILE" ]; then return 0 else return 1 @@ -36,7 +34,7 @@ } save_snapd_state() { - if is_core_system; then + if os.query is-core; then boot_path="$(get_boot_path)" test -n "$boot_path" || return 1 @@ -75,7 +73,7 @@ core="$(readlink -f "$SNAP_MOUNT_DIR/core/current")" # on 14.04 it is possible that the core snap is still mounted at this point, unmount # to prevent errors starting the mount unit - if is_ubuntu_14_system && mount | grep -q "$core"; then + if os.query is-trusty && mount | grep -q "$core"; then umount "$core" || true fi for unit in $units; do @@ -88,7 +86,7 @@ } restore_snapd_state() { - if is_core_system; then + if os.query is-core; then # we need to ensure that we also restore the boot environment # fully for tests that break it boot_path="$(get_boot_path)" @@ -128,7 +126,7 @@ # Synchronize snaps, seed and cache directories. The this is done separately in order to avoid copying # the snap files due to it is a heavy task and take most of the time of the restore phase. rsync -av --delete "$SNAPD_STATE_PATH"/snapd-lib/snaps /var/lib/snapd - if is_core20_system ; then + if os.query is-core20 ; then # TODO:UC20: /var/lib/snapd/seed is a read only bind mount, use the rw # mount or later mount seed as needed rsync -av --delete "$SNAPD_STATE_PATH"/snapd-lib/seed/ /run/mnt/ubuntu-seed/ diff -Nru snapd-2.47.1+20.10.1build1/tests/lib/store.sh snapd-2.48+21.04/tests/lib/store.sh --- snapd-2.47.1+20.10.1build1/tests/lib/store.sh 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/lib/store.sh 2020-11-19 16:51:02.000000000 +0000 @@ -125,7 +125,7 @@ echo "Create fakestore at the given port" PORT="11028" - systemd_create_and_start_unit fakestore "$(command -v fakestore) run --dir $top_dir --addr localhost:$PORT --https-proxy=${https_proxy} --http-proxy=${http_proxy} --assert-fallback" "SNAPD_DEBUG=1 SNAPD_DEBUG_HTTP=7 SNAPPY_TESTING=1 SNAPPY_USE_STAGING_STORE=$SNAPPY_USE_STAGING_STORE" + systemd-run --unit fakestore --setenv SNAPD_DEBUG=1 --setenv SNAPD_DEBUG_HTTP=7 --setenv SNAPPY_TESTING=1 --setenv SNAPPY_USE_STAGING_STORE="$SNAPPY_USE_STAGING_STORE" fakestore run --dir "$top_dir" --addr "localhost:$PORT" --https-proxy="${https_proxy}" --http-proxy="${http_proxy}" --assert-fallback echo "And snapd is configured to use the controlled store" _configure_store_backends "SNAPPY_FORCE_API_URL=http://localhost:$PORT" "SNAPPY_USE_STAGING_STORE=$SNAPPY_USE_STAGING_STORE" @@ -146,7 +146,7 @@ teardown_fake_store(){ local top_dir=$1 - systemd_stop_and_destroy_unit fakestore + systemctl stop fakestore || true if [ "$REMOTE_STORE" = "staging" ]; then setup_staging_store diff -Nru snapd-2.47.1+20.10.1build1/tests/lib/systemd.sh snapd-2.48+21.04/tests/lib/systemd.sh --- snapd-2.47.1+20.10.1build1/tests/lib/systemd.sh 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/lib/systemd.sh 2020-11-19 16:51:02.000000000 +0000 @@ -1,21 +1,11 @@ #!/bin/bash -# Use like systemd_create_and_start_unit(fakestore, "$(which fakestore) -start -dir $top_dir -addr localhost:11028 $@") -systemd_create_and_start_unit() { - printf '[Unit]\nDescription=Support for test %s\n[Service]\nType=simple\nExecStart=%s\n' "${SPREAD_JOB:-unknown}" "$2" > "/run/systemd/system/$1.service" - if [ -n "${3:-}" ]; then - echo "Environment=$3" >> "/run/systemd/system/$1.service" - fi - systemctl daemon-reload - systemctl start "$1" -} - # Create and start a persistent systemd unit that survives reboots. Use as: -# systemd_create_and_start_persistent_unit "name" "my-service --args" +# systemd_create_and_start_unit "name" "my-service --args" # The third arg supports "overrides" which allow to customize the service # as needed, e.g.: -# systemd_create_and_start_persistent_unit "name" "start" "[Unit]\nAfter=foo" -systemd_create_and_start_persistent_unit() { +# systemd_create_and_start_unit "name" "start" "[Unit]\nAfter=foo" +systemd_create_and_start_unit() { printf '[Unit]\nDescription=Support for test %s\n[Service]\nType=simple\nExecStart=%s\n[Install]\nWantedBy=multi-user.target\n' "${SPREAD_JOB:-unknown}" "$2" > "/etc/systemd/system/$1.service" if [ -n "${3:-}" ]; then mkdir -p "/etc/systemd/system/$1.service.d" @@ -28,19 +18,11 @@ wait_for_service "$1" } -system_stop_and_remove_persistent_unit() { +systemd_stop_and_remove_unit() { systemctl stop "$1" || true systemctl disable "$1" || true rm -f "/etc/systemd/system/$1.service" rm -rf "/etc/systemd/system/$1.service.d" -} - -# Use like systemd_stop_and_destroy_unit(fakestore) -systemd_stop_and_destroy_unit() { - if systemctl is-active "$1"; then - systemctl stop "$1" - fi - rm -f "/run/systemd/system/$1.service" systemctl daemon-reload } diff -Nru snapd-2.47.1+20.10.1build1/tests/lib/systems.sh snapd-2.48+21.04/tests/lib/systems.sh --- snapd-2.47.1+20.10.1build1/tests/lib/systems.sh 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/lib/systems.sh 2020-11-19 16:51:02.000000000 +0000 @@ -1,60 +1,5 @@ #!/bin/bash -is_core_system(){ - if [[ "$SPREAD_SYSTEM" == ubuntu-core-* ]]; then - return 0 - fi - return 1 -} - -is_xenial_system() { - test "$(lsb_release -cs)" = xenial -} - -is_core16_system(){ - if [[ "$SPREAD_SYSTEM" == ubuntu-core-16-* ]]; then - return 0 - fi - return 1 -} - -is_bionic_system() { - test "$(lsb_release -cs)" = bionic -} - -is_core18_system(){ - if [[ "$SPREAD_SYSTEM" == ubuntu-core-18-* ]]; then - return 0 - fi - return 1 -} - -is_focal_system() { - test "$(lsb_release -cs)" = focal -} - -is_core20_system(){ - if [[ "$SPREAD_SYSTEM" == ubuntu-core-20-* ]]; then - return 0 - fi - return 1 -} - -is_classic_system(){ - if [[ "$SPREAD_SYSTEM" != ubuntu-core-* ]]; then - return 0 - fi - return 1 -} - - -is_ubuntu_14_system(){ - if [[ "$SPREAD_SYSTEM" == ubuntu-14.04-* ]]; then - return 0 - fi - return 1 -} - get_snap_for_system(){ local snap=$1 diff -Nru snapd-2.47.1+20.10.1build1/tests/lib/tools/boot-state snapd-2.48+21.04/tests/lib/tools/boot-state --- snapd-2.47.1+20.10.1build1/tests/lib/tools/boot-state 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48+21.04/tests/lib/tools/boot-state 2020-11-19 16:51:02.000000000 +0000 @@ -0,0 +1,197 @@ +#!/bin/bash + +show_help() { + echo "usage: boot-state bootenv show [var]" + echo " boot-state bootenv set " + echo " boot-state bootenv unset " + echo " boot-state boot-path" + echo " boot-state wait-core-post-boot" + echo "" + echo "Get information and manages the boot loader for the current system" + echo "" + echo "COMMANDS:" + echo " bootenv show: prints the whole bootenv or just the variable passed as parameter" + echo " bootenv set: sets the given var and value on boot configuration" + echo " bootenv unset: unsets the given var from boot configuration" + echo " boot-path: prints the boot path" + echo " wait-core-post-boot: waits until the snap_mode bootenv var is empty" +} + +get_grub_editenv() { + if [ -e /boot/grub2/grubenv ]; then + command -v grub2-editenv + elif [ -e /boot/grub/grubenv ]; then + command -v grub-editenv + fi +} + +run_grub_editenv() { + "$(get_grub_editenv)" "$@" +} + +get_grub_envfile() { + if [ -e /boot/grub2/grubenv ]; then + echo /boot/grub2/grubenv + elif [ -e /boot/grub/grubenv ]; then + echo /boot/grub/grubenv + else + echo "" + fi +} + +bootenv() { + case "${1:-}" in + show) + shift + bootenv_show "$@" + exit + ;; + set) + shift + bootenv_set "$@" + exit + ;; + unset) + shift + bootenv_unset "$@" + exit + ;; + *) + echo "boot-state: unsupported bootenv sub-command $1" >&2 + show_help + exit 1 + ;; + esac +} + +bootenv_show() { + local var="${1:-}" + local grubenv_file + grubenv_file="$(get_grub_envfile)" + + if [ -z "$var" ]; then + if run_grub_editenv list; then + return + elif [ -s "$grubenv_file" ]; then + cat "$grubenv_file" + else + fw_printenv + fi + else + # TODO: fix that, it could be problematic if var ends up with regular expression + if run_grub_editenv list | grep "^$var"; then + return + elif [ -s "$grubenv_file" ]; then + grep "^$var" "$grubenv_file" + else + fw_printenv "$1" + fi | sed "s/^${var}=//" + fi +} + +bootenv_set() { + local var="$1" + local value="$2" + + if [ -z "$var" ] || [ -z "$value" ]; then + echo "boot-state: variable and value required to set in bootenv" >&2 + show_help + exit 1 + fi + local grubenv_file + grubenv_file="$(get_grub_envfile)" + + if run_grub_editenv set "$var=$value"; then + return + elif [ -s "$grubenv_file" ]; then + sed -i "/^$var=/d" "$grubenv_file" + #The grubenv file could not have a new line at the end + if [ -n "$(tail -n 1 "$grubenv_file")" ]; then + echo "" >> "$grubenv_file" + fi + echo "$var=$value" >> "$grubenv_file" + else + fw_setenv "$var" "$value" + fi +} + +bootenv_unset() { + local var="$1" + + if [ -z "$var" ]; then + echo "boot-state: variable required to unset from bootenv" >&2 + show_help + exit 1 + fi + local grubenv_file + grubenv_file="$(get_grub_envfile)" + + if run_grub_editenv "$grubenv_file" unset "$var"; then + return + elif [ -s "$grubenv_file" ]; then + sed -i "/^$var=/d" "$grubenv_file" + else + fw_setenv "$var" + fi +} + +boot_path() { + if [ -f /boot/uboot/uboot.env ] || [ -f /boot/uboot/boot.sel ]; then + # uc16/uc18 have /boot/uboot/uboot.env + # uc20 has /boot/uboot/boot.sel + echo "/boot/uboot/" + elif [ -f /boot/grub/grubenv ]; then + echo "/boot/grub/" + elif [ -f /boot/grub2/grubenv ]; then + echo "/boot/grub2/" + else + echo "boot-state: cannot determine boot path" >&2 + ls -alR /boot + exit 1 + fi +} + +wait_core_post_boot() { + for _ in $(seq 120); do + if [ "$(bootenv_show snap_mode)" = "" ]; then + return + fi + sleep 1 + done + echo "boot-state: timeout reached waiting for core after boot" >&2 + exit 1 +} + +main() { + if [ $# -eq 0 ]; then + show_help + exit 0 + fi + + case "$1" in + -h|--help) + show_help + exit + ;; + bootenv) + shift + bootenv "$@" + exit + ;; + boot-path) + boot_path + exit + ;; + wait-core-post-boot) + wait_core_post_boot + exit + ;; + *) + echo "boot-state: unsupported parameter $1" >&2 + show_help + exit 1 + ;; + esac +} + +main "$@" diff -Nru snapd-2.47.1+20.10.1build1/tests/lib/tools/nested-state snapd-2.48+21.04/tests/lib/tools/nested-state --- snapd-2.47.1+20.10.1build1/tests/lib/tools/nested-state 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48+21.04/tests/lib/tools/nested-state 2020-11-19 16:51:02.000000000 +0000 @@ -0,0 +1,148 @@ +#!/bin/bash -e + +show_help() { + echo "usage: prepare" + echo " restore" + echo " build-image IMAGE-TYPE" + echo " create-vm IMAGE-TYPE [--param-cdrom PARAM] [--param-mem PARAM]" + echo " start-vm" + echo " stop-vm" + echo " remove-vm" + echo "" + echo "Available options:" + echo " -h --help show this help message." + echo "" + echo "COMMANDS:" + echo " prepare: creates all the directories needed to run a nested test" + echo " restore: removes all the directories and data used by nested tests" + echo " build-image: creates an image using ubuntu image tool" + echo " create-vm: creates new virtual machine and leave it running" + echo " start-vm: starts a stopped vm" + echo " stop-vm: shutdowns a running vm" + echo " remove-vm: removes a vm" + echo "" + echo "IMAGE-TYPES:" + echo " core: work with a core image" + echo " classic: work with a classic image" + echo "" +} + +prepare() { + nested_prepare_env +} + +restore() { + nested_cleanup_env +} + +build_image() { + if [ $# -eq 0 ]; then + show_help + exit 1 + fi + while [ $# -gt 0 ]; do + case "$1" in + classic) + nested_create_classic_vm + exit + ;; + core) + nested_create_core_vm + exit + ;; + *) + echo "nested-state: expected either classic or core as argument" >&2 + exit 1 + ;; + esac + done +} + +create_vm() { + if [ $# -eq 0 ]; then + show_help + exit 1 + fi + local action= + case "$1" in + classic) + shift 1 + action=nested_start_classic_vm + ;; + core) + shift 1 + action=nested_start_core_vm + ;; + *) + echo "nested-state: unsupported parameter $1" >&2 + exit 1 + ;; + esac + + while [ $# -gt 0 ]; do + case "$1" in + --param-cdrom) + export NESTED_PARAM_CD="$2" + shift 2 + ;; + --param-mem) + export NESTED_PARAM_MEM="$2" + shift 2 + ;; + *) + echo "nested-state: unsupported parameter $1" >&2 + exit 1 + ;; + esac + done + + "$action" +} + + +start_vm() { + nested_start +} + +stop_vm() { + nested_shutdown +} + +remove_vm() { + nested_destroy_vm +} + +main() { + if [ $# -eq 0 ]; then + show_help + exit 0 + fi + + local subcommand="$1" + local action= + while [ $# -gt 0 ]; do + case "$1" in + -h|--help) + show_help + exit 0 + ;; + *) + action=$(echo "$subcommand" | tr '-' '_') + shift + break + ;; + esac + done + + if [ -z "$(declare -f "$action")" ]; then + echo "nested-state: no such command: $subcommand" + show_help + exit 1 + fi + + #shellcheck source=tests/lib/nested.sh + . "$TESTSLIB/nested.sh" + "$action" "$@" +} + +main "$@" diff -Nru snapd-2.47.1+20.10.1build1/tests/lib/tools/os.query snapd-2.48+21.04/tests/lib/tools/os.query --- snapd-2.47.1+20.10.1build1/tests/lib/tools/os.query 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48+21.04/tests/lib/tools/os.query 2020-11-19 16:51:02.000000000 +0000 @@ -0,0 +1,84 @@ +#!/bin/bash + +show_help() { + echo "usage: is-core" + echo " is-core16" + echo " is-core18" + echo " is-core20" + echo " is-classic" + echo " is-trusty" + echo " is-xenial" + echo " is-bionic" + echo " is-focal" + echo "" + echo "Get general information about the current system" +} + +is_core() { + [[ "$SPREAD_SYSTEM" == ubuntu-core-* ]] +} + +is_core16() { + [[ "$SPREAD_SYSTEM" == ubuntu-core-16-* ]] +} + +is_core18() { + [[ "$SPREAD_SYSTEM" == ubuntu-core-18-* ]] +} + +is_core20() { + [[ "$SPREAD_SYSTEM" == ubuntu-core-20-* ]] +} + +is_classic() { + ! is_core +} + +is_trusty() { + grep -qFx 'ID=ubuntu' /etc/os-release && grep -qFx 'VERSION_ID="14.04"' /etc/os-release +} + +is_xenial() { + grep -qFx 'UBUNTU_CODENAME=xenial' /etc/os-release +} + +is_bionic() { + grep -qFx 'UBUNTU_CODENAME=bionic' /etc/os-release +} + +is_focal() { + grep -qFx 'UBUNTU_CODENAME=focal' /etc/os-release +} + +main() { + if [ $# -eq 0 ]; then + show_help + exit 0 + fi + + local subcommand="$1" + local action= + while [ $# -gt 0 ]; do + case "$1" in + -h|--help) + show_help + exit 0 + ;; + *) + action=$(echo "$subcommand" | tr '-' '_') + shift + break + ;; + esac + done + + if [ -z "$(declare -f "$action")" ]; then + echo "os.query: no such command: $subcommand" >&2 + show_help + exit 1 + fi + + "$action" "$@" +} + +main "$@" diff -Nru snapd-2.47.1+20.10.1build1/tests/lib/tools/snaps-state snapd-2.48+21.04/tests/lib/tools/snaps-state --- snapd-2.47.1+20.10.1build1/tests/lib/tools/snaps-state 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48+21.04/tests/lib/tools/snaps-state 2020-11-19 16:51:02.000000000 +0000 @@ -0,0 +1,93 @@ +#!/bin/bash -e + +show_help() { + echo "usage: pack-local " + echo " install-local [OPTIONS]" + echo " install-local-as [OPTIONS]" + echo "" + echo "Available options:" + echo " --devmode --jailmode --classic" + echo "" + echo "Pack and install commands save the packed snap for future uses," + echo "which is reused on the following calls." + echo "The paths for locating the sources of the snaps to either pack or" + echo "install are the local path and then 'tests/lib/snaps/'" +} + +pack_local() { + local SNAP_NAME="$1" + local SNAP_DIR="${2:-$TESTSLIB/snaps/${SNAP_NAME}}" + local SNAP_VERSION="${3:-1.0}" + + local META_FILE META_NAME SNAP_FILE + META_FILE="$SNAP_DIR/meta/snap.yaml" + if [ ! -f "$META_FILE" ]; then + echo "snap.yaml file not found for $SNAP_NAME snap" + return 1 + fi + META_NAME="$(grep '^name:' "$META_FILE" | awk '{ print $2 }' | tr -d ' ')" + SNAP_FILE="${SNAP_DIR}/${META_NAME}_${SNAP_VERSION}_all.snap" + # assigned in a separate step to avoid hiding a failure + if [ ! -f "$SNAP_FILE" ]; then + snap pack "$SNAP_DIR" "$SNAP_DIR" >/dev/null + fi + # echo the snap name + if [ -f "$SNAP_FILE" ]; then + echo "$SNAP_FILE" + else + find "$SNAP_DIR" -name "${META_NAME}_*.snap"| head -n1 + fi +} + +install_local() { + local SNAP_NAME="$1" + local SNAP_DIR="$TESTSLIB/snaps/${SNAP_NAME}" + shift + + if [ -d "$SNAP_NAME" ]; then + SNAP_DIR="$PWD/$SNAP_NAME" + fi + SNAP_FILE=$(pack_local "$SNAP_NAME" "$SNAP_DIR") + + snap install --dangerous "$@" "$SNAP_FILE" +} + +install_local_as() { + local snap="$1" + local name="$2" + shift 2 + install_local "$snap" --name "$name" "$@" +} + +main() { + if [ $# -eq 0 ]; then + show_help + exit 0 + fi + + local subcommand="$1" + local action= + while [ $# -gt 0 ]; do + case "$1" in + -h|--help) + show_help + exit 0 + ;; + *) + action=$(echo "$subcommand" | tr '-' '_') + shift + break + ;; + esac + done + + if [ -z "$(declare -f "$action")" ]; then + echo "snaps-state: no such command: $subcommand" + show_help + exit 1 + fi + + "$action" "$@" +} + +main "$@" diff -Nru snapd-2.47.1+20.10.1build1/tests/lib/tools/suite/journal-state/task.yaml snapd-2.48+21.04/tests/lib/tools/suite/journal-state/task.yaml --- snapd-2.47.1+20.10.1build1/tests/lib/tools/suite/journal-state/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/lib/tools/suite/journal-state/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -13,7 +13,8 @@ # Check that the test is correctly started in the journal log "$TESTSTOOLS"/journal-state start-new-log echo "Add some extra logs" | systemd-cat -t snapd-test - retry --wait 1 --attempts 30 sh -c "test $(journalctl --cursor "$cursor1" | grep -c "$SPREAD_JOB") -eq 1" + # shellcheck disable=SC2016 + retry --wait 1 --attempts 10 sh -c 'test $(journalctl --cursor '\'"$cursor1"\'' | grep -c '"$SPREAD_JOB"') -eq 1' # Check that the subcommand check-log-started works # The subcommand check-log-started could fail if the log does not contain the current SPREAD_JOB information @@ -21,16 +22,18 @@ # Check that the subcommand get-log works echo "TEST-XX1" | systemd-cat -t snapd-test - retry --wait 1 --attempts 30 sh -c "test $("$TESTSTOOLS"/journal-state get-log | grep -c TEST-XX1) -eq 1" + # shellcheck disable=SC2016 + retry --wait 1 --attempts 10 sh -c 'test "$("$TESTSTOOLS"/journal-state get-log | grep -c TEST-XX1)" -eq 1' # Check that the subcommand get-last-cursor works echo "TEST-XX2" | systemd-cat -t snapd-test echo "Add some extra logs" | systemd-cat -t different-test - retry --wait 1 --attempts 30 sh -c "test $("$TESTSTOOLS"/journal-state get-log | grep -c TEST-XX2) -eq 1" + # shellcheck disable=SC2016 + retry --wait 1 --attempts 10 sh -c 'test "$("$TESTSTOOLS"/journal-state get-log | grep -c TEST-XX2)" -eq 1' cursor2=$("$TESTSTOOLS"/journal-state get-last-cursor) test "$("$TESTSTOOLS"/journal-state get-log-from-cursor "$cursor2" | grep -c "TEST-XX1")" -eq 0 - # Check the the subcommand match-log works + # Check that the subcommand match-log works "$TESTSTOOLS"/journal-state match-log TEST-XX1 not "$TESTSTOOLS"/journal-state match-log TEST-XX1 -u testservice diff -Nru snapd-2.47.1+20.10.1build1/tests/lib/tools/suite/tests.backup/task.yaml snapd-2.48+21.04/tests/lib/tools/suite/tests.backup/task.yaml --- snapd-2.47.1+20.10.1build1/tests/lib/tools/suite/tests.backup/task.yaml 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48+21.04/tests/lib/tools/suite/tests.backup/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -0,0 +1,67 @@ +summary: tests for the tests.backup tool + +prepare: | + # It is needed to save the test backup "${PWD}.tar" which is created during + # the generic prepare phase. + mv "${PWD}.tar" "${PWD}.tar.back" + +restore: | + # The backup "${PWD}.tar.back" needs to be restored to the initial + # "${PWD}.tar" during the test restore phase. + mv "${PWD}.tar.back" "${PWD}.tar" + +execute: | + # Without any arguments a help message is printed. + tests.backup | MATCH "usage: tests.backup prepare" + tests.backup | MATCH " tests.backup restore" + + # Both -h and --help are recognized. + tests.backup --help | MATCH "usage: tests.backup" + tests.backup -h | MATCH "usage: tests.backup" + + # Unknown commands and options are reported + tests.backup --foo 2>&1 | MATCH "tests.backup: unknown option --foo" + tests.backup foo 2>&1 | MATCH "tests.backup: unknown command foo" + + # Create a file and a directory inside the current path + touch testfile-old + mkdir testdir-old + + # Check prepare creates a backup for the current directory + test ! -f "${PWD}.tar" + tests.backup prepare + test -f "${PWD}.tar" + + # Delete old data and create new data + rm "${PWD}/testfile-old" + rm -r testdir-old + touch testfile-new + mkdir testdir-new + + # Restore the backup + tests.backup restore + + # Check old files and directories are restored after restore + test -e testfile-old + test -e testdir-old + + # Check new files and directories are gone after restore + test ! -e testfile-new + test ! -e testdir-new + + # Check the backup file is gone + test ! -e "${PWD}.tar" + + # Validate restore cannot be called if backup file does not exist + tests.backup restore 2>&1 | MATCH "tests.backup: cannot restore ${PWD}.tar, the file does not exist" + + # Check the tool support a path to prepare and restore + TMP_DIR=$(mktemp -d) + tests.backup prepare "$TMP_DIR" + test -e "${TMP_DIR}.tar" + tests.backup restore "$TMP_DIR" + test ! -e "${TMP_DIR}.tar" + + # Validate prepare cannot be called if backup dir does not exist + rm -rf "$TMP_DIR" + tests.backup prepare "$TMP_DIR" 2>&1 | MATCH "tests.backup: cannot backup $TMP_DIR, not a directory" diff -Nru snapd-2.47.1+20.10.1build1/tests/lib/tools/suite/tests.cleanup/task.yaml snapd-2.48+21.04/tests/lib/tools/suite/tests.cleanup/task.yaml --- snapd-2.47.1+20.10.1build1/tests/lib/tools/suite/tests.cleanup/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/lib/tools/suite/tests.cleanup/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -11,6 +11,7 @@ # Without any arguments a help message is printed. tests.cleanup | MATCH "usage: tests.cleanup prepare" tests.cleanup | MATCH " tests.cleanup defer \[args\]" + tests.cleanup | MATCH " tests.cleanup pop" tests.cleanup | MATCH " tests.cleanup restore" # Both -h and --help are also recognized. @@ -44,7 +45,7 @@ tests.cleanup prepare tests.cleanup restore not tests.cleanup restore - tests.cleanup restore 2>&1 | MATCH "tests.cleanup: cannot restore, must call tests.prepare first" + tests.cleanup restore 2>&1 | MATCH "tests.cleanup: cannot restore, must call tests.cleanup prepare first" # Deferred commands are appended to defer.sh tests.cleanup prepare @@ -72,3 +73,26 @@ tests.invariant check leftover-defer-sh 2>&1 | MATCH "tests.invariant: leftover defer.sh script" rm -f defer.sh tests.invariant check leftover-defer-sh + + # Deferred commands can be popped and executed one by one. This is useful + # to ensure correctness in case of failure while still allowing precise + # resource management. + tests.cleanup prepare + tests.cleanup defer echo popped + tests.cleanup pop | MATCH popped + + # Popping removes the last command from the stack + tests.cleanup defer echo cmd-a + tests.cleanup defer echo cmd-b + tests.cleanup defer echo cmd-c + tests.cleanup pop | MATCH cmd-c + diff -u defer.sh - <&1 | MATCH 'tests.cleanup: cannot pop, cleanup stack is empty' + not tests.cleanup pop diff -Nru snapd-2.47.1+20.10.1build1/tests/lib/tools/tests.backup snapd-2.48+21.04/tests/lib/tools/tests.backup --- snapd-2.47.1+20.10.1build1/tests/lib/tools/tests.backup 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48+21.04/tests/lib/tools/tests.backup 2020-11-19 16:51:02.000000000 +0000 @@ -0,0 +1,69 @@ +#!/bin/bash -e +# Tool used to backup/restore a specific directory +# It is used by the test tool to make sure each test +# leaves the test directory as was initially + +show_help() { + echo "usage: tests.backup prepare [PATH]" + echo " tests.backup restore [PATH]" +} + +cmd_prepare() { + local BACKUP_PATH=$1 + + if [ ! -d "$BACKUP_PATH" ]; then + echo "tests.backup: cannot backup $BACKUP_PATH, not a directory" >&2 + exit 1 + fi + tar cf "${BACKUP_PATH}.tar" "$BACKUP_PATH" +} + +cmd_restore() { + local BACKUP_PATH=$1 + if [ -f "${BACKUP_PATH}.tar" ]; then + # Find all the files in the path $BACKUP_PATH and delete them + # This command deletes also the hidden files + find "${BACKUP_PATH}" -maxdepth 1 -mindepth 1 -exec rm -rf {} \; + tar -C/ -xf "${BACKUP_PATH}.tar" + rm "${BACKUP_PATH}.tar" + else + echo "tests.backup: cannot restore ${BACKUP_PATH}.tar, the file does not exist" >&2 + exit 1 + fi +} + +main() { + if [ $# -eq 0 ]; then + show_help + exit 0 + fi + + while [ $# -gt 0 ]; do + case "$1" in + -h|--help) + show_help + exit + ;; + prepare) + local BACKUP_PATH="${2:-$(pwd)}" + cmd_prepare "$BACKUP_PATH" + exit + ;; + restore) + local BACKUP_PATH="${2:-$(pwd)}" + cmd_restore "$BACKUP_PATH" + exit + ;; + -*) + echo "tests.backup: unknown option $1" >&2 + exit 1 + ;; + *) + echo "tests.backup: unknown command $1" >&2 + exit 1 + ;; + esac + done +} + +main "$@" diff -Nru snapd-2.47.1+20.10.1build1/tests/lib/tools/tests.cleanup snapd-2.48+21.04/tests/lib/tools/tests.cleanup --- snapd-2.47.1+20.10.1build1/tests/lib/tools/tests.cleanup 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/lib/tools/tests.cleanup 2020-11-19 16:51:02.000000000 +0000 @@ -3,7 +3,17 @@ show_help() { echo "usage: tests.cleanup prepare" echo " tests.cleanup defer [args]" + echo " tests.cleanup pop" echo " tests.cleanup restore" + echo + echo "COMMANDS:" + echo " prepare: establishes a cleanup stack" + echo " restore: invokes all cleanup commands in reverse order" + echo " defer: pushes a command onto the cleanup stack" + echo " pop: invoke the most recently deferred command, discarding it" + echo + echo "The defer and pop commands can be to establish temporary" + echo "cleanup handler and remove it, in the case of success" } cmd_prepare() { @@ -17,26 +27,43 @@ cmd_defer() { if [ ! -e defer.sh ]; then - echo "tests.cleanup: cannot defer, must call tests.prepare first" >&2 + echo "tests.cleanup: cannot defer, must call tests.cleanup prepare first" >&2 exit 1 fi echo "$*" >> defer.sh } +run_one_cmd() { + CMD="$1" + set +e + sh -ec "$CMD" + RET=$? + set -e + if [ $RET -ne 0 ]; then + echo "tests.cleanup: deferred command \"$CMD\" failed with exit code $RET" + exit $RET + fi +} + +cmd_pop() { + if [ ! -s defer.sh ]; then + echo "tests.cleanup: cannot pop, cleanup stack is empty" >&2 + exit 1 + fi + head -n-1 defer.sh >defer.sh.pop + trap "mv defer.sh.pop defer.sh" EXIT + tail -n-1 defer.sh | while read -r CMD; do + run_one_cmd "$CMD" + done +} + cmd_restore() { if [ ! -e defer.sh ]; then - echo "tests.cleanup: cannot restore, must call tests.prepare first" >&2 + echo "tests.cleanup: cannot restore, must call tests.cleanup prepare first" >&2 exit 1 fi tac defer.sh | while read -r CMD; do - set +e - sh -ec "$CMD" - RET=$? - set -e - if [ $RET -ne 0 ]; then - echo "tests.cleanup: deferred command \"$CMD\" failed with exit code $RET" - exit $RET - fi + run_one_cmd "$CMD" done rm -f defer.sh } @@ -62,6 +89,11 @@ cmd_defer "$@" exit ;; + pop) + shift + cmd_pop "$@" + exit + ;; restore) shift cmd_restore diff -Nru snapd-2.47.1+20.10.1build1/tests/lib/tools/tests.session snapd-2.48+21.04/tests/lib/tools/tests.session --- snapd-2.47.1+20.10.1build1/tests/lib/tools/tests.session 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/lib/tools/tests.session 2020-11-19 16:51:02.000000000 +0000 @@ -363,8 +363,9 @@ trap - INT TERM QUIT # Kill dbus-monitor that otherwise runs until it notices the pipe is no longer -# connected, which happens after a longer while. -kill $dbus_monitor_pid || true +# connected, which happens after a longer while. Redirect stderr to /dev/null +# to avoid upsetting tests which are sensitive to stderr, e.g. tests/main/document-portal-activation +kill $dbus_monitor_pid 2>/dev/null || true wait $dbus_monitor_pid 2>/dev/null || true wait $awk_pid diff -Nru snapd-2.47.1+20.10.1build1/tests/lib/uc20-create-partitions/main.go snapd-2.48+21.04/tests/lib/uc20-create-partitions/main.go --- snapd-2.47.1+20.10.1build1/tests/lib/uc20-create-partitions/main.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/lib/uc20-create-partitions/main.go 2020-11-19 16:51:02.000000000 +0000 @@ -53,8 +53,8 @@ encryptionKey secboot.EncryptionKey } -func (o *simpleObserver) Observe(op gadget.ContentOperation, affectedStruct *gadget.LaidOutStructure, root, dst string, data *gadget.ContentChange) (bool, error) { - return true, nil +func (o *simpleObserver) Observe(op gadget.ContentOperation, affectedStruct *gadget.LaidOutStructure, root, dst string, data *gadget.ContentChange) (gadget.ContentChangeAction, error) { + return gadget.ChangeApply, nil } func (o *simpleObserver) ChosenEncryptionKey(key secboot.EncryptionKey) { @@ -91,13 +91,23 @@ Mount: args.Mount, Encrypt: args.Encrypt, } - err = installRun(args.Positional.GadgetRoot, args.Positional.Device, options, obs) + installSideData, err := installRun(args.Positional.GadgetRoot, args.Positional.Device, options, obs) if err != nil { panic(err) } if args.Encrypt { - if err := ioutil.WriteFile("unsealed-key", obs.encryptionKey[:], 0644); err != nil { + if installSideData == nil || installSideData.KeysForRoles == nil { + panic("expected encryption keys") + } + dataKey := installSideData.KeysForRoles[gadget.SystemData] + if dataKey == nil { + panic("ubuntu-data encryption key is unset") + } + if err := ioutil.WriteFile("unsealed-key", dataKey.Key[:], 0644); err != nil { + panic(err) + } + if err := ioutil.WriteFile("recovery-key", dataKey.RecoveryKey[:], 0644); err != nil { panic(err) } } diff -Nru snapd-2.47.1+20.10.1build1/tests/main/abort/task.yaml snapd-2.48+21.04/tests/main/abort/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/abort/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/abort/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -16,9 +16,7 @@ echo "Abort with valid id - error" subdirPath="$SNAP_MOUNT_DIR/$SNAP_NAME/current/foo" mkdir -p "$subdirPath" - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB/snaps.sh" - if install_local "$SNAP_NAME"; then + if "$TESTSTOOLS"/snaps-state install-local "$SNAP_NAME"; then echo "install should fail when the target directory exists" exit 1 fi @@ -31,7 +29,7 @@ rm -rf "$subdirPath" echo "Abort with valid id - done" - install_local "$SNAP_NAME" + "$TESTSTOOLS"/snaps-state install-local "$SNAP_NAME" idPattern="\\d+(?= +Done.*?Install \"$SNAP_NAME\" snap)" id=$(snap changes | grep -Pzo "$idPattern") if snap abort "$id"; then diff -Nru snapd-2.47.1+20.10.1build1/tests/main/alias/task.yaml snapd-2.48+21.04/tests/main/alias/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/alias/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/alias/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -1,9 +1,7 @@ summary: Check snap alias and snap unalias prepare: | - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB/snaps.sh" - install_local aliases + "$TESTSTOOLS"/snaps-state install-local aliases execute: | #shellcheck source=tests/lib/dirs.sh diff -Nru snapd-2.47.1+20.10.1build1/tests/main/apt-hooks/task.yaml snapd-2.48+21.04/tests/main/apt-hooks/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/apt-hooks/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/apt-hooks/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -3,8 +3,6 @@ # apt hook only available on 18.04+ and aws-cli only for amd64 systems: [ubuntu-18.04-64, ubuntu-2*-64] -manual: true - debug: | ls -lh /var/cache/snapd # low tech dump of db diff -Nru snapd-2.47.1+20.10.1build1/tests/main/auto-refresh/task.yaml snapd-2.48+21.04/tests/main/auto-refresh/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/auto-refresh/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/auto-refresh/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -19,16 +19,13 @@ #shellcheck source=tests/lib/dirs.sh . "$TESTSLIB/dirs.sh" - #shellcheck source=tests/lib/systems.sh - . "$TESTSLIB/systems.sh" - echo "Auto refresh information is shown" output=$(snap refresh --time) for expected in ^schedule: ^last: ^next:; do echo "$output" | MATCH "$expected" done - if is_core_system; then + if os.query is-core; then # no holding echo "$output" | not MATCH "^hold:" else diff -Nru snapd-2.47.1+20.10.1build1/tests/main/base-policy/task.yaml snapd-2.48+21.04/tests/main/base-policy/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/base-policy/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/base-policy/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -2,14 +2,12 @@ prepare: | echo "Given basic snaps are installed" - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh - install_local test-snapd-sh-core - install_local test-snapd-sh-core18 + "$TESTSTOOLS"/snaps-state install-local test-snapd-sh-core + "$TESTSTOOLS"/snaps-state install-local test-snapd-sh-core18 # test-snapd-core18 is only available on amd64 if [ "$(uname -p)" = "x86_64" ]; then - install_local test-snapd-sh-other18 + "$TESTSTOOLS"/snaps-state install-local test-snapd-sh-other18 fi execute: | diff -Nru snapd-2.47.1+20.10.1build1/tests/main/base-snaps/task.yaml snapd-2.48+21.04/tests/main/base-snaps/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/base-snaps/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/base-snaps/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -3,21 +3,18 @@ systems: [-opensuse-*] execute: | - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh - echo "Ensure a snap that requires a unavailable base snap can not be installed" - if install_local test-snapd-requires-base; then + if "$TESTSTOOLS"/snaps-state install-local test-snapd-requires-base; then echo "ERROR: test-snapd-requires-base should not be installable without test-snapd-base" exit 1 fi echo "Ensure a base snap can be installed" - install_local test-snapd-base + "$TESTSTOOLS"/snaps-state install-local test-snapd-base snap list | MATCH test-snapd-base echo "With test-snapd-base installed we now can install test-snapd-requires-base" - install_local test-snapd-requires-base + "$TESTSTOOLS"/snaps-state install-local test-snapd-requires-base snap list | MATCH test-snapd-requires-base echo "Ensure the bare base works" diff -Nru snapd-2.47.1+20.10.1build1/tests/main/boot-state/task.yaml snapd-2.48+21.04/tests/main/boot-state/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/boot-state/task.yaml 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48+21.04/tests/main/boot-state/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -0,0 +1,78 @@ +summary: smoke test for the boot-state tool + +execute: | + + # Check help + "$TESTSTOOLS"/boot-state | MATCH "usage: boot-state bootenv" + "$TESTSTOOLS"/boot-state -h | MATCH "usage: boot-state bootenv" + "$TESTSTOOLS"/boot-state --help | MATCH "usage: boot-state bootenv" + + # check the bootenv command + case "$SPREAD_SYSTEM" in + ubuntu-core-16-*|ubuntu-core-18-*) + # check snap_core and snap_kernel vars are set in bootnev + "$TESTSTOOLS"/boot-state bootenv show | MATCH 'snap_core=core.*.snap' + "$TESTSTOOLS"/boot-state bootenv show | MATCH 'snap_kernel=.*-kernel_.*.snap' + ;; + ubuntu-core-20-*) + # check kernel_status var is set in bootnev + "$TESTSTOOLS"/boot-state bootenv show | MATCH 'kernel_status=' + ;; + *) + # check bootnev command can be called + "$TESTSTOOLS"/boot-state bootenv show + ;; + esac + + # check the get-boot-path command + case "$SPREAD_SYSTEM" in + ubuntu-core-16-arm-*|ubuntu-core-18-arm-*|ubuntu-core-20-arm-*) + "$TESTSTOOLS"/boot-state boot-path | MATCH "/boot/uboot/" + ;; + fedora-*|opensuse-*|amazon-*|centos-*) + "$TESTSTOOLS"/boot-state boot-path | MATCH "/boot/grub2/" + ;; + *) + "$TESTSTOOLS"/boot-state boot-path | MATCH "/boot/grub/" + ;; + esac + + # check a new var can be set in bootenv + "$TESTSTOOLS"/boot-state bootenv show | NOMATCH 'snap_test_1=' + "$TESTSTOOLS"/boot-state bootenv set snap_test_1 test_1 + "$TESTSTOOLS"/boot-state bootenv show | MATCH 'snap_test_1=test_1$' + + # check a var can be showed in bootenv + "$TESTSTOOLS"/boot-state bootenv set snap_test_2 test_2 + "$TESTSTOOLS"/boot-state bootenv show snap_test_1 | MATCH 'test_1' + "$TESTSTOOLS"/boot-state bootenv show snap_test_1 | NOMATCH 'test_2' + + # check a var can be set even if it is already defined in bootenv + "$TESTSTOOLS"/boot-state bootenv set snap_test_1 test_3 + "$TESTSTOOLS"/boot-state bootenv show snap_test_1 | MATCH 'test_3' + test "$("$TESTSTOOLS"/boot-state bootenv show | grep -c snap_test_1)" -eq 1 + + # check an existing var can be unset in bootenv + "$TESTSTOOLS"/boot-state bootenv unset snap_test_1 + "$TESTSTOOLS"/boot-state bootenv show | NOMATCH 'snap_test_1=' + "$TESTSTOOLS"/boot-state bootenv show | MATCH 'snap_test_2=test_2$' + "$TESTSTOOLS"/boot-state bootenv unset snap_test_2 + "$TESTSTOOLS"/boot-state bootenv show | NOMATCH 'snap_test_2=' + + # check an inexistent var can be unset in bootenv + "$TESTSTOOLS"/boot-state bootenv unset snap_boot_no_exist + + # wait-core-post-boot should finish inmediatly + "$TESTSTOOLS"/boot-state wait-core-post-boot + # TODO: Test the scenario when the core reaches the timeout running wait-core-post-boot + + # check bootenv needs a subcommand + "$TESTSTOOLS"/boot-state bootenv 2>&1 | MATCH "boot-state: unsupported bootenv sub-command" + "$TESTSTOOLS"/boot-state bootenv noexist 2>&1 | MATCH "boot-state: unsupported bootenv sub-command noexist" + + # check var and value are needed to call bootenv set + "$TESTSTOOLS"/boot-state bootenv set 2>&1 | MATCH "boot-state: variable and value required to set in bootenv" + "$TESTSTOOLS"/boot-state bootenv set justvar 2>&1 | MATCH "boot-state: variable and value required to set in bootenv" + + # check var is needed to call bootenv unset + "$TESTSTOOLS"/boot-state bootenv unset 2>&1 | MATCH "boot-state: variable required to unset from bootenv" diff -Nru snapd-2.47.1+20.10.1build1/tests/main/cgroup-freezer/task.yaml snapd-2.48+21.04/tests/main/cgroup-freezer/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/cgroup-freezer/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/cgroup-freezer/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -8,9 +8,7 @@ placed into the appropriate hierarchy under the freezer cgroup. prepare: | - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh - install_local test-snapd-sh + "$TESTSTOOLS"/snaps-state install-local test-snapd-sh restore: | rmdir /sys/fs/cgroup/freezer/snap.test-snapd-sh || true diff -Nru snapd-2.47.1+20.10.1build1/tests/main/cgroup-tracking-failure/task.yaml snapd-2.48+21.04/tests/main/cgroup-tracking-failure/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/cgroup-tracking-failure/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/cgroup-tracking-failure/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -19,9 +19,7 @@ # This feature depends on the release-app-awareness feature snap set core experimental.refresh-app-awareness=true tests.cleanup defer snap unset core experimental.refresh-app-awareness - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh - install_local test-snapd-sh + "$TESTSTOOLS"/snaps-state install-local test-snapd-sh tests.cleanup defer snap remove --purge test-snapd-sh restore: | tests.cleanup restore diff -Nru snapd-2.47.1+20.10.1build1/tests/main/classic-custom-device-reg/task.yaml snapd-2.48+21.04/tests/main/classic-custom-device-reg/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/classic-custom-device-reg/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/classic-custom-device-reg/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -1,7 +1,8 @@ summary: | Test gadget customized device initialisation and registration also on classic -systems: [-ubuntu-core-*] +# ubuntu-14.04: systemd-run not supported +systems: [-ubuntu-core-*, -ubuntu-14.04*] environment: SEED_DIR: /var/lib/snapd/seed @@ -13,8 +14,6 @@ echo "This test needs test keys to be trusted" exit fi - #shellcheck source=tests/lib/systemd.sh - . "$TESTSLIB/systemd.sh" snap pack "$TESTSLIB/snaps/classic-gadget" snap download "--$CORE_CHANNEL" core @@ -41,18 +40,17 @@ echo "Copy the needed snaps to $SEED_DIR/snaps" cp ./core_*.snap "$SEED_DIR/snaps/core.snap" cp ./classic-gadget_1.0_all.snap "$SEED_DIR/snaps/classic-gadget.snap" + # start fake device svc - systemd_create_and_start_unit fakedevicesvc "$(command -v fakedevicesvc) localhost:11029" + #shellcheck disable=SC2148 + systemd-run --unit fakedevicesvc fakedevicesvc localhost:11029 restore: | if [ "$TRUST_TEST_KEYS" = "false" ]; then echo "This test needs test keys to be trusted" exit fi - #shellcheck source=tests/lib/systemd.sh - . "$TESTSLIB/systemd.sh" - systemctl stop snapd.service snapd.socket - systemd_stop_and_destroy_unit fakedevicesvc + systemctl stop snapd.service snapd.socket fakedevicesvc rm -rf "$SEED_DIR" rm -f -- *.snap diff -Nru snapd-2.47.1+20.10.1build1/tests/main/classic-prepare-image/task.yaml snapd-2.48+21.04/tests/main/classic-prepare-image/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/classic-prepare-image/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/classic-prepare-image/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -1,6 +1,7 @@ summary: Check that prepare-image --classic works. -systems: [-ubuntu-core-*, -fedora-*, -opensuse-*, -arch-*, -amazon-*, -centos-*] +# ubuntu-14.04: systemd-run not supported +systems: [-ubuntu-core-*, -fedora-*, -opensuse-*, -arch-*, -amazon-*, -centos-*, -ubuntu-14.04*] backends: [-autopkgtest] @@ -40,18 +41,15 @@ cp -ar "$ROOT/$SEED_DIR" "$SEED_DIR" # start fake device svc - systemd_create_and_start_unit fakedevicesvc "$(command -v fakedevicesvc) localhost:11029" + #shellcheck disable=SC2148 + systemd-run --unit fakedevicesvc fakedevicesvc localhost:11029 restore: | if [ "$TRUST_TEST_KEYS" = "false" ]; then echo "This test needs test keys to be trusted" exit fi - - #shellcheck source=tests/lib/systemd.sh - . "$TESTSLIB/systemd.sh" - systemctl stop snapd.service snapd.socket - systemd_stop_and_destroy_unit fakedevicesvc + systemctl stop snapd.service snapd.socket fakedevicesvc rm -rf "$SEED_DIR" systemctl start snapd.socket snapd.service diff -Nru snapd-2.47.1+20.10.1build1/tests/main/classic-prepare-image-no-core/task.yaml snapd-2.48+21.04/tests/main/classic-prepare-image-no-core/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/classic-prepare-image-no-core/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/classic-prepare-image-no-core/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -1,6 +1,7 @@ summary: Check that prepare-image --classic works. -systems: [-ubuntu-core-*, -fedora-*, -opensuse-*, -arch-*, -amazon-*, -centos-*] +# ubuntu-14.04: systemd-run not supported +systems: [-ubuntu-core-*, -fedora-*, -opensuse-*, -arch-*, -amazon-*, -centos-*, -ubuntu-14.04*] backends: [-autopkgtest] @@ -40,18 +41,15 @@ cp -ar "$ROOT/$SEED_DIR" "$SEED_DIR" # start fake device svc - systemd_create_and_start_unit fakedevicesvc "$(command -v fakedevicesvc) localhost:11029" + #shellcheck disable=SC2148 + systemd-run --unit fakedevicesvc fakedevicesvc localhost:11029 restore: | if [ "$TRUST_TEST_KEYS" = "false" ]; then echo "This test needs test keys to be trusted" exit fi - - #shellcheck source=tests/lib/systemd.sh - . "$TESTSLIB/systemd.sh" - systemctl stop snapd.service snapd.socket - systemd_stop_and_destroy_unit fakedevicesvc + systemctl stop snapd.service snapd.socket fakedevicesvc rm -rf "$SEED_DIR" systemctl start snapd.socket snapd.service diff -Nru snapd-2.47.1+20.10.1build1/tests/main/configure-hook-with-network-control/task.yaml snapd-2.48+21.04/tests/main/configure-hook-with-network-control/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/configure-hook-with-network-control/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/configure-hook-with-network-control/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -4,11 +4,8 @@ Test for https://forum.snapcraft.io/t/9452 execute: | - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh - echo "Install test snap with configure hook and network-control" - install_local test-snapd-with-configure-nc + "$TESTSTOOLS"/snaps-state install-local test-snapd-with-configure-nc echo "Ensure configure was run fully" test -e /var/snap/test-snapd-with-configure-nc/common/configure-ran diff -Nru snapd-2.47.1+20.10.1build1/tests/main/config-versions/task.yaml snapd-2.48+21.04/tests/main/config-versions/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/config-versions/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/config-versions/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -16,11 +16,8 @@ fi } - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh - echo "Install test snap" - install_local config-versions + "$TESTSTOOLS"/snaps-state install-local config-versions # sanity snap get config-versions configure-marker | MATCH "executed-for-v1" @@ -29,7 +26,7 @@ snap set config-versions value=100 echo "Install a new version of the test snap" - install_local config-versions-v2 + "$TESTSTOOLS"/snaps-state install-local config-versions-v2 # sanity snap get config-versions configure-marker | MATCH "executed-for-v2" diff -Nru snapd-2.47.1+20.10.1build1/tests/main/core16-base/task.yaml snapd-2.48+21.04/tests/main/core16-base/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/core16-base/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/core16-base/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -5,7 +5,5 @@ snap install --edge core16 echo "Ensure core16 is usable" - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB/snaps.sh" - install_local test-snapd-sh-core16 + "$TESTSTOOLS"/snaps-state install-local test-snapd-sh-core16 test-snapd-sh-core16.sh -c "echo hello" | MATCH hello diff -Nru snapd-2.47.1+20.10.1build1/tests/main/core16-provided-by-core/task.yaml snapd-2.48+21.04/tests/main/core16-provided-by-core/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/core16-provided-by-core/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/core16-provided-by-core/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -20,9 +20,7 @@ exit 1 fi - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB/snaps.sh" - install_local test-snapd-sh-core16 + "$TESTSTOOLS"/snaps-state install-local test-snapd-sh-core16 echo "and core16 was not pulled in" not snap list core16 diff -Nru snapd-2.47.1+20.10.1build1/tests/main/core18-configure-hook/task.yaml snapd-2.48+21.04/tests/main/core18-configure-hook/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/core18-configure-hook/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/core18-configure-hook/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -15,11 +15,8 @@ snap wait system seed.loaded execute: | - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh - echo "Install test snap" - install_local test-snapd-with-configure-core18 + "$TESTSTOOLS"/snaps-state install-local test-snapd-with-configure-core18 snap list | MATCH core18 if snap list | grep -E -q "^core "; then diff -Nru snapd-2.47.1+20.10.1build1/tests/main/core18-with-hooks/task.yaml snapd-2.48+21.04/tests/main/core18-with-hooks/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/core18-with-hooks/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/core18-with-hooks/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -4,9 +4,7 @@ # FIXME: we need at least beta of core18 for this to work snap install --beta core18 - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB/snaps.sh" - install_local test-snapd-snapctl-core18 + "$TESTSTOOLS"/snaps-state install-local test-snapd-snapctl-core18 journalctl -u test-snapd-snapctl-core18.service diff -Nru snapd-2.47.1+20.10.1build1/tests/main/core-snap-refresh/task.yaml snapd-2.48+21.04/tests/main/core-snap-refresh/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/core-snap-refresh/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/core-snap-refresh/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -11,9 +11,6 @@ #shellcheck source=tests/lib/boot.sh . "$TESTSLIB"/boot.sh - #shellcheck source=tests/lib/systems.sh - . "$TESTSLIB"/systems.sh - if [ "$SPREAD_REBOOT" = 0 ]; then # save current core revision snap list | awk "/^core / {print(\$3)}" > prevBoot @@ -24,7 +21,7 @@ # check boot env vars snap list | awk "/^core / {print(\$3)}" > nextBoot - if is_core_system; then + if os.query is-core; then test "$(bootenv snap_core)" = "core_$(cat prevBoot).snap" test "$(bootenv snap_try_core)" = "core_$(cat nextBoot).snap" fi @@ -41,7 +38,7 @@ sleep 1 done - if is_core_system; then + if os.query is-core; then test "$(bootenv snap_core)" = "core_$(cat nextBoot).snap" test "$(bootenv snap_try_core)" = "" fi diff -Nru snapd-2.47.1+20.10.1build1/tests/main/cwd/task.yaml snapd-2.48+21.04/tests/main/cwd/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/cwd/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/cwd/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -6,9 +6,7 @@ to the view inside the mount namespace. If the directory does not exist the special fallback /var/lib/snapd/void is used. prepare: | - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh - install_local test-snapd-sh + "$TESTSTOOLS"/snaps-state install-local test-snapd-sh restore: | snap remove --purge test-snapd-sh rmdir /tmp/test || true diff -Nru snapd-2.47.1+20.10.1build1/tests/main/dirs-not-shared-with-host/task.yaml snapd-2.48+21.04/tests/main/dirs-not-shared-with-host/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/dirs-not-shared-with-host/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/dirs-not-shared-with-host/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -15,9 +15,7 @@ prepare: | echo "Having installed the test snap" - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh - install_local test-snapd-sh + "$TESTSTOOLS"/snaps-state install-local test-snapd-sh execute: | #shellcheck source=tests/lib/dirs.sh diff -Nru snapd-2.47.1+20.10.1build1/tests/main/disconnect-undo/task.yaml snapd-2.48+21.04/tests/main/disconnect-undo/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/disconnect-undo/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/disconnect-undo/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -5,9 +5,7 @@ remains connected. prepare: | - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB/snaps.sh" - install_local test-disconnect + "$TESTSTOOLS"/snaps-state install-local test-disconnect restore: | rm -f /var/snap/test-disconnect/common/do-not-fail diff -Nru snapd-2.47.1+20.10.1build1/tests/main/docker-smoke/task.yaml snapd-2.48+21.04/tests/main/docker-smoke/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/docker-smoke/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/docker-smoke/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -1,4 +1,4 @@ -summary: Check that the the docker snap works basically +summary: Check that the docker snap works basically # only run on ubuntus for now, the docker snap has issues on non-ubuntu ATM # TODO:UC20: enable for UC20, fails with what looks like apparmor diff -Nru snapd-2.47.1+20.10.1build1/tests/main/document-portal-activation/task.yaml snapd-2.48+21.04/tests/main/document-portal-activation/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/document-portal-activation/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/document-portal-activation/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -24,10 +24,8 @@ - -ubuntu-core-* prepare: | - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh - install_local test-snapd-desktop - install_local test-snapd-sh + "$TESTSTOOLS"/snaps-state install-local test-snapd-desktop + "$TESTSTOOLS"/snaps-state install-local test-snapd-sh rm -f /usr/share/dbus-1/services/fake-document-portal.service tests.session -u test prepare diff -Nru snapd-2.47.1+20.10.1build1/tests/main/enable-disable/task.yaml snapd-2.48+21.04/tests/main/enable-disable/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/enable-disable/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/enable-disable/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -2,9 +2,7 @@ prepare: | echo "Install test-snapd-sh and ensure it runs" - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh - install_local test-snapd-sh + "$TESTSTOOLS"/snaps-state install-local test-snapd-sh test-snapd-sh.sh -c 'echo Hello' | MATCH Hello execute: | diff -Nru snapd-2.47.1+20.10.1build1/tests/main/exitcodes/task.yaml snapd-2.48+21.04/tests/main/exitcodes/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/exitcodes/task.yaml 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48+21.04/tests/main/exitcodes/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -0,0 +1,35 @@ +summary: Checks for snap exit codes + +systems: [ubuntu-1*, ubuntu-2*] + +prepare: | + tests.cleanup prepare + +execute: | + echo "snap command with unknown command return exit code 5" + set +e + snap unknown-command + RET=$? + set -e + test "$RET" -eq 64 + + echo "snap command with unknown flag return exit code 5" + set +e + snap pack --unknown-option + RET=$? + set -e + test "$RET" -eq 64 + + echo "snap command with broken mksquashfs returns exit code 20" + for b in /usr/bin/mksquashfs /snap/core/current/usr/bin/mksquashfs; do + mount -o bind /bin/false "$b" + tests.cleanup defer umount "$b" + done + set +e + snap pack "$TESTSLIB/snaps/test-snapd-sh" + RET=$? + set -e + test "$RET" -eq 20 + +restore: | + tests.cleanup restore diff -Nru snapd-2.47.1+20.10.1build1/tests/main/fakestore-install/task.yaml snapd-2.48+21.04/tests/main/fakestore-install/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/fakestore-install/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/fakestore-install/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -1,5 +1,8 @@ summary: Ensure that the fakestore works +# ubuntu-14.04: systemd-run not supported +systems: [-ubuntu-14.04*] + environment: BLOB_DIR: $(pwd)/fake-store-blobdir diff -Nru snapd-2.47.1+20.10.1build1/tests/main/find-private/task.yaml snapd-2.48+21.04/tests/main/find-private/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/find-private/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/find-private/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -16,8 +16,6 @@ snap logout || true execute: | - #shellcheck source=tests/lib/systems.sh - . "$TESTSLIB"/systems.sh echo "When a snap is private it doesn't show up in the find without login and without specifying private search" snap find test-snapd-private | not MATCH 'test-snapd-private +[0-9]+\.[0-9]+' @@ -27,7 +25,7 @@ echo "Given account store credentials are available" # we don't have expect available on ubuntu-core, so the authenticated check need to be skipped on those systems - if [ -n "$SPREAD_STORE_USER" ] && [ -n "$SPREAD_STORE_PASSWORD" ] && is_classic_system; then + if [ -n "$SPREAD_STORE_USER" ] && [ -n "$SPREAD_STORE_PASSWORD" ] && os.query is-classic; then echo "And the user has logged in" expect -f "$TESTSLIB"/successful_login.exp diff -Nru snapd-2.47.1+20.10.1build1/tests/main/hook-permissions/task.yaml snapd-2.48+21.04/tests/main/hook-permissions/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/hook-permissions/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/hook-permissions/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -8,19 +8,13 @@ and enumerate upower devices. prepare: | - # shellcheck source=tests/lib/systems.sh - . "$TESTSLIB/systems.sh" - if is_core_system; then + if os.query is-core; then snap install test-snapd-upower --edge fi - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh - install_local test-snap + "$TESTSTOOLS"/snaps-state install-local test-snap execute: | - # shellcheck source=tests/lib/systems.sh - . "$TESTSLIB/systems.sh" - if ! is_core_system; then + if ! os.query is-core; then # trigger upowerd to have the service started as AppArmor would deny to # start it in response to a dbus call from inside a snap (because the # service is not started yet and AppArmor doesn't know what confinement diff -Nru snapd-2.47.1+20.10.1build1/tests/main/install-errors/task.yaml snapd-2.48+21.04/tests/main/install-errors/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/install-errors/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/install-errors/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -11,9 +11,7 @@ prepare: | echo "Given a snap with a failing command is installed" - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh - install_local "$SNAP_NAME" + "$TESTSTOOLS"/snaps-state install-local "$SNAP_NAME" execute: | echo "Install unexisting snap prints error" diff -Nru snapd-2.47.1+20.10.1build1/tests/main/install-refresh-remove-hooks/task.yaml snapd-2.48+21.04/tests/main/install-refresh-remove-hooks/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/install-refresh-remove-hooks/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/install-refresh-remove-hooks/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -26,10 +26,7 @@ rm -f "$REMOVE_HOOK_FILE" execute: | - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh - # shellcheck disable=SC2153 - install_local_as snap-hooks "$NAME" + "$TESTSTOOLS"/snaps-state install-local-as snap-hooks "$NAME" echo "Verify configuration value with snap get" snap get "$NAME" installed | MATCH 1 @@ -49,7 +46,7 @@ echo "Verify that install hook is run only once" snap set "$NAME" installed=2 - install_local_as snap-hooks "$NAME" + "$TESTSTOOLS"/snaps-state install-local-as snap-hooks "$NAME" snap get "$NAME" installed | MATCH 2 echo "Verify that pre-refresh hook was executed" @@ -78,7 +75,7 @@ echo "Installing a snap with hooks again" rm -f "$REMOVE_HOOK_FILE" > /dev/null 2>&1 - install_local_as snap-hooks "$NAME" + "$TESTSTOOLS"/snaps-state install-local-as snap-hooks "$NAME" snap connect "$NAME:home" echo "Forcing remove script to fail" @@ -91,7 +88,7 @@ fi echo "Installing a snap with broken install hook aborts the installation" - if install_local snap-hook-broken; then + if "$TESTSTOOLS"/snaps-state install-local snap-hook-broken; then echo "Expected installation to fail" exit 1 fi diff -Nru snapd-2.47.1+20.10.1build1/tests/main/install-remove-multi/task.yaml snapd-2.48+21.04/tests/main/install-remove-multi/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/install-remove-multi/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/install-remove-multi/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -11,10 +11,8 @@ not snap list test-snapd-sh not snap list test-snapd-control-consumer - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB/snaps.sh" echo "Installing of a snap with a desktop file creates the desktop file" - install_local basic-desktop + "$TESTSTOOLS"/snaps-state install-local basic-desktop test -e /var/lib/snapd/desktop/applications/basic-desktop_io.snapcraft.echoecho.desktop echo "Removing a snap with a desktop file removes the desktop file again" snap remove basic-desktop diff -Nru snapd-2.47.1+20.10.1build1/tests/main/install-sideload/task.yaml snapd-2.48+21.04/tests/main/install-sideload/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/install-sideload/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/install-sideload/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -67,12 +67,10 @@ snap install --dangerous ./test-snapd-tools_1.0_all.snap test-snapd-tools.success - # shellcheck source=tests/lib/systems.sh - . "$TESTSLIB/systems.sh" # TODO:UC20: fix to work on uc20 too # The "seed/" dir is on a FAT partition on uc20 so the permissions are # different here. - if ! is_core20_system; then + if ! os.query is-core20; then echo "All snap blobs are 0600" test "$( find /var/lib/snapd/{snaps,cache,seed/snaps}/ -type f -printf '%#m\n' | sort -u | xargs )" = "0600" fi diff -Nru snapd-2.47.1+20.10.1build1/tests/main/install-socket-activation/task.yaml snapd-2.48+21.04/tests/main/install-socket-activation/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/install-socket-activation/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/install-socket-activation/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -4,9 +4,7 @@ This installs a snap which define sockets for systemd socket activation. prepare: | - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB/snaps.sh" - install_local socket-activation + "$TESTSTOOLS"/snaps-state install-local socket-activation restore: | systemctl daemon-reload diff -Nru snapd-2.47.1+20.10.1build1/tests/main/interfaces-account-control/task.yaml snapd-2.48+21.04/tests/main/interfaces-account-control/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/interfaces-account-control/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/interfaces-account-control/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -12,9 +12,7 @@ prepare: | echo "Given a snap declaring a plug on account-control is installed" - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh - install_local "$TSNAP" + "$TESTSTOOLS"/snaps-state install-local "$TSNAP" echo "And the account-control plug is connected" snap connect "$TSNAP":account-control diff -Nru snapd-2.47.1+20.10.1build1/tests/main/interfaces-adb-support/task.yaml snapd-2.48+21.04/tests/main/interfaces-adb-support/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/interfaces-adb-support/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/interfaces-adb-support/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -4,9 +4,7 @@ rules to the system. Before such connection is placed there are no wide-open, write-access rules present. prepare: | - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh - install_local test-snapd-adb-support + "$TESTSTOOLS"/snaps-state install-local test-snapd-adb-support execute: | # If there are any udev rules from existing snaps they are not granting # writable-to-all permission to anything. This part is conditional because diff -Nru snapd-2.47.1+20.10.1build1/tests/main/interfaces-appstream-metadata/task.yaml snapd-2.48+21.04/tests/main/interfaces-appstream-metadata/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/interfaces-appstream-metadata/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/interfaces-appstream-metadata/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -14,9 +14,7 @@ systems: [-ubuntu-core-*] prepare: | - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh - install_local test-snapd-appstream-metadata + "$TESTSTOOLS"/snaps-state install-local test-snapd-appstream-metadata # Set up some dummy Appstream metadata on the host system mkdir -p /usr/share/metainfo diff -Nru snapd-2.47.1+20.10.1build1/tests/main/interfaces-bluez/task.yaml snapd-2.48+21.04/tests/main/interfaces-bluez/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/interfaces-bluez/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/interfaces-bluez/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -4,7 +4,7 @@ The bluez interface allows the bluez service to run and clients to communicate with it. - This test verifies the the bluez snap from the store installs and + This test verifies that the bluez snap from the store installs and we can connect its slot and plug. environment: diff -Nru snapd-2.47.1+20.10.1build1/tests/main/interfaces-broadcom-asic-control/task.yaml snapd-2.48+21.04/tests/main/interfaces-broadcom-asic-control/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/interfaces-broadcom-asic-control/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/interfaces-broadcom-asic-control/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -11,9 +11,7 @@ The broadcom-asic-control interface allow access to broadcom asic kernel module. prepare: | - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh - install_local test-snapd-sh + "$TESTSTOOLS"/snaps-state install-local test-snapd-sh "$TESTSTOOLS"/fs-state mock-file /dev/linux-user-bde "$TESTSTOOLS"/fs-state mock-file /dev/linux-kernel-bde diff -Nru snapd-2.47.1+20.10.1build1/tests/main/interfaces-cli/task.yaml snapd-2.48+21.04/tests/main/interfaces-cli/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/interfaces-cli/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/interfaces-cli/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -6,11 +6,8 @@ PLUG: network prepare: | - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB/snaps.sh" - echo "Given a snap with the $PLUG plug is installed" - install_local "$SNAP_NAME" + "$TESTSTOOLS"/snaps-state install-local "$SNAP_NAME" execute: | expected="(?s)Slot +Plug\\n\ diff -Nru snapd-2.47.1+20.10.1build1/tests/main/interfaces-content-mimic/task.yaml snapd-2.48+21.04/tests/main/interfaces-content-mimic/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/interfaces-content-mimic/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/interfaces-content-mimic/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -11,10 +11,8 @@ SLOT: test-snapd-content-mimic-slot:content prepare: | - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh - install_local test-snapd-content-mimic-plug - install_local test-snapd-content-mimic-slot + "$TESTSTOOLS"/snaps-state install-local test-snapd-content-mimic-plug + "$TESTSTOOLS"/snaps-state install-local test-snapd-content-mimic-slot execute: | # Before the content interface is connected we expect to see certain files diff -Nru snapd-2.47.1+20.10.1build1/tests/main/interfaces-content-mkdir-writable/task.yaml snapd-2.48+21.04/tests/main/interfaces-content-mkdir-writable/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/interfaces-content-mkdir-writable/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/interfaces-content-mkdir-writable/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -23,10 +23,8 @@ prepare: | # Install a pair of snaps that both have two content interfaces as # sub-directories of $SNAP_DATA and $SNAP_COMMON. - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh - install_local test-snapd-content-advanced-plug - install_local test-snapd-content-advanced-slot + "$TESTSTOOLS"/snaps-state install-local test-snapd-content-advanced-plug + "$TESTSTOOLS"/snaps-state install-local test-snapd-content-advanced-slot execute: | # Test that initially there are no mount points on the plug side (because diff -Nru snapd-2.47.1+20.10.1build1/tests/main/interfaces-cups/task.yaml snapd-2.48+21.04/tests/main/interfaces-cups/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/interfaces-cups/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/interfaces-cups/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -9,10 +9,8 @@ server. prepare: | - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh - install_local test-snapd-provider - install_local test-snapd-consumer + "$TESTSTOOLS"/snaps-state install-local test-snapd-provider + "$TESTSTOOLS"/snaps-state install-local test-snapd-consumer if [ -e /run/cups ]; then mv /run/cups /run/cups.orig diff -Nru snapd-2.47.1+20.10.1build1/tests/main/interfaces-daemon-notify/task.yaml snapd-2.48+21.04/tests/main/interfaces-daemon-notify/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/interfaces-daemon-notify/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/interfaces-daemon-notify/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -10,9 +10,7 @@ to systemd through the notify socket prepare: | - # shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB/snaps.sh" - install_local test-snapd-daemon-notify + "$TESTSTOOLS"/snaps-state install-local test-snapd-daemon-notify execute: | echo "The interface is not connected by default" diff -Nru snapd-2.47.1+20.10.1build1/tests/main/interfaces-desktop-document-portal/task.yaml snapd-2.48+21.04/tests/main/interfaces-desktop-document-portal/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/interfaces-desktop-document-portal/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/interfaces-desktop-document-portal/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -10,10 +10,7 @@ systems: [-ubuntu-core-*] prepare: | - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh - - install_local test-snapd-desktop + "$TESTSTOOLS"/snaps-state install-local test-snapd-desktop snap disconnect test-snapd-desktop:desktop restore: | diff -Nru snapd-2.47.1+20.10.1build1/tests/main/interfaces-device-buttons/task.yaml snapd-2.48+21.04/tests/main/interfaces-device-buttons/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/interfaces-device-buttons/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/interfaces-device-buttons/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -5,9 +5,7 @@ devices. prepare: | - # shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB/snaps.sh" - install_local test-snapd-sh + "$TESTSTOOLS"/snaps-state install-local test-snapd-sh # Create device files which are going to be used so simulate a real device # and input data. In case the device already exists, it is going to be diff -Nru snapd-2.47.1+20.10.1build1/tests/main/interfaces-dvb/task.yaml snapd-2.48+21.04/tests/main/interfaces-dvb/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/interfaces-dvb/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/interfaces-dvb/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -4,9 +4,7 @@ The dvb interface allows access to all DVB (Digital Video Broadcasting) devices and APIs. prepare: | - # shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB/snaps.sh" - install_local test-snapd-sh + "$TESTSTOOLS"/snaps-state install-local test-snapd-sh "$TESTSTOOLS"/fs-state mock-dir /dev/dvb/adapter9 "$TESTSTOOLS"/fs-state mock-file /dev/dvb/adapter9/video9 diff -Nru snapd-2.47.1+20.10.1build1/tests/main/interfaces-firewall-control/task.yaml snapd-2.48+21.04/tests/main/interfaces-firewall-control/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/interfaces-firewall-control/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/interfaces-firewall-control/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -1,6 +1,7 @@ summary: Ensure that the firewall-control interface works. -systems: [-fedora-*, -opensuse-*, -arch-*] +# ubuntu-14.04: systemd-run not supported +systems: [-fedora-*, -opensuse-*, -arch-*, -ubuntu-14.04*] details: | The firewall-control interface allows a snap to configure the firewall. @@ -17,27 +18,18 @@ environment: PORT: 8081 - SERVICE_FILE: "./service.sh" SERVICE_NAME: "test-service" REQUEST_FILE: "./request.txt" DESTINATION_IP: "172.26.0.15" prepare: | - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB/snaps.sh" echo "Given a snap declaring a plug on the firewall-control interface is installed" - install_local firewall-control-consumer + "$TESTSTOOLS"/snaps-state install-local firewall-control-consumer echo "And a service is listening" - printf '#!/bin/sh -e\nwhile true; do echo "HTTP/1.1 200 OK\n\nok\n" | nc -l -p %s -w 1; done' "$PORT" > "$SERVICE_FILE" - chmod a+x "$SERVICE_FILE" - #shellcheck source=tests/lib/systemd.sh - . "$TESTSLIB/systemd.sh" - #shellcheck disable=SC2153 - systemd_create_and_start_unit "$SERVICE_NAME" "$(readlink -f "$SERVICE_FILE")" # shellcheck source=tests/lib/network.sh . "$TESTSLIB"/network.sh - wait_listen_port "$PORT" + make_network_service "$SERVICE_NAME" "$PORT" echo "And we store a basic HTTP request" cat > "$REQUEST_FILE" < "$READABLE_FILE" @@ -36,10 +33,7 @@ rm -f "$READABLE_FILE" "$WRITABLE_FILE" "$CREATABLE_FILE" "$HIDDEN_READABLE_FILE" execute: | - #shellcheck source=tests/lib/systems.sh - . "$TESTSLIB"/systems.sh - - if is_core_system; then + if os.query is-core; then echo "The interface is not connected by default" snap interfaces -i home | MATCH '^- +home-consumer:home' diff -Nru snapd-2.47.1+20.10.1build1/tests/main/interfaces-hooks/task.yaml snapd-2.48+21.04/tests/main/interfaces-hooks/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/interfaces-hooks/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/interfaces-hooks/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -5,13 +5,11 @@ PRODUCER_DATA: /var/snap/basic-iface-hooks-producer/common prepare: | - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB/snaps.sh" snap install --devmode jq echo "Install test hooks snaps" - install_local basic-iface-hooks-consumer - install_local basic-iface-hooks-producer + "$TESTSTOOLS"/snaps-state install-local basic-iface-hooks-consumer + "$TESTSTOOLS"/snaps-state install-local basic-iface-hooks-producer restore: | rm -f "$CONSUMER_DATA/prepare-plug-consumer-done" diff -Nru snapd-2.47.1+20.10.1build1/tests/main/interfaces-hostname-control/task.yaml snapd-2.48+21.04/tests/main/interfaces-hostname-control/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/interfaces-hostname-control/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/interfaces-hostname-control/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -5,14 +5,9 @@ prepare: | echo "Install test hostname-control snap" - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB/snaps.sh" - install_local test-snapd-sh + "$TESTSTOOLS"/snaps-state install-local test-snapd-sh execute: | - #shellcheck source=tests/lib/systems.sh - . "$TESTSLIB/systems.sh" - hostname="$(hostname)" echo "The plug is disconnected by default" @@ -26,7 +21,7 @@ [ "$hostname" = "$snap_hostname" ] # On core systems /etc/hostname is not writable - if is_classic_system; then + if os.query is-classic; then echo "And the /etc/hostname file can be written" test-snapd-sh.with-hostname-control-plug -c "echo $hostname > /etc/hostname" fi diff -Nru snapd-2.47.1+20.10.1build1/tests/main/interfaces-joystick/task.yaml snapd-2.48+21.04/tests/main/interfaces-joystick/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/interfaces-joystick/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/interfaces-joystick/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -4,9 +4,7 @@ The joystick interface allows reading and writing to joystick devices. prepare: | - # shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB/snaps.sh" - install_local test-snapd-sh + "$TESTSTOOLS"/snaps-state install-local test-snapd-sh # Create device files which are going to be used so simulate a real device and input data # In case the device already exists, it is going to be backed up diff -Nru snapd-2.47.1+20.10.1build1/tests/main/interfaces-juju-client-observe/task.yaml snapd-2.48+21.04/tests/main/interfaces-juju-client-observe/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/interfaces-juju-client-observe/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/interfaces-juju-client-observe/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -7,9 +7,7 @@ systems: [-ubuntu-core-*] prepare: | - # shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB/snaps.sh" - install_local test-snapd-sh + "$TESTSTOOLS"/snaps-state install-local test-snapd-sh "$TESTSTOOLS"/fs-state mock-dir "$HOME"/.local/share/juju "$TESTSTOOLS"/fs-state mock-file "$HOME"/.local/share/juju/juju.conf diff -Nru snapd-2.47.1+20.10.1build1/tests/main/interfaces-kvm/task.yaml snapd-2.48+21.04/tests/main/interfaces-kvm/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/interfaces-kvm/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/interfaces-kvm/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -4,9 +4,7 @@ The kvm interface allows read/write access to kvm. prepare: | - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh - install_local test-snapd-kvm + "$TESTSTOOLS"/snaps-state install-local test-snapd-kvm if [ -e /dev/kvm ]; then mv /dev/kvm /dev/kvm.bak diff -Nru snapd-2.47.1+20.10.1build1/tests/main/interfaces-locale-control/task.yaml snapd-2.48+21.04/tests/main/interfaces-locale-control/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/interfaces-locale-control/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/interfaces-locale-control/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -22,11 +22,8 @@ fi fi - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB/snaps.sh" - echo "Given a snap declaring a plug on the locale-control interface is installed" - install_local locale-control-consumer + "$TESTSTOOLS"/snaps-state install-local locale-control-consumer mv /etc/default/locale locale.back cat > /etc/default/locale < now.txt diff -Nru snapd-2.47.1+20.10.1build1/tests/main/interfaces-timeserver-control/task.yaml snapd-2.48+21.04/tests/main/interfaces-timeserver-control/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/interfaces-timeserver-control/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/interfaces-timeserver-control/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -42,9 +42,7 @@ esac # Install a snap declaring a plug on timeserver-control - # shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh - install_local test-snapd-timedate-control-consumer + "$TESTSTOOLS"/snaps-state install-local test-snapd-timedate-control-consumer tests.cleanup defer snap remove --purge test-snapd-timedate-control-consumer restore: | diff -Nru snapd-2.47.1+20.10.1build1/tests/main/interfaces-timezone-control/task.yaml snapd-2.48+21.04/tests/main/interfaces-timezone-control/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/interfaces-timezone-control/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/interfaces-timezone-control/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -9,11 +9,8 @@ can access timezone information and update it. prepare: | - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh - # Install a snap declaring a plug on timezone-control - install_local test-snapd-timedate-control-consumer + "$TESTSTOOLS"/snaps-state install-local test-snapd-timedate-control-consumer restore: | # Restore the initial timezone diff -Nru snapd-2.47.1+20.10.1build1/tests/main/interfaces-udev/task.yaml snapd-2.48+21.04/tests/main/interfaces-udev/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/interfaces-udev/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/interfaces-udev/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -11,11 +11,8 @@ more interfaces declare udev snippets. prepare: | - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh - echo "Given a snap declaring a slot with associated udev rules is installed" - install_local modem-manager-consumer + "$TESTSTOOLS"/snaps-state install-local modem-manager-consumer execute: | echo "Then the udev rules files specific to it are created" diff -Nru snapd-2.47.1+20.10.1build1/tests/main/interfaces-upower-observe/task.yaml snapd-2.48+21.04/tests/main/interfaces-upower-observe/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/interfaces-upower-observe/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/interfaces-upower-observe/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -23,9 +23,7 @@ echo "Given a snap declaring a plug on the upower-observe interface is installed" snap install --edge test-snapd-upower-observe-consumer - # shellcheck source=tests/lib/systems.sh - . "$TESTSLIB/systems.sh" - if is_core_system; then + if os.query is-core; then echo "And a snap providing a upower-observe slot is installed" snap install test-snapd-upower --edge fi diff -Nru snapd-2.47.1+20.10.1build1/tests/main/interfaces-x11-unix-socket/task.yaml snapd-2.48+21.04/tests/main/interfaces-x11-unix-socket/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/interfaces-x11-unix-socket/task.yaml 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48+21.04/tests/main/interfaces-x11-unix-socket/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -0,0 +1,51 @@ +summary: ensure that the x11 interface shares UNIX domain sockets +description: | + In addition to the abstract "@/tmp/.X11-unix/X?" socket an X + server listens on, it also listens on a regular UNIX domain socket + in /tmp/.X11-unix. + + The x11 plug will bind mount the socket directory from the slot + providing it: either the host system's /tmp for the implicit + system:x11 slot, or an application snap's private /tmp if it is a + regular slot. + +restore: | + rm -f /tmp/.X11-unix/X0 + +execute: | + echo "Install test snaps" + "$TESTSTOOLS"/snaps-state install-local x11-client + "$TESTSTOOLS"/snaps-state install-local x11-server + + echo "Ensure x11 plug is not connected to implicit slot" + snap disconnect x11-client:x11 + + echo "Connect x11-client to x11-server" + snap connect x11-client:x11 x11-server:x11 + + echo "The snaps can communicate via the unix domain socket in /tmp" + x11-server & + retry -n 4 --wait 0.5 test -e /tmp/snap.x11-server/tmp/.X11-unix/X0 + x11-client | MATCH "Hello from xserver" + + echo "The client cannot remove the unix domain sockets shared with it" + not x11-client.rm -f /tmp/.X11-unix/X0 + + # Ubuntu Core does not have a system:x11 implicit slot + if [[ "$SPREAD_SYSTEM" = ubuntu-core-* ]]; then + exit 0 + fi + + echo "Connect the client snap to the implicit system slot" + snap disconnect x11-client:x11 + snap connect x11-client:x11 + + echo "The client can communicate with the host system X socket" + mkdir -p /tmp/.X11-unix + rm -f /tmp/.X11-unix/X0 + echo "Hello from host system" | nc -l -w 1 -U /tmp/.X11-unix/X0 & + retry -n 4 --wait 0.5 test -e /tmp/.X11-unix/X0 + x11-client | MATCH "Hello from host system" + + echo "The client cannot remove host system sockets either" + not x11-client.rm -f /tmp/.X11-unix/X0 diff -Nru snapd-2.47.1+20.10.1build1/tests/main/interfaces-x11-unix-socket/x11-client/bin/rm.sh snapd-2.48+21.04/tests/main/interfaces-x11-unix-socket/x11-client/bin/rm.sh --- snapd-2.47.1+20.10.1build1/tests/main/interfaces-x11-unix-socket/x11-client/bin/rm.sh 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48+21.04/tests/main/interfaces-x11-unix-socket/x11-client/bin/rm.sh 2020-11-19 16:51:02.000000000 +0000 @@ -0,0 +1,2 @@ +#!/bin/sh +exec rm "$@" diff -Nru snapd-2.47.1+20.10.1build1/tests/main/interfaces-x11-unix-socket/x11-client/bin/xclient.sh snapd-2.48+21.04/tests/main/interfaces-x11-unix-socket/x11-client/bin/xclient.sh --- snapd-2.47.1+20.10.1build1/tests/main/interfaces-x11-unix-socket/x11-client/bin/xclient.sh 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48+21.04/tests/main/interfaces-x11-unix-socket/x11-client/bin/xclient.sh 2020-11-19 16:51:02.000000000 +0000 @@ -0,0 +1,2 @@ +#!/bin/sh +exec nc -w 30 -U /tmp/.X11-unix/X0 diff -Nru snapd-2.47.1+20.10.1build1/tests/main/interfaces-x11-unix-socket/x11-client/meta/snap.yaml snapd-2.48+21.04/tests/main/interfaces-x11-unix-socket/x11-client/meta/snap.yaml --- snapd-2.47.1+20.10.1build1/tests/main/interfaces-x11-unix-socket/x11-client/meta/snap.yaml 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48+21.04/tests/main/interfaces-x11-unix-socket/x11-client/meta/snap.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -0,0 +1,12 @@ +name: x11-client +version: 1.0 +summary: Fake x11 client +description: Fake x11 client + +apps: + x11-client: + command: bin/xclient.sh + plugs: [x11, network] + rm: + command: bin/rm.sh + plugs: [x11] diff -Nru snapd-2.47.1+20.10.1build1/tests/main/interfaces-x11-unix-socket/x11-server/bin/xserver.sh snapd-2.48+21.04/tests/main/interfaces-x11-unix-socket/x11-server/bin/xserver.sh --- snapd-2.47.1+20.10.1build1/tests/main/interfaces-x11-unix-socket/x11-server/bin/xserver.sh 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48+21.04/tests/main/interfaces-x11-unix-socket/x11-server/bin/xserver.sh 2020-11-19 16:51:02.000000000 +0000 @@ -0,0 +1,7 @@ +#!/bin/sh + +mkdir -p /tmp/.X11-unix + +SOCKET=/tmp/.X11-unix/X0 +rm -f $SOCKET +echo "Hello from xserver" | nc -l -w 1 -U $SOCKET diff -Nru snapd-2.47.1+20.10.1build1/tests/main/interfaces-x11-unix-socket/x11-server/meta/snap.yaml snapd-2.48+21.04/tests/main/interfaces-x11-unix-socket/x11-server/meta/snap.yaml --- snapd-2.47.1+20.10.1build1/tests/main/interfaces-x11-unix-socket/x11-server/meta/snap.yaml 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48+21.04/tests/main/interfaces-x11-unix-socket/x11-server/meta/snap.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -0,0 +1,10 @@ +name: x11-server +version: 1.0 +summary: Fake x11 server +description: Fake x11 server + +apps: + x11-server: + command: bin/xserver.sh + plugs: [network] + slots: [x11] diff -Nru snapd-2.47.1+20.10.1build1/tests/main/layout/task.yaml snapd-2.48+21.04/tests/main/layout/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/layout/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/layout/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -7,9 +7,7 @@ hooks get permissions to access those areas. prepare: | - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh - install_local test-snapd-layout + "$TESTSTOOLS"/snaps-state install-local test-snapd-layout debug: | ls -ld /etc || : @@ -18,12 +16,10 @@ ls -ld /etc/demo.cfg || : execute: | - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh for i in $(seq 2); do if [ "$i" -eq 2 ]; then echo "The snap works across refreshes" - install_local test-snapd-layout + "$TESTSTOOLS"/snaps-state install-local test-snapd-layout fi echo "snap declaring layouts doesn't explode on startup" diff -Nru snapd-2.47.1+20.10.1build1/tests/main/listing/task.yaml snapd-2.48+21.04/tests/main/listing/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/listing/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/listing/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -1,12 +1,10 @@ summary: Check snap listings prepare: | - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh - install_local test-snapd-sh + "$TESTSTOOLS"/snaps-state install-local test-snapd-sh snap set system experimental.parallel-instances=true - install_local_as test-snapd-sh test-snapd-sh_foo + "$TESTSTOOLS"/snaps-state install-local-as test-snapd-sh test-snapd-sh_foo restore: | snap set system experimental.parallel-instances=null @@ -75,9 +73,7 @@ snap list | MATCH "^test-snapd-sh_foo +$NUMERIC_VERSION +$SIDELOAD_REV +- +- +- *$" echo "Install test-snapd-sh again" - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh - install_local test-snapd-sh + "$TESTSTOOLS"/snaps-state install-local test-snapd-sh echo "And run snap list --all" output=$(snap list --all | grep 'test-snapd-sh ') diff -Nru snapd-2.47.1+20.10.1build1/tests/main/lxd/task.yaml snapd-2.48+21.04/tests/main/lxd/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/lxd/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/lxd/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -2,10 +2,8 @@ # Only run this on ubuntu 16+, lxd will not work on !ubuntu systems # currently nor on ubuntu 14.04 -# TODO:UC20: enable for UC20 # TODO: enable for ubuntu-16-32 again -# TODO: enable ubuntu-20.10-64 once the image is available -systems: [ubuntu-16.04*64, ubuntu-18.04*, ubuntu-20.04*, ubuntu-core-1*] +systems: [ubuntu-16*, ubuntu-18*, ubuntu-2*, ubuntu-core-*] # autopkgtest run only a subset of tests that deals with the integration # with the distro @@ -27,7 +25,6 @@ REFRESH_APP_AWARENESS_OUTER/snapd_cgroup_neither: false REFRESH_APP_AWARENESS_INNER/snapd_cgroup_neither: false - prepare: | # using apt here is ok because this test only runs on ubuntu echo "Remove any installed debs (some images carry them) to ensure we test the snap" @@ -45,8 +42,8 @@ fi for cont_name in my-nesting-ubuntu my-ubuntu; do - lxd.lxc stop $cont_name --force - lxd.lxc delete $cont_name + lxd.lxc stop $cont_name --force || true + lxd.lxc delete $cont_name || true done snap remove --purge lxd snap remove --purge lxd-demo-server @@ -146,7 +143,14 @@ lxd.lxc exec my-ubuntu -- find /var/run/systemd/generator/ -name container.conf | MATCH "/var/run/systemd/generator/snap-core-.*mount.d/container.conf" lxd.lxc exec my-ubuntu -- test -f /var/run/systemd/generator/snap.mount - # Ensure that we can run lxd as a snap inside a nested container + # Ensure that we can run lxd as a snap inside a container to create a nested + # container + + if [ "$SPREAD_SYSTEM" = "ubuntu-16.04-64" ]; then + # related bug: https://bugs.launchpad.net/snapd/+bug/1892468 + echo "Not running old xenial combination which lacks proper patches" + exit 0 + fi echo "Ensure we can use lxd as a snap inside lxd" lxd.lxc exec my-nesting-ubuntu -- apt autoremove -y lxd diff -Nru snapd-2.47.1+20.10.1build1/tests/main/lxd-mount-units/task.yaml snapd-2.48+21.04/tests/main/lxd-mount-units/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/lxd-mount-units/task.yaml 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48+21.04/tests/main/lxd-mount-units/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -0,0 +1,38 @@ +summary: Test mount units generated for snaps inside lxd container are correct. + +details: | + Test that mount units generated by snapd-generator inside lxd container + are correct for snapfuse. + +# only 20.10+, we want lxd images that come with snaps preinstalled. +systems: [ubuntu-20.10*] + +execute: | + echo "Install lxd" + snap install lxd + + lxd waitready + lxd init --auto + + VERSION_ID="$(. /etc/os-release && echo "$VERSION_ID" )" + lxd.lxc launch --quiet "ubuntu:$VERSION_ID" ubuntu + + lxd.lxc file push --quiet "$GOHOME"/snapd_*.deb "ubuntu/root/" + + DEB=$(basename "$GOHOME"/snapd_*.deb) + lxd.lxc exec ubuntu -- apt install -y /root/"$DEB" + lxd.lxc restart ubuntu + + echo "Sanity check that mount overrides were generated inside the container" + lxd.lxc exec ubuntu -- find /var/run/systemd/generator/ -name container.conf | MATCH "/var/run/systemd/generator/snap-core18.*mount.d/container.conf" + lxd.lxc exec ubuntu -- test -f /var/run/systemd/generator/snap.mount + + echo "Sanity check that core18 snap is mounted correctly" + # Make sure core18 is mounted and readable for a regular user + retry -n 5 --wait 1 sh -c 'lxd.lxc exec ubuntu -- sudo --user ubuntu --login ls /snap/core18/current' + retry -n 5 --wait 1 sh -c 'lxd.lxc exec ubuntu -- sudo --user ubuntu --login mount | grep core18| grep allow_other' + +restore: | + lxd.lxc stop ubuntu --force || true + lxd.lxc delete ubuntu || true + snap remove --purge lxd diff -Nru snapd-2.47.1+20.10.1build1/tests/main/lxd-services-smoke/task.yaml snapd-2.48+21.04/tests/main/lxd-services-smoke/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/lxd-services-smoke/task.yaml 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48+21.04/tests/main/lxd-services-smoke/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -0,0 +1,34 @@ +summary: Ensure refreshing lxd snap after service commands works. + +details: | + Execute a set of operations on lxd snap that were known to render it unusable + due to a combination of three problems: remove hook of lxd snap didn't clean + its mountpoints properly in namespace of the host, causing error when removing + snap data; snapd wouldn't ignore errors on snap data removal, triggering undo + on remove; remove would fail completely on undo, leaving lxd snap in an + undefined state. With fixed lxd remove hook and snapd fixes related to + https://bugs.launchpad.net/snapd/+bug/1899614, this test should never fail on + any of the above. + +systems: [ubuntu-18.04*, ubuntu-20.04*] + +execute: | + echo "Installing lxd snap" + snap install lxd + snap stop lxd + snap start lxd + + # This may fail if revision is same as stable + snap refresh --edge lxd || true + + # Critical operation: depends on correct lxd remove hook that + # unmounts its mountpoints properly in the namespace of the host. + # If the hook is not doing the right thing, then remove should still + # succeed but we will fail with leftovers in /var/snap/lxd/common. + snap remove lxd --purge + + # We would fail on restore of test suite anyway, but make it more explicit. + if test -d /var/snap/lxd/common; then + echo "lxd snap wasn't fully removed" + exit 1 + fi diff -Nru snapd-2.47.1+20.10.1build1/tests/main/media-sharing/task.yaml snapd-2.48+21.04/tests/main/media-sharing/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/media-sharing/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/media-sharing/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -7,11 +7,9 @@ use /run/media path instead. prepare: | - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh #shellcheck source=tests/lib/dirs.sh . "$TESTSLIB"/dirs.sh - install_local_devmode test-snapd-tools + "$TESTSTOOLS"/snaps-state install-local test-snapd-tools --devmode mkdir -p ${MEDIA_DIR}/src mkdir -p ${MEDIA_DIR}/dst touch ${MEDIA_DIR}/src/canary diff -Nru snapd-2.47.1+20.10.1build1/tests/main/nfs-support/task.yaml snapd-2.48+21.04/tests/main/nfs-support/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/nfs-support/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/nfs-support/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -15,9 +15,7 @@ backends: [-autopkgtest] prepare: | - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB/snaps.sh" - install_local test-snapd-sh + "$TESTSTOOLS"/snaps-state install-local test-snapd-sh # If /proc/fs/nfsd is not initially mounted then ask the test to unmount it later. if not mountinfo.query /proc/fs/nfsd .fs_type=nfsd; then diff -Nru snapd-2.47.1+20.10.1build1/tests/main/op-install-failed-undone/task.yaml snapd-2.48+21.04/tests/main/op-install-failed-undone/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/op-install-failed-undone/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/op-install-failed-undone/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -21,9 +21,7 @@ mkdir -p $SNAP_MOUNT_DIR/test-snapd-sh/current/foo echo "And we try to install it" - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh - if install_local test-snapd-sh; then + if "$TESTSTOOLS"/snaps-state install-local test-snapd-sh; then echo "A snap shouldn't be installable if its mount point is busy" exit 1 fi diff -Nru snapd-2.47.1+20.10.1build1/tests/main/op-remove-retry/task.yaml snapd-2.48+21.04/tests/main/op-remove-retry/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/op-remove-retry/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/op-remove-retry/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -1,5 +1,8 @@ summary: Check that a remove operation is working even if the mount point is busy. +# ubuntu-14.04: systemd-run not supported +systems: [-ubuntu-14.04*] + restore: | kill %1 || true @@ -14,9 +17,7 @@ . "$TESTSLIB"/systemd.sh echo "Given a snap is installed" - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh - install_local test-snapd-tools + "$TESTSTOOLS"/snaps-state install-local test-snapd-tools echo "And its mount point is kept busy" # we need a marker file, because just using systemd to figure out @@ -25,7 +26,7 @@ MARKER=/var/snap/test-snapd-tools/current/block-running rm -f $MARKER - systemd_create_and_start_unit unmount-blocker "$(command -v test-snapd-tools.block)" + systemd-run --unit unmount-blocker test-snapd-tools.block wait_for_service unmount-blocker active while [ ! -f $MARKER ]; do sleep 1; done @@ -40,5 +41,4 @@ while snap list | grep -q test-snapd-tools; do sleep 1; done # cleanup umount blocker - systemd_stop_and_destroy_unit unmount-blocker - + systemctl stop unmount-blocker diff -Nru snapd-2.47.1+20.10.1build1/tests/main/os.query/task.yaml snapd-2.48+21.04/tests/main/os.query/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/os.query/task.yaml 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48+21.04/tests/main/os.query/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -0,0 +1,58 @@ +summary: smoke test for the system-state tool + +execute: | + + # Check help + os.query | MATCH "usage: is-core" + os.query -h | MATCH "usage: is-core" + os.query --help | MATCH "usage: is-core" + + case "$SPREAD_SYSTEM" in + ubuntu-core-16-*) + os.query is-core + os.query is-core16 + not os.query is-core18 + not os.query is-classic + ;; + ubuntu-core-18-*) + os.query is-core + os.query is-core18 + not os.query is-core20 + not os.query is-classic + ;; + ubuntu-core-20-*) + os.query is-core + os.query is-core20 + not os.query is-core18 + not os.query is-classic + ;; + ubuntu-14-*) + os.query is-classic + os.query is-trusty + not os.query is-bionic + not os.query is-core + ;; + ubuntu-16-*) + os.query is-classic + os.query is-xenial + not os.query is-bionic + not os.query is-core + ;; + ubuntu-18-*) + os.query is-classic + os.query is-bionic + not os.query is-focal + not os.query is-core + ;; + ubuntu-20.04-*) + os.query is-classic + os.query is-focal + not os.query is-xenial + not os.query is-core + ;; + *) + os.query is-classic + not os.query is-focal + not os.query is-core + ;; + esac diff -Nru snapd-2.47.1+20.10.1build1/tests/main/parallel-install-aliases/task.yaml snapd-2.48+21.04/tests/main/parallel-install-aliases/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/parallel-install-aliases/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/parallel-install-aliases/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -1,13 +1,10 @@ summary: Check snap alias and snap unalias across different instances of the same snap prepare: | - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB/snaps.sh" - snap set system experimental.parallel-instances=true - install_local aliases - install_local_as aliases aliases_foo + "$TESTSTOOLS"/snaps-state install-local aliases + "$TESTSTOOLS"/snaps-state install-local-as aliases aliases_foo restore: | snap set system experimental.parallel-instances=null diff -Nru snapd-2.47.1+20.10.1build1/tests/main/parallel-install-basic/task.yaml snapd-2.48+21.04/tests/main/parallel-install-basic/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/parallel-install-basic/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/parallel-install-basic/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -8,11 +8,8 @@ snap set system experimental.parallel-instances=true execute: | - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh - - install_local test-snapd-sh - install_local_as test-snapd-sh test-snapd-sh_foo + "$TESTSTOOLS"/snaps-state install-local test-snapd-sh + "$TESTSTOOLS"/snaps-state install-local-as test-snapd-sh test-snapd-sh_foo su -l -c '! test -d ~/snap/test-snapd-sh' test su -l -c '! test -d ~/snap/test-snapd-sh_foo' test diff -Nru snapd-2.47.1+20.10.1build1/tests/main/parallel-install-classic/task.yaml snapd-2.48+21.04/tests/main/parallel-install-classic/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/parallel-install-classic/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/parallel-install-classic/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -35,11 +35,8 @@ esac execute: | - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh - - install_local test-snapd-classic-confinement --classic - install_local_as test-snapd-classic-confinement test-snapd-classic-confinement_foo --classic + "$TESTSTOOLS"/snaps-state install-local test-snapd-classic-confinement --classic + "$TESTSTOOLS"/snaps-state install-local-as test-snapd-classic-confinement test-snapd-classic-confinement_foo --classic su test -l -c '! test -d ~/snap/test-snapd-classic-confinement' su test -l -c '! test -d ~/snap/test-snapd-classic-confinement_foo' diff -Nru snapd-2.47.1+20.10.1build1/tests/main/parallel-install-common-dirs/task.yaml snapd-2.48+21.04/tests/main/parallel-install-common-dirs/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/parallel-install-common-dirs/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/parallel-install-common-dirs/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -7,13 +7,11 @@ snap set system experimental.parallel-instances=true execute: | - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh #shellcheck source=tests/lib/dirs.sh . "$TESTSLIB"/dirs.sh echo "Install a snap with instance key set" - install_local_as test-snapd-sh test-snapd-sh_foo + "$TESTSTOOLS"/snaps-state install-local-as test-snapd-sh test-snapd-sh_foo # foo instance directories are present test -d "$SNAP_MOUNT_DIR/test-snapd-sh_foo" @@ -24,13 +22,13 @@ test -d "/var/snap/test-snapd-sh" # get another revision of test-snapd-sh_foo - install_local_as test-snapd-sh test-snapd-sh_foo + "$TESTSTOOLS"/snaps-state install-local-as test-snapd-sh test-snapd-sh_foo # install instance-key-less snap - install_local test-snapd-sh + "$TESTSTOOLS"/snaps-state install-local test-snapd-sh # and a bar instance - install_local_as test-snapd-sh test-snapd-sh_bar + "$TESTSTOOLS"/snaps-state install-local-as test-snapd-sh test-snapd-sh_bar # bar instance directories are present test -d "$SNAP_MOUNT_DIR/test-snapd-sh_bar" test -d "/var/snap/test-snapd-sh_bar" @@ -71,9 +69,9 @@ not test -d "/var/snap/test-snapd-sh" # make sure that the sole snap without instance key is handled correctly too - install_local test-snapd-sh + "$TESTSTOOLS"/snaps-state install-local test-snapd-sh # another revision - install_local test-snapd-sh + "$TESTSTOOLS"/snaps-state install-local test-snapd-sh test -d "$SNAP_MOUNT_DIR/test-snapd-sh" test -d "/var/snap/test-snapd-sh" snap remove --purge test-snapd-sh diff -Nru snapd-2.47.1+20.10.1build1/tests/main/parallel-install-desktop/task.yaml snapd-2.48+21.04/tests/main/parallel-install-desktop/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/parallel-install-desktop/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/parallel-install-desktop/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -1,11 +1,8 @@ summary: Checks for parallel installation of sideloaded snaps containing desktop applications execute: | - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh - echo "Sideload the regular snap" - install_local basic-desktop + "$TESTSTOOLS"/snaps-state install-local basic-desktop #shellcheck source=tests/lib/dirs.sh . "$TESTSLIB"/dirs.sh @@ -15,7 +12,7 @@ for instance in foo longname; do echo "Sideload same snap as different instance named basic-desktop+$instance" expected="^basic-desktop_$instance 1.0 installed\$" - install_local_as basic-desktop "basic-desktop_$instance" | MATCH "$expected" + "$TESTSTOOLS"/snaps-state install-local-as basic-desktop "basic-desktop_$instance" | MATCH "$expected" diff -u <(head -n5 "/var/lib/snapd/desktop/applications/basic-desktop+${instance}_echo.desktop") - <<-EOF [Desktop Entry] diff -Nru snapd-2.47.1+20.10.1build1/tests/main/parallel-install-interfaces/task.yaml snapd-2.48+21.04/tests/main/parallel-install-interfaces/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/parallel-install-interfaces/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/parallel-install-interfaces/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -4,14 +4,11 @@ backends: [-autopkgtest] prepare: | - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh - snap set system experimental.parallel-instances=true echo "Install test snaps" - install_local home-consumer - install_local_as home-consumer home-consumer_foo + "$TESTSTOOLS"/snaps-state install-local home-consumer + "$TESTSTOOLS"/snaps-state install-local-as home-consumer home-consumer_foo # the home interface is not autoconnected on all-snap systems if [[ ! "$SPREAD_SYSTEM" == ubuntu-core-* ]]; then diff -Nru snapd-2.47.1+20.10.1build1/tests/main/parallel-install-interfaces-content/task.yaml snapd-2.48+21.04/tests/main/parallel-install-interfaces-content/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/parallel-install-interfaces-content/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/parallel-install-interfaces-content/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -20,14 +20,11 @@ snap set system experimental.parallel-instances=true execute: | - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh + "$TESTSTOOLS"/snaps-state install-local test-snapd-content-advanced-slot - install_local test-snapd-content-advanced-slot - - install_local test-snapd-content-advanced-plug - install_local_as test-snapd-content-advanced-plug test-snapd-content-advanced-plug_foo - install_local_as test-snapd-content-advanced-plug test-snapd-content-advanced-plug_bar + "$TESTSTOOLS"/snaps-state install-local test-snapd-content-advanced-plug + "$TESTSTOOLS"/snaps-state install-local-as test-snapd-content-advanced-plug test-snapd-content-advanced-plug_foo + "$TESTSTOOLS"/snaps-state install-local-as test-snapd-content-advanced-plug test-snapd-content-advanced-plug_bar test-snapd-content-advanced-plug.sh -c "$(printf 'test ! -e $%s/target' "$VAR")" diff -Nru snapd-2.47.1+20.10.1build1/tests/main/parallel-install-layout/task.yaml snapd-2.48+21.04/tests/main/parallel-install-layout/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/parallel-install-layout/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/parallel-install-layout/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -10,14 +10,11 @@ snap set system experimental.parallel-instances=true execute: | - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh - echo "Install the regular snap" - install_local test-snapd-layout + "$TESTSTOOLS"/snaps-state install-local test-snapd-layout echo "Sideload the parallel installed snap" - install_local_as test-snapd-layout test-snapd-layout_foo + "$TESTSTOOLS"/snaps-state install-local-as test-snapd-layout test-snapd-layout_foo for name in test-snapd-layout test-snapd-layout_foo; do # workaround trespassing bug diff -Nru snapd-2.47.1+20.10.1build1/tests/main/parallel-install-local/task.yaml snapd-2.48+21.04/tests/main/parallel-install-local/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/parallel-install-local/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/parallel-install-local/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -4,17 +4,14 @@ snap set system experimental.parallel-instances=null execute: | - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh - echo "Install the regular snap" - install_local test-snapd-sh + "$TESTSTOOLS"/snaps-state install-local test-snapd-sh #shellcheck source=tests/lib/dirs.sh . "$TESTSLIB"/dirs.sh - if install_local_as test-snapd-sh test-snapd-sh_foo 2>run.err; then - echo "install_local_as was expected to fail" + if "$TESTSTOOLS"/snaps-state install-local-as test-snapd-sh test-snapd-sh_foo 2>run.err; then + echo "install-local-as was expected to fail" exit 1 fi MATCH 'experimental feature disabled' < run.err @@ -24,7 +21,7 @@ for instance in foo longname; do echo "Install snap instance named test-snapd-sh_$instance" expected="^test-snapd-sh_$instance 1.0 installed\$" - install_local_as test-snapd-sh "test-snapd-sh_$instance" | MATCH "$expected" + "$TESTSTOOLS"/snaps-state install-local-as test-snapd-sh "test-snapd-sh_$instance" | MATCH "$expected" test -d "$SNAP_MOUNT_DIR/test-snapd-sh_$instance/x1" diff -Nru snapd-2.47.1+20.10.1build1/tests/main/parallel-install-remove-after/task.yaml snapd-2.48+21.04/tests/main/parallel-install-remove-after/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/parallel-install-remove-after/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/parallel-install-remove-after/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -28,15 +28,10 @@ esac execute: | - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh - #shellcheck source=tests/lib/systems.sh - . "$TESTSLIB"/systems.sh - # install confined and classic snaps - install_local test-snapd-sh - if is_classic_system ; then - install_local test-snapd-classic-confinement --classic + "$TESTSTOOLS"/snaps-state install-local test-snapd-sh + if os.query is-classic ; then + "$TESTSTOOLS"/snaps-state install-local test-snapd-classic-confinement --classic fi snap set system experimental.parallel-instances=true @@ -44,33 +39,33 @@ # regular instances work # shellcheck disable=SC2016 test-snapd-sh.sh -c 'echo confined $SNAP_INSTANCE_NAME works' - if is_classic_system ; then + if os.query is-classic ; then # shellcheck disable=SC2016 test-snapd-classic-confinement.sh -c 'echo classic $SNAP_INSTANCE_NAME works' fi # new instances of same snaps - install_local_as test-snapd-sh test-snapd-sh_foo - if is_classic_system ; then - install_local_as test-snapd-classic-confinement test-snapd-classic-confinement_foo --classic + "$TESTSTOOLS"/snaps-state install-local-as test-snapd-sh test-snapd-sh_foo + if os.query is-classic ; then + "$TESTSTOOLS"/snaps-state install-local-as test-snapd-classic-confinement test-snapd-classic-confinement_foo --classic fi # parallel instances works # shellcheck disable=SC2016 test-snapd-sh_foo.sh -c 'echo confined $SNAP_INSTANCE_NAME works' - if is_classic_system ; then + if os.query is-classic ; then # shellcheck disable=SC2016 test-snapd-classic-confinement_foo.sh -c 'echo classic $SNAP_INSTANCE_NAME works' fi # removal of snaps should not fail snap remove --purge test-snapd-sh - if is_classic_system ; then + if os.query is-classic ; then snap remove --purge test-snapd-classic-confinement fi # neither should the removal of instances snap remove --purge test-snapd-sh_foo - if is_classic_system ; then + if os.query is-classic ; then snap remove --purge test-snapd-classic-confinement_foo fi diff -Nru snapd-2.47.1+20.10.1build1/tests/main/parallel-install-services/task.yaml snapd-2.48+21.04/tests/main/parallel-install-services/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/parallel-install-services/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/parallel-install-services/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -10,10 +10,7 @@ snap set system experimental.parallel-instances=null execute: | - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh - - install_local test-snapd-service + "$TESTSTOOLS"/snaps-state install-local test-snapd-service #shellcheck source=tests/lib/dirs.sh . "$TESTSLIB"/dirs.sh @@ -27,7 +24,7 @@ for instance in foo longname; do echo "Install a snap as instance named test-snapd-service_$instance" expected="^test-snapd-service_$instance 1.0 installed\$" - install_local_as test-snapd-service "test-snapd-service_$instance" | MATCH "$expected" + "$TESTSTOOLS"/snaps-state install-local-as test-snapd-service "test-snapd-service_$instance" | MATCH "$expected" test -d "$SNAP_MOUNT_DIR/test-snapd-service_$instance/x1" diff -Nru snapd-2.47.1+20.10.1build1/tests/main/parallel-install-snap-icons/task.yaml snapd-2.48+21.04/tests/main/parallel-install-snap-icons/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/parallel-install-snap-icons/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/parallel-install-snap-icons/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -4,16 +4,13 @@ snap unset system experimental.parallel-instances execute: | - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh - echo "Install a snap providing icons" - install_local test-snapd-icon-theme + "$TESTSTOOLS"/snaps-state install-local test-snapd-icon-theme echo "Install additional instances of the snap" snap set system experimental.parallel-instances=true - install_local_as test-snapd-icon-theme test-snapd-icon-theme_longname - install_local_as test-snapd-icon-theme test-snapd-icon-theme_foo + "$TESTSTOOLS"/snaps-state install-local-as test-snapd-icon-theme test-snapd-icon-theme_longname + "$TESTSTOOLS"/snaps-state install-local-as test-snapd-icon-theme test-snapd-icon-theme_foo echo "Each instance provides its own icons" icondir=/var/lib/snapd/desktop/icons/hicolor/scalable/apps diff -Nru snapd-2.47.1+20.10.1build1/tests/main/postrm-purge/task.yaml snapd-2.48+21.04/tests/main/postrm-purge/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/postrm-purge/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/postrm-purge/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -13,8 +13,7 @@ prepare: | # TODO: unify this with tests/main/snap-mgmt/task.yaml echo "When some snaps are installed" - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh + snap set core experimental.user-daemons=true @@ -29,7 +28,7 @@ # None of the "user" snaps work on 14.04 continue fi - install_local "$name" + "$TESTSTOOLS"/snaps-state install-local "$name" snap list | MATCH "$name" done diff -Nru snapd-2.47.1+20.10.1build1/tests/main/prepare-image-grub/task.yaml snapd-2.48+21.04/tests/main/prepare-image-grub/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/prepare-image-grub/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/prepare-image-grub/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -4,6 +4,9 @@ backends: [-autopkgtest] +# ubuntu-14.04: systemd-run not supported +systems: [-ubuntu-14.04*] + # TODO: use the real stores with proper assertions fully as well once possible environment: ROOT: /tmp/root diff -Nru snapd-2.47.1+20.10.1build1/tests/main/refresh/task.yaml snapd-2.48+21.04/tests/main/refresh/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/refresh/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/refresh/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -9,6 +9,9 @@ a given snap with an updatable version (version string like 2.0+fake1) in the edge channel. +# ubuntu-14.04: systemd-run not supported +systems: [-ubuntu-core-*, -ubuntu-14.04*] + environment: SNAP_NAME/parallel_strict_fake,parallel_strict_remote: test-snapd-tools_instance SNAP_NAME/strict_fake,strict_remote: test-snapd-tools @@ -19,11 +22,10 @@ STORE_TYPE/parallel_strict_remote,strict_remote,classic_remote: ${REMOTE_STORE} prepare: | - #shellcheck source=tests/lib/systems.sh - . "$TESTSLIB"/systems.sh + tests.cleanup prepare if [ "$STORE_TYPE" = "fake" ]; then - if is_core_system; then + if os.query is-core; then exit fi if [ "$TRUST_TEST_KEYS" = "false" ]; then @@ -61,11 +63,10 @@ fi restore: | - #shellcheck source=tests/lib/systems.sh - . "$TESTSLIB"/systems.sh + tests.cleanup restore if [ "$STORE_TYPE" = "fake" ]; then - if is_core_system; then + if os.query is-core; then exit fi if [ "$TRUST_TEST_KEYS" = "false" ]; then @@ -82,11 +83,8 @@ fi execute: | - #shellcheck source=tests/lib/systems.sh - . "$TESTSLIB"/systems.sh - if [ "$STORE_TYPE" = "fake" ]; then - if is_core_system; then + if os.query is-core; then exit fi if [ "$TRUST_TEST_KEYS" = "false" ]; then @@ -110,9 +108,43 @@ # # echo "=================================" + # Set refresh app awareness even on core systems, where we do not have + # inotify tools, so that we exercise that code path, even if it is not + # measured in the test. + snap set system experimental.refresh-app-awareness=true + tests.cleanup defer snap unset system experimental.refresh-app-awareness + + #shellcheck source=tests/lib/pkgdb.sh + if os.query is-classic && ! os.query is-trusty && [ "$(. /etc/os-release && echo "$ID")" != amzn ]; then + . "$TESTSLIB/pkgdb.sh" + distro_install_package inotify-tools + # XXX: probably a bug in defer, this fails without ' ' quotes around the rest. + # shellcheck disable=SC2016 + tests.cleanup defer 'bash -c ". $TESTSLIB/pkgdb.sh; distro_purge_package inotify-tools"' + systemd-run \ + --unit test-snapd-watch-inhibit.service \ + -- \ + "$(command -v inotifywait)" \ + --monitor \ + --recursive \ + --outfile /tmp/inhibit.events \ + /var/lib/snapd/inhibit + tests.cleanup defer systemctl stop test-snapd-watch-inhibit.service + fi + echo "When the snap is refreshed" snap refresh --channel=edge "$SNAP_NAME" + if [ -f /tmp/inhibit.events ]; then + echo "During the refresh process, the inhibition lock was established and released" + MATCH "/var/lib/snapd/inhibit/ OPEN $SNAP_NAME.lock" /tmp/inhibit.events + MATCH "/var/lib/snapd/inhibit/ MODIFY $SNAP_NAME.lock" /tmp/inhibit.events + MATCH "/var/lib/snapd/inhibit/ CLOSE_WRITE,CLOSE $SNAP_NAME.lock" /tmp/inhibit.events + tests.cleanup pop # stop the inotifywait unit + tests.cleanup pop # remove inotify-tools + fi + tests.cleanup pop # unset refresh-app-awareness + echo "Then the new version is listed" expected="$SNAP_NAME +$SNAP_VERSION_PATTERN" snap list | grep -Pzq "$expected" @@ -124,9 +156,7 @@ echo "classic snaps " echo "When multiple snaps have no update we have a good message" - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh - install_local basic + "$TESTSTOOLS"/snaps-state install-local basic snap refresh "$SNAP_NAME" basic 2>&1 | MATCH "All snaps up to date." echo "When moving to stable" diff -Nru snapd-2.47.1+20.10.1build1/tests/main/refresh-all/task.yaml snapd-2.48+21.04/tests/main/refresh-all/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/refresh-all/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/refresh-all/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -1,6 +1,7 @@ summary: Check that more than one snap is refreshed. -systems: [-ubuntu-core-*] +# ubuntu-14.04: systemd-run not supported +systems: [-ubuntu-core-*, -ubuntu-14.04*] details: | We use only the fake store for this test because we currently diff -Nru snapd-2.47.1+20.10.1build1/tests/main/refresh-all-undo/task.yaml snapd-2.48+21.04/tests/main/refresh-all-undo/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/refresh-all-undo/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/refresh-all-undo/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -1,6 +1,7 @@ summary: Check that undo for snap refresh works -systems: [-ubuntu-core-*] +# ubuntu-14.04: systemd-run not supported +systems: [-ubuntu-core-*, -ubuntu-14.04*] environment: BLOB_DIR: $(pwd)/fake-store-blobdir diff -Nru snapd-2.47.1+20.10.1build1/tests/main/refresh-devmode/task.yaml snapd-2.48+21.04/tests/main/refresh-devmode/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/refresh-devmode/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/refresh-devmode/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -9,6 +9,9 @@ a given snap with an updatable version (version string like 2.0+fake1) in the edge channel. +# ubuntu-14.04: systemd-run not supported +systems: [-ubuntu-14.04*] + environment: SNAP_NAME: test-snapd-tools SNAP_VERSION_PATTERN: \d+\.\d+\+fake1 @@ -17,11 +20,8 @@ STORE_TYPE/remote: ${REMOTE_STORE} prepare: | - #shellcheck source=tests/lib/systems.sh - . "$TESTSLIB"/systems.sh - if [ "$STORE_TYPE" = "fake" ]; then - if is_core_system; then + if os.query is-core; then exit fi if [ "$TRUST_TEST_KEYS" = "false" ]; then @@ -45,11 +45,8 @@ fi restore: | - #shellcheck source=tests/lib/systems.sh - . "$TESTSLIB"/systems.sh - if [ "$STORE_TYPE" = "fake" ]; then - if is_core_system; then + if os.query is-core; then exit fi if [ "$TRUST_TEST_KEYS" = "false" ]; then @@ -62,11 +59,8 @@ fi execute: | - #shellcheck source=tests/lib/systems.sh - . "$TESTSLIB"/systems.sh - if [ "$STORE_TYPE" = "fake" ]; then - if is_core_system; then + if os.query is-core; then exit fi if [ "$TRUST_TEST_KEYS" = "false" ]; then diff -Nru snapd-2.47.1+20.10.1build1/tests/main/regression-home-snap-root-owned/task.yaml snapd-2.48+21.04/tests/main/regression-home-snap-root-owned/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/regression-home-snap-root-owned/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/regression-home-snap-root-owned/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -5,9 +5,7 @@ # ensure we have no snap user data directory yet rm -rf /home/test/snap rm -rf /root/snap - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh - install_local test-snapd-sh + "$TESTSTOOLS"/snaps-state install-local test-snapd-sh tests.session -u test prepare restore: | tests.session -u test restore diff -Nru snapd-2.47.1+20.10.1build1/tests/main/remove-errors/task.yaml snapd-2.48+21.04/tests/main/remove-errors/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/remove-errors/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/remove-errors/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -1,22 +1,18 @@ summary: Check remove command errors execute: | - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB/snaps.sh" - #shellcheck source=tests/lib/systems.sh - . "$TESTSLIB"/systems.sh #shellcheck source=tests/lib/names.sh . "$TESTSLIB/names.sh" BASE_SNAP=core TARGET_SNAP=test-snapd-tools - if is_core18_system; then + if os.query is-core18; then BASE_SNAP=core18 TARGET_SNAP=test-snapd-tools-core18 fi echo "Given a base snap, $BASE_SNAP, is installed" - install_local "$TARGET_SNAP" + "$TESTSTOOLS"/snaps-state install-local "$TARGET_SNAP" echo "Ensure the important snaps can not be removed" for sn in $BASE_SNAP $kernel_name $gadget_name; do diff -Nru snapd-2.47.1+20.10.1build1/tests/main/revert/task.yaml snapd-2.48+21.04/tests/main/revert/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/revert/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/revert/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -1,16 +1,16 @@ summary: Check that revert works. +# ubuntu-14.04: systemd-run not supported +systems: [-ubuntu-14.04*] + environment: STORE_TYPE/fake: fake STORE_TYPE/remote: ${REMOTE_STORE} BLOB_DIR: $(pwd)/fake-store-blobdir prepare: | - #shellcheck source=tests/lib/systems.sh - . "$TESTSLIB"/systems.sh - if [ "$STORE_TYPE" = "fake" ]; then - if is_core_system; then + if os.query is-core; then exit fi if [ "$TRUST_TEST_KEYS" = "false" ]; then @@ -34,11 +34,8 @@ fi restore: | - #shellcheck source=tests/lib/systems.sh - . "$TESTSLIB"/systems.sh - if [ "$STORE_TYPE" = "fake" ]; then - if is_core_system; then + if os.query is-core; then exit fi if [ "$TRUST_TEST_KEYS" = "false" ]; then @@ -51,11 +48,8 @@ fi execute: | - #shellcheck source=tests/lib/systems.sh - . "$TESTSLIB"/systems.sh - if [ "$STORE_TYPE" = "fake" ]; then - if is_core_system; then + if os.query is-core; then exit fi if [ "$TRUST_TEST_KEYS" = "false" ]; then diff -Nru snapd-2.47.1+20.10.1build1/tests/main/revert-devmode/task.yaml snapd-2.48+21.04/tests/main/revert-devmode/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/revert-devmode/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/revert-devmode/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -3,17 +3,17 @@ # slow in autopkgtest (>1m) backends: [-autopkgtest] +# ubuntu-14.04: systemd-run not supported +systems: [-ubuntu-14.04*] + environment: STORE_TYPE/fake: fake STORE_TYPE/remote: ${REMOTE_STORE} BLOB_DIR: $(pwd)/fake-store-blobdir prepare: | - #shellcheck source=tests/lib/systems.sh - . "$TESTSLIB"/systems.sh - if [ "$STORE_TYPE" = "fake" ]; then - if is_core_system; then + if os.query is-core; then exit fi if [ "$TRUST_TEST_KEYS" = "false" ]; then @@ -37,11 +37,8 @@ fi restore: | - #shellcheck source=tests/lib/systems.sh - . "$TESTSLIB"/systems.sh - if [ "$STORE_TYPE" = "fake" ]; then - if is_core_system; then + if os.query is-core; then exit fi if [ "$TRUST_TEST_KEYS" = "false" ]; then @@ -54,11 +51,8 @@ fi execute: | - #shellcheck source=tests/lib/systems.sh - . "$TESTSLIB"/systems.sh - if [ "$STORE_TYPE" = "fake" ]; then - if is_core_system; then + if os.query is-core; then exit fi if [ "$TRUST_TEST_KEYS" = "false" ]; then diff -Nru snapd-2.47.1+20.10.1build1/tests/main/searching/task.yaml snapd-2.48+21.04/tests/main/searching/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/searching/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/searching/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -49,7 +49,16 @@ # TODO: discuss with the store how we can make this test stable, i.e. # that section/snap changes do not break us if [ "$(uname -m)" = "x86_64" ]; then - snap find --section=photo-and-video vlc | MATCH vlc + set +e + snap find --section=photo-and-video vlc >vlc.log 2>&1 + retval=$? + set -e + + if [ "$retval" -eq 0 ]; then + MATCH vlc < vlc.log + else + MATCH 'error: cannot get snap sections: cannot sections: got unexpected HTTP status code 403 via GET to "https://api.snapcraft.io/api/v1/snaps/sections"' < vlc.log + fi else # actual output: # Name Version Publisher Notes Summary diff -Nru snapd-2.47.1+20.10.1build1/tests/main/seccomp-statx/task.yaml snapd-2.48+21.04/tests/main/seccomp-statx/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/seccomp-statx/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/seccomp-statx/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -10,9 +10,7 @@ systems: [ubuntu-16.04-*, ubuntu-18.04-*, ubuntu-2*] prepare: | - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh - install_local test-snapd-statx + "$TESTSTOOLS"/snaps-state install-local test-snapd-statx execute: | # Notably, this doesn't print statx: blocked anymore diff -Nru snapd-2.47.1+20.10.1build1/tests/main/security-apparmor/task.yaml snapd-2.48+21.04/tests/main/security-apparmor/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/security-apparmor/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/security-apparmor/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -2,9 +2,7 @@ prepare: | echo "Given a basic snap is installed" - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh - install_local test-snapd-sh + "$TESTSTOOLS"/snaps-state install-local test-snapd-sh execute: | if [ "$(snap debug confinement)" = partial ] ; then diff -Nru snapd-2.47.1+20.10.1build1/tests/main/security-device-cgroups/task.yaml snapd-2.48+21.04/tests/main/security-device-cgroups/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/security-device-cgroups/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/security-device-cgroups/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -77,9 +77,7 @@ fi echo "Given a snap is installed" - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh - install_local test-snapd-sh + "$TESTSTOOLS"/snaps-state install-local test-snapd-sh echo "Then the device is not assigned to that snap" udevadm info "$UDEVADM_PATH" | not MATCH "E: TAGS=.*snap_test-snapd-sh_sh" diff -Nru snapd-2.47.1+20.10.1build1/tests/main/security-device-cgroups-classic/task.yaml snapd-2.48+21.04/tests/main/security-device-cgroups-classic/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/security-device-cgroups-classic/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/security-device-cgroups-classic/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -18,9 +18,7 @@ fi echo "Given a snap declaring a plug on framebuffer is installed in classic" - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh - install_local_classic test-classic-cgroup + "$TESTSTOOLS"/snaps-state install-local test-classic-cgroup --classic restore: | if [ -e /dev/fb0.spread ]; then diff -Nru snapd-2.47.1+20.10.1build1/tests/main/security-device-cgroups-devmode/task.yaml snapd-2.48+21.04/tests/main/security-device-cgroups-devmode/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/security-device-cgroups-devmode/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/security-device-cgroups-devmode/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -20,9 +20,7 @@ fi echo "Given a snap declaring a plug on framebuffer is installed in devmode" - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh - install_local_devmode test-devmode-cgroup + "$TESTSTOOLS"/snaps-state install-local test-devmode-cgroup --devmode restore: | if [ -e /dev/fb0.spread ]; then diff -Nru snapd-2.47.1+20.10.1build1/tests/main/security-device-cgroups-jailmode/task.yaml snapd-2.48+21.04/tests/main/security-device-cgroups-jailmode/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/security-device-cgroups-jailmode/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/security-device-cgroups-jailmode/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -23,9 +23,7 @@ fi echo "Given a snap declaring a plug on framebuffer is installed in jailmode" - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh - install_local_jailmode test-devmode-cgroup + "$TESTSTOOLS"/snaps-state install-local test-devmode-cgroup --jailmode restore: | if [ -e /dev/fb0.spread ]; then diff -Nru snapd-2.47.1+20.10.1build1/tests/main/security-device-cgroups-serial-port/task.yaml snapd-2.48+21.04/tests/main/security-device-cgroups-serial-port/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/security-device-cgroups-serial-port/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/security-device-cgroups-serial-port/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -21,9 +21,7 @@ execute: | echo "Given a snap is installed" - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh - install_local test-snapd-sh + "$TESTSTOOLS"/snaps-state install-local test-snapd-sh echo "Then the device is not assigned to that snap" if udevadm info /dev/ttyS4 > info.txt; then diff -Nru snapd-2.47.1+20.10.1build1/tests/main/security-device-cgroups-strict/task.yaml snapd-2.48+21.04/tests/main/security-device-cgroups-strict/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/security-device-cgroups-strict/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/security-device-cgroups-strict/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -16,9 +16,7 @@ fi echo "Given a snap declaring a plug on framebuffer is installed in strict" - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh - install_local test-strict-cgroup + "$TESTSTOOLS"/snaps-state install-local test-strict-cgroup restore: | if [ -e /dev/fb0.spread ]; then diff -Nru snapd-2.47.1+20.10.1build1/tests/main/security-dev-input-event-denied/task.yaml snapd-2.48+21.04/tests/main/security-dev-input-event-denied/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/security-dev-input-event-denied/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/security-dev-input-event-denied/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -17,9 +17,7 @@ prepare: | echo "Given the test-snapd-event snap is installed" - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh - install_local test-snapd-event + "$TESTSTOOLS"/snaps-state install-local test-snapd-event execute: | if [ -z "$(find /dev/input/by-path -name '*-event-kbd')" ]; then diff -Nru snapd-2.47.1+20.10.1build1/tests/main/security-devpts/task.yaml snapd-2.48+21.04/tests/main/security-devpts/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/security-devpts/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/security-devpts/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -6,9 +6,7 @@ fi echo "Given a basic snap is installed" - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh - install_local test-snapd-devpts + "$TESTSTOOLS"/snaps-state install-local test-snapd-devpts echo "When no plugs are not connected" if snap interfaces -i physical-memory-observe | MATCH ":physical-memory-observe .*test-snapd-devpts" ; then diff -Nru snapd-2.47.1+20.10.1build1/tests/main/security-private-tmp/task.yaml snapd-2.48+21.04/tests/main/security-private-tmp/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/security-private-tmp/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/security-private-tmp/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -8,9 +8,7 @@ prepare: | echo "Given a basic snap is installed" - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh - install_local test-snapd-sh + "$TESTSTOOLS"/snaps-state install-local test-snapd-sh echo "And another basic snap is installed" mkdir -p "$SNAP_INSTALL_DIR" diff -Nru snapd-2.47.1+20.10.1build1/tests/main/security-profiles/task.yaml snapd-2.48+21.04/tests/main/security-profiles/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/security-profiles/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/security-profiles/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -11,9 +11,7 @@ seccomp_profile_directory="/var/lib/snapd/seccomp/bpf" echo "Security profiles are generated and loaded for apps" - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh - install_local test-snapd-tools + "$TESTSTOOLS"/snaps-state install-local test-snapd-tools loaded_profiles=$(cat /sys/kernel/security/apparmor/profiles) for profile in snap.test-snapd-tools.block snap.test-snapd-tools.cat snap.test-snapd-tools.echo snap.test-snapd-tools.fail snap.test-snapd-tools.success diff -Nru snapd-2.47.1+20.10.1build1/tests/main/security-setuid-root/task.yaml snapd-2.48+21.04/tests/main/security-setuid-root/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/security-setuid-root/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/security-setuid-root/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -10,9 +10,7 @@ systems: [-debian-*, -fedora-*, -opensuse-*, -arch-*, -amazon-*, -centos-*] prepare: | - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh - install_local test-snapd-sh + "$TESTSTOOLS"/snaps-state install-local test-snapd-sh echo "Ensure the snap-confine profiles on core are not loaded" for p in /var/lib/snapd/apparmor/profiles/snap-confine.*; do apparmor_parser -R "$p" diff -Nru snapd-2.47.1+20.10.1build1/tests/main/security-udev-input-subsystem/task.yaml snapd-2.48+21.04/tests/main/security-udev-input-subsystem/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/security-udev-input-subsystem/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/security-udev-input-subsystem/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -12,9 +12,7 @@ prepare: | echo "Given the test-snapd-udev-input-subsystem is installed" - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh - install_local test-snapd-udev-input-subsystem + "$TESTSTOOLS"/snaps-state install-local test-snapd-udev-input-subsystem execute: | if [ -z "$(find /dev/input/by-path -name '*-event-kbd')" ]; then diff -Nru snapd-2.47.1+20.10.1build1/tests/main/selinux-clean/task.yaml snapd-2.48+21.04/tests/main/selinux-clean/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/selinux-clean/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/selinux-clean/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -39,10 +39,7 @@ systemctl restart snapd.socket ausearch -i --checkpoint stamp --start checkpoint -m AVC 2>&1 | MATCH 'no matches' - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB/snaps.sh" - - install_local test-snapd-desktop + "$TESTSTOOLS"/snaps-state install-local test-snapd-desktop test-snapd-desktop.sh -c echo 'hello world' tests.session -u test exec test-snapd-desktop.sh -c 'echo hello world' tests.session -u test exec test-snapd-desktop.sh -c 'mkdir /home/test/foo' @@ -68,17 +65,17 @@ not MATCH 'type=AVC' # another revision triggers copy of snap data - install_local test-snapd-desktop + "$TESTSTOOLS"/snaps-state install-local test-snapd-desktop ausearch -i --checkpoint stamp --start checkpoint -m AVC 2>&1 | MATCH 'no matches' # removal triggers cleanups snap remove test-snapd-desktop ausearch -i --checkpoint stamp --start checkpoint -m AVC 2>&1 | MATCH 'no matches' - install_local test-snapd-appstream-metadata + "$TESTSTOOLS"/snaps-state install-local test-snapd-appstream-metadata snap connect test-snapd-appstream-metadata:appstream-metadata ausearch -i --checkpoint stamp --start checkpoint -m AVC 2>&1 | MATCH 'no matches' - install_local test-snapd-service + "$TESTSTOOLS"/snaps-state install-local test-snapd-service snap stop test-snapd-service snap start test-snapd-service # TODO: enable once there is a workaround for denials caused by journalctl @@ -86,13 +83,13 @@ snap remove test-snapd-service ausearch -i --checkpoint stamp --start checkpoint -m AVC 2>&1 | MATCH 'no matches' - install_local test-snapd-layout + "$TESTSTOOLS"/snaps-state install-local test-snapd-layout test-snapd-layout.sh -c 'ls /' su test -c "test-snapd-layout.sh -c 'ls /'" snap remove test-snapd-layout ausearch -i --checkpoint stamp --start checkpoint -m AVC 2>&1 | MATCH 'no matches' - install_local socket-activation + "$TESTSTOOLS"/snaps-state install-local socket-activation [ -S /var/snap/socket-activation/common/socket ] snap remove socket-activation ausearch -i --checkpoint stamp --start checkpoint -m AVC 2>&1 | MATCH 'no matches' @@ -101,7 +98,7 @@ # is bind mounted into the mount namespace of a snap, thus the SELinux # contexts from the host appear inside, and the policy needs to allow # proper transitions with these labels - install_local test-snapd-snapctl-core18 + "$TESTSTOOLS"/snaps-state install-local test-snapd-snapctl-core18 snap restart test-snapd-snapctl-core18 snap remove test-snapd-snapctl-core18 ausearch -i --checkpoint stamp --start checkpoint -m AVC 2>&1 | MATCH 'no matches' diff -Nru snapd-2.47.1+20.10.1build1/tests/main/selinux-data-context/task.yaml snapd-2.48+21.04/tests/main/selinux-data-context/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/selinux-data-context/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/selinux-data-context/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -38,11 +38,8 @@ #shellcheck disable=SC2012 ls -Zd /run/snapd | MATCH ':snappy_var_run_t:' - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh - # install a snap that does some file manipulation - install_local test-snapd-service-writer + "$TESTSTOOLS"/snaps-state install-local test-snapd-service-writer ls -Zd /var/snap/test-snapd-service-writer/common \ /var/snap/test-snapd-service-writer/common/by-hook \ @@ -72,7 +69,7 @@ MATCH '^.*system_u:object_r:snappy_var_t:s0 /var/snap/test-snapd-service-writer/current/foo$' < service-labels MATCH '^.*system_u:object_r:snappy_var_t:s0 /var/snap/test-snapd-service-writer/current/foo/bar$' < service-labels - install_local socket-activation + "$TESTSTOOLS"/snaps-state install-local socket-activation [ -S /var/snap/socket-activation/common/socket ] #shellcheck disable=SC2012 ls -Zd /var/snap/socket-activation/common/socket | MATCH ':snappy_var_t:' diff -Nru snapd-2.47.1+20.10.1build1/tests/main/services-after-before/task.yaml snapd-2.48+21.04/tests/main/services-after-before/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/services-after-before/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/services-after-before/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -2,12 +2,11 @@ execute: | echo "When the service snap is installed" - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh + # we are using systemd-notify indicate the service is active, this is # currently not allowed by daemon-notify interface, so we may as well just # install in devmode - install_local test-snapd-after-before-service --devmode + "$TESTSTOOLS"/snaps-state install-local test-snapd-after-before-service --devmode echo "We can see all services running" for service in before-middle middle after-middle; do diff -Nru snapd-2.47.1+20.10.1build1/tests/main/services-after-before-install/task.yaml snapd-2.48+21.04/tests/main/services-after-before-install/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/services-after-before-install/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/services-after-before-install/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -18,8 +18,6 @@ execute: | echo "When the service snap is installed" - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh get_prop() { prop=$(systemctl show -p "$1" "$2") @@ -38,7 +36,7 @@ # we are using systemd-notify indicate the service is active, this is # currently not allowed by daemon-notify interface, so we may as well # just install in devmode - install_local_as test-snapd-after-before-service "test-snapd-after-before-service_$INSTANCE" --devmode + "$TESTSTOOLS"/snaps-state install-local-as test-snapd-after-before-service "test-snapd-after-before-service_$INSTANCE" --devmode service_prefix="snap.test-snapd-after-before-service_$INSTANCE" diff -Nru snapd-2.47.1+20.10.1build1/tests/main/services-disable-install-hook/task.yaml snapd-2.48+21.04/tests/main/services-disable-install-hook/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/services-disable-install-hook/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/services-disable-install-hook/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -1,9 +1,7 @@ summary: Check that `snapctl stop --disable` actually stops services on install execute: | - # shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh - install_local test-snapd-svcs-disable-install-hook + "$TESTSTOOLS"/snaps-state install-local test-snapd-svcs-disable-install-hook for service in simple forking; do echo "Verify that the $service service isn't running" diff -Nru snapd-2.47.1+20.10.1build1/tests/main/services-disable-refresh-hook/task.yaml snapd-2.48+21.04/tests/main/services-disable-refresh-hook/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/services-disable-refresh-hook/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/services-disable-refresh-hook/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -2,11 +2,8 @@ Check that `snapctl stop --disable` actually stops services on post-refresh execute: | - # shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh - echo "Installing snap first time starts services" - install_local test-snapd-svcs-disable-refresh-hook + "$TESTSTOOLS"/snaps-state install-local test-snapd-svcs-disable-refresh-hook echo "Services are running after install hook" for service in simple forking; do @@ -15,7 +12,7 @@ done echo "Refreshing the snap triggers post-refresh hook which disables the services" - install_local test-snapd-svcs-disable-refresh-hook + "$TESTSTOOLS"/snaps-state install-local test-snapd-svcs-disable-refresh-hook echo "Services are now disabled" for service in simple forking; do diff -Nru snapd-2.47.1+20.10.1build1/tests/main/services-install-hook-can-run-svcs/task.yaml snapd-2.48+21.04/tests/main/services-install-hook-can-run-svcs/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/services-install-hook-can-run-svcs/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/services-install-hook-can-run-svcs/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -1,11 +1,8 @@ summary: Check that install hooks in snaps can start services execute: | - # shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh - echo "Verify that the snap installs" - install_local test-snapd-install-hook-runs-svc + "$TESTSTOOLS"/snaps-state install-local test-snapd-install-hook-runs-svc echo "Verify that the snap service is still disabled" snap services | MATCH "test-snapd-install-hook-runs-svc\\.svc\\s+disabled\\s+inactive" diff -Nru snapd-2.47.1+20.10.1build1/tests/main/services-multi-service-failing/task.yaml snapd-2.48+21.04/tests/main/services-multi-service-failing/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/services-multi-service-failing/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/services-multi-service-failing/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -2,10 +2,8 @@ Check that `snap install` doesn't leave a service running when the install fails. execute: | - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh echo "when a snap install fails" - not install_local test-snapd-multi-service + not "$TESTSTOOLS"/snaps-state install-local test-snapd-multi-service echo "we don't leave a service running" not systemctl is-active snap.test-snapd-multi-service.ok.service diff -Nru snapd-2.47.1+20.10.1build1/tests/main/services-refresh-mode/task.yaml snapd-2.48+21.04/tests/main/services-refresh-mode/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/services-refresh-mode/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/services-refresh-mode/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -10,18 +10,15 @@ systemctl status snap.test-snapd-service.test-snapd-endure-service || true execute: | - # shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB/snaps.sh" - echo "When the service snap is installed" - install_local test-snapd-service + "$TESTSTOOLS"/snaps-state install-local test-snapd-service echo "We can see it running" systemctl status snap.test-snapd-service.test-snapd-endure-service|MATCH "running" systemctl show -p MainPID snap.test-snapd-service.test-snapd-endure-service > old-main.pid echo "When it is re-installed" - install_local test-snapd-service + "$TESTSTOOLS"/snaps-state install-local test-snapd-service echo "We can still see it running with the same PID" systemctl show -p MainPID snap.test-snapd-service.test-snapd-endure-service > new-main.pid diff -Nru snapd-2.47.1+20.10.1build1/tests/main/services-snapctl/task.yaml snapd-2.48+21.04/tests/main/services-snapctl/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/services-snapctl/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/services-snapctl/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -26,11 +26,8 @@ done } - # shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB/snaps.sh" - echo "When the service snap is installed" - install_local test-snapd-service + "$TESTSTOOLS"/snaps-state install-local test-snapd-service echo "We can see it running" _wait_for_service "test-snapd-service.test-snapd-service" " active" @@ -67,7 +64,7 @@ echo "Reinstalling the snap with configure hook calling snapctl restart works" snap set test-snapd-service command=restart - install_local test-snapd-service + "$TESTSTOOLS"/snaps-state install-local test-snapd-service # shellcheck disable=SC2119 if "$TESTSTOOLS"/journal-state get-log | MATCH "error running snapctl"; then echo "snapctl should not report errors" diff -Nru snapd-2.47.1+20.10.1build1/tests/main/services-stop-mode/task.yaml snapd-2.48+21.04/tests/main/services-stop-mode/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/services-stop-mode/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/services-stop-mode/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -19,11 +19,8 @@ done execute: | - # shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB/snaps.sh" - echo "When the service snap is installed" - install_local test-snapd-service + "$TESTSTOOLS"/snaps-state install-local test-snapd-service # Because we cannot use daemon: notify easily yet just wait for a "ready" # file to show up. The file is removed when the service is stopped. This # pattern repeats around the test without additional comments. @@ -39,7 +36,7 @@ done echo "When it is re-installed" - install_local test-snapd-service + "$TESTSTOOLS"/snaps-state install-local test-snapd-service while ! test -f /var/snap/test-snapd-service/common/ready; do sleep 1; done # note that sigterm{,-all} is tested separately diff -Nru snapd-2.47.1+20.10.1build1/tests/main/services-stop-mode-sigkill/task.yaml snapd-2.48+21.04/tests/main/services-stop-mode-sigkill/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/services-stop-mode-sigkill/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/services-stop-mode-sigkill/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -18,9 +18,7 @@ pkill sleep || true echo "When the service snap is installed" - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh - install_local test-snapd-service + "$TESTSTOOLS"/snaps-state install-local test-snapd-service # Because we cannot use daemon: notify easily yet just wait for a "ready" # file to show up. The file is removed when the service is stopped. This # pattern repeats around the test without additional comments. @@ -36,7 +34,7 @@ [ "$n" = "2" ] echo "When it is re-installed one process uses sigterm, the other sigterm-all" - install_local test-snapd-service + "$TESTSTOOLS"/snaps-state install-local test-snapd-service while ! test -f /var/snap/test-snapd-service/common/ready; do sleep 1; done echo "After reinstall the sigterm-all service and all children got killed" diff -Nru snapd-2.47.1+20.10.1build1/tests/main/services-timer/task.yaml snapd-2.48+21.04/tests/main/services-timer/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/services-timer/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/services-timer/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -2,9 +2,7 @@ execute: | echo "When the service snap is installed" - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh - install_local test-snapd-timer-service + "$TESTSTOOLS"/snaps-state install-local test-snapd-timer-service echo "We can see the timers being active" for service in regular-timer random-timer range-timer trivial-timer; do diff -Nru snapd-2.47.1+20.10.1build1/tests/main/services-watchdog/task.yaml snapd-2.48+21.04/tests/main/services-watchdog/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/services-watchdog/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/services-watchdog/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -10,11 +10,8 @@ done execute: | - # shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh - echo "When the service snap is installed" - install_local test-snapd-service-watchdog + "$TESTSTOOLS"/snaps-state install-local test-snapd-service-watchdog # the interface is disconnected by default snap connect test-snapd-service-watchdog:daemon-notify diff -Nru snapd-2.47.1+20.10.1build1/tests/main/set-proxy-store/task.yaml snapd-2.48+21.04/tests/main/set-proxy-store/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/set-proxy-store/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/set-proxy-store/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -1,6 +1,7 @@ summary: Check that the setting proxy.store config works -systems: [-ubuntu-core-*] +# ubuntu-14.04: systemd-run not supported +systems: [-ubuntu-core-*, -ubuntu-14.04*] environment: SNAP_NAME: test-snapd-tools diff -Nru snapd-2.47.1+20.10.1build1/tests/main/snap-advise-command/task.yaml snapd-2.48+21.04/tests/main/snap-advise-command/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/snap-advise-command/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/snap-advise-command/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -1,11 +1,15 @@ summary: Ensure that `snap advise-snap` works -# we need https://github.com/snapcore/core/pull/70 landed before -# we can use this on ubuntu-core-* -systems: [ubuntu-16*, ubuntu-18*] +# advise-snap / command-not-found only works on ubuntu classic, and on uc16 +# on uc18+ we don't have the /usr/lib/command-not-found symlink so it's not +# useful +systems: + - ubuntu-1* + - ubuntu-2* + - ubuntu-core-16* prepare: | - if [ -e /usr/lib/command-not-found ]; then + if ! os.query is-core16 && [ -e /usr/lib/command-not-found ]; then mv /usr/lib/command-not-found /usr/lib/command-not-found.orig fi @@ -15,10 +19,6 @@ fi execute: | - # FIXME: remove this once the store is in good shape again - echo "the store is unhappy right now" - exit 0 - echo "wait for snapd to pull in the commands data" echo "(it will do that on startup)" for _ in $(seq 120); do @@ -40,11 +40,14 @@ snap advise-snap --command test-snapd-tools.echo | MATCH test-snapd-tools echo "Ensure 'advise-snap --command' works as command-not-found symlink" - ln -s /usr/bin/snap /usr/lib/command-not-found + # it's already this symlink on uc16, just use as-is there + if ! os.query is-core16; then + ln -s /usr/bin/snap /usr/lib/command-not-found + fi /usr/lib/command-not-found test-snapd-tools.echo | MATCH test-snapd-tools echo "Ensure short names are found too" - snap advise-snap --command test_snapd_wellknown1 | MATCH 'The program ".*" can be found' + snap advise-snap --command test_snapd_wellknown1 | MATCH '"test_snapd_wellknown1" not found, but can be installed with' echo "Ensure advise-snap without a match returns exit code 1" if snap advise-snap --command no-such-command-for-any-snap; then diff -Nru snapd-2.47.1+20.10.1build1/tests/main/snap-confine-drops-sys-admin/task.yaml snapd-2.48+21.04/tests/main/snap-confine-drops-sys-admin/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/snap-confine-drops-sys-admin/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/snap-confine-drops-sys-admin/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -11,11 +11,9 @@ prepare: | echo "Install a helper snap with default confinement" - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh # we only need devmode since this test is only about whether or not # snap-confine dropped CAP_SYS_ADMIN - install_local_devmode test-snapd-sh + "$TESTSTOOLS"/snaps-state install-local test-snapd-sh --devmode echo "Compile and prepare the support program" # Because we use the snap data directory we don't need to clean it up diff -Nru snapd-2.47.1+20.10.1build1/tests/main/snap-confine-privs/task.yaml snapd-2.48+21.04/tests/main/snap-confine-privs/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/snap-confine-privs/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/snap-confine-privs/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -18,9 +18,7 @@ prepare: | echo "Install a helper snap (for confinement testing)" - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh - install_local test-snapd-sh + "$TESTSTOOLS"/snaps-state install-local test-snapd-sh echo "Compile and prepare the support program" # Because we use the snap data directory we don't need to clean it up diff -Nru snapd-2.47.1+20.10.1build1/tests/main/snap-connect/task.yaml snapd-2.48+21.04/tests/main/snap-connect/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/snap-connect/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/snap-connect/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -1,11 +1,8 @@ summary: Check that snap connect works prepare: | - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh - echo "Install a test snap" - install_local home-consumer + "$TESTSTOOLS"/snaps-state install-local home-consumer # the home interface is not autoconnected on all-snap systems if [[ ! "$SPREAD_SYSTEM" == ubuntu-core-16-* ]]; then snap disconnect home-consumer:home diff -Nru snapd-2.47.1+20.10.1build1/tests/main/snap-connections/task.yaml snapd-2.48+21.04/tests/main/snap-connections/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/snap-connections/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/snap-connections/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -11,9 +11,6 @@ snap remove test-snapd-daemon-notify || true execute: | - # shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB/snaps.sh" - snap connections > all.out 2>&1 initial_connections="$(wc -l < all.out)" @@ -36,7 +33,7 @@ snap connections test-snapd-content-slot | MATCH "$expected" # :network is connected by default - install_local network-consumer + "$TESTSTOOLS"/snaps-state install-local network-consumer expected='network +network-consumer:network +:network +-' snap connections network-consumer | MATCH "$expected" # disconect it manually @@ -45,7 +42,7 @@ snap connections network-consumer 2>&1 | MATCH "$expected" # try with an interface which is not connected by default - install_local test-snapd-daemon-notify + "$TESTSTOOLS"/snaps-state install-local test-snapd-daemon-notify expected='daemon-notify +test-snapd-daemon-notify:daemon-notify +- +-' snap connections test-snapd-daemon-notify | MATCH "$expected" diff -Nru snapd-2.47.1+20.10.1build1/tests/main/snapctl-from-snap/task.yaml snapd-2.48+21.04/tests/main/snapctl-from-snap/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/snapctl-from-snap/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/snapctl-from-snap/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -42,9 +42,7 @@ snap install --edge core18 fi - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh - install_local "$SNAP" + "$TESTSTOOLS"/snaps-state install-local "$SNAP" echo "Verify that cookie file exists and has proper permissions and size" check_cookie "$SNAP" @@ -77,7 +75,7 @@ snap get "$SNAP" foo | MATCH 123 echo "Given two revisions of a snap have been installed" - install_local "$SNAP" + "$TESTSTOOLS"/snaps-state install-local "$SNAP" check_cookie "$SNAP" echo "And a single revision gets removed" diff -Nru snapd-2.47.1+20.10.1build1/tests/main/snap-debug-timings/task.yaml snapd-2.48+21.04/tests/main/snap-debug-timings/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/snap-debug-timings/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/snap-debug-timings/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -1,11 +1,8 @@ summary: Ensure `snap debug change-timings` works execute: | - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB/snaps.sh" - echo "When a snap gets installed" - install_local test-snapd-sh + "$TESTSTOOLS"/snaps-state install-local test-snapd-sh echo "There is timing data available for it" snap debug timings --last=install | MATCH 'Done +[0-9]+' diff -Nru snapd-2.47.1+20.10.1build1/tests/main/snap-discard-ns/task.yaml snapd-2.48+21.04/tests/main/snap-discard-ns/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/snap-discard-ns/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/snap-discard-ns/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -7,9 +7,7 @@ is not an error if it doesn't exist. prepare: | - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB/snaps.sh" - install_local test-snapd-tools + "$TESTSTOOLS"/snaps-state install-local test-snapd-tools execute: | echo "We can try to discard a namespace before snap runs" diff -Nru snapd-2.47.1+20.10.1build1/tests/main/snap-disconnect/task.yaml snapd-2.48+21.04/tests/main/snap-disconnect/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/snap-disconnect/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/snap-disconnect/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -15,12 +15,6 @@ rm -f ./*.snap execute: | - #shellcheck source=tests/lib/systems.sh - . "$TESTSLIB"/systems.sh - - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB/snaps.sh" - inspect_connection() { CONN="$1" # shellcheck disable=SC2002 @@ -65,7 +59,7 @@ # these checks rely on automatic connection of home on non-core systems - if ! is_core_system; then + if ! os.query is-core; then echo "Checking that --forget forgets connection when auto-connected" snap remove --purge "$SNAP_FILE" snap install --dangerous "$SNAP_FILE" @@ -88,12 +82,12 @@ fi echo "Checking that a connection for missing plug can be forgotten" - install_local test-snap-producer - install_local test-snap-consumer.v1 + "$TESTSTOOLS"/snaps-state install-local test-snap-producer + "$TESTSTOOLS"/snaps-state install-local test-snap-consumer.v1 snap connect test-snap-consumer:shared-content-plug test-snap-producer:shared-content-slot snap connections test-snap-consumer | MATCH "content\[mylib\] *test-snap-consumer:shared-content-plug *test-snap-producer:shared-content-slot" # refresh to a newer version without content plug - install_local test-snap-consumer.v2 + "$TESTSTOOLS"/snaps-state install-local test-snap-consumer.v2 snap connections test-snap-consumer | not MATCH "content\[mylib\] *test-snap-consumer:shared-content-plug *test-snap-producer:shared-content-slot" inspect_connection "test-snap-consumer:shared-content-plug test-snap-producer:shared-content-slot" | MATCH "true" snap disconnect --forget test-snap-consumer:shared-content-plug test-snap-producer:shared-content-slot diff -Nru snapd-2.47.1+20.10.1build1/tests/main/snap-env/task.yaml snapd-2.48+21.04/tests/main/snap-env/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/snap-env/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/snap-env/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -2,19 +2,15 @@ environment: NAME/regular: test-snapd-tools - INSTANCE_KEY/regular: + INSTANCE_KEY/regular: "" NAME/parallel: test-snapd-tools_foo INSTANCE_KEY/parallel: foo prepare: | - # shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB/snaps.sh" - if [[ "$SPREAD_VARIANT" == "parallel" ]]; then snap set system experimental.parallel-instances=true fi - # shellcheck disable=SC2153 - install_local_as test-snapd-tools "$NAME" + "$TESTSTOOLS"/snaps-state install-local-as test-snapd-tools "$NAME" restore: | if [[ "$SPREAD_VARIANT" == "parallel" ]]; then diff -Nru snapd-2.47.1+20.10.1build1/tests/main/snap-handle-link/task.yaml snapd-2.48+21.04/tests/main/snap-handle-link/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/snap-handle-link/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/snap-handle-link/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -23,9 +23,6 @@ tests.session -u test restore execute: | - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB/snaps.sh" - echo "URI Handler fails if snap-store is not installed and user refuses to install it" mount --bind /bin/false /usr/bin/zenity if tests.session -u test exec snap handle-link snap://package 2>errors.log; then @@ -36,7 +33,7 @@ MATCH "Snap Store required" < errors.log echo "Now with snap-store installed" - install_local snap-store + "$TESTSTOOLS"/snaps-state install-local snap-store tests.session -u test exec snap handle-link snap://package | MATCH "Fake snap got snap://package" echo "The same should work with xdg-open" diff -Nru snapd-2.47.1+20.10.1build1/tests/main/snap-icons/task.yaml snapd-2.48+21.04/tests/main/snap-icons/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/snap-icons/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/snap-icons/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -1,11 +1,8 @@ summary: Snaps can install icon theme icons execute: | - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh - echo "Install a snap providing icons" - install_local test-snapd-icon-theme + "$TESTSTOOLS"/snaps-state install-local test-snapd-icon-theme echo "Icons provided by the snap are installed to a shared location" iconfile=/var/lib/snapd/desktop/icons/hicolor/scalable/apps/snap.test-snapd-icon-theme.foo.svg diff -Nru snapd-2.47.1+20.10.1build1/tests/main/snap-info/task.yaml snapd-2.48+21.04/tests/main/snap-info/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/snap-info/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/snap-info/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -32,10 +32,8 @@ > out PYTHONIOENCODING=utf8 python3 check.py < out - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh snap info --verbose "$TESTSLIB"/snaps/basic-desktop|MATCH "path: " - install_local basic-desktop + "$TESTSTOOLS"/snaps-state install-local basic-desktop snap info basic-desktop|MATCH "license:[ ]+GPL-3.0" snap info --verbose basic_1.0_all.snap|MATCH "sha3-384:" diff -Nru snapd-2.47.1+20.10.1build1/tests/main/snap-interface/task.yaml snapd-2.48+21.04/tests/main/snap-interface/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/snap-interface/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/snap-interface/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -4,9 +4,7 @@ The "snap interface" command displays a listing of used interfaces execute: | - # shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB/snaps.sh" - install_local network-consumer + "$TESTSTOOLS"/snaps-state install-local network-consumer snap interface | MATCH 'network\s+ allows access to the network' snap interface --all | MATCH 'classic-support\s+ special permissions for the classic snap' snap interface network > out.yaml diff -Nru snapd-2.47.1+20.10.1build1/tests/main/snap-logs/task.yaml snapd-2.48+21.04/tests/main/snap-logs/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/snap-logs/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/snap-logs/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -5,9 +5,7 @@ for all supported systems. prepare: | - # shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB/snaps.sh" - install_local test-snapd-service + "$TESTSTOOLS"/snaps-state install-local test-snapd-service execute: | echo "check the logs are displayed by service-name and service-name.app" diff -Nru snapd-2.47.1+20.10.1build1/tests/main/snap-mgmt/task.yaml snapd-2.48+21.04/tests/main/snap-mgmt/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/snap-mgmt/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/snap-mgmt/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -10,8 +10,6 @@ # TODO: unify this with tests/main/postrm-purge/task.yaml #shellcheck source=tests/lib/dirs.sh . "$TESTSLIB"/dirs.sh - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh snap set core experimental.user-daemons=true @@ -26,7 +24,7 @@ # None of the "user" snaps work on 14.04 continue fi - install_local "$name" + "$TESTSTOOLS"/snaps-state install-local "$name" snap list | MATCH test-snapd-service done diff -Nru snapd-2.47.1+20.10.1build1/tests/main/snap-routine-file-access/task.yaml snapd-2.48+21.04/tests/main/snap-routine-file-access/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/snap-routine-file-access/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/snap-routine-file-access/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -21,9 +21,7 @@ access. prepare: | - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh - install_local test-snapd-file-access + "$TESTSTOOLS"/snaps-state install-local test-snapd-file-access # Ensure interfaces are disconnected snap disconnect test-snapd-file-access:home @@ -100,8 +98,6 @@ if [[ "$SPREAD_SYSTEM" = ubuntu-core-* ]]; then exit 0 fi - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh - install_local test-snapd-classic-confinement --classic + "$TESTSTOOLS"/snaps-state install-local test-snapd-classic-confinement --classic snap routine file-access test-snapd-classic-confinement / | MATCH read-write diff -Nru snapd-2.47.1+20.10.1build1/tests/main/snap-routine-portal-info/task.yaml snapd-2.48+21.04/tests/main/snap-routine-portal-info/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/snap-routine-portal-info/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/snap-routine-portal-info/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -8,9 +8,7 @@ - -centos-7-* prepare: | - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh - install_local test-snapd-desktop + "$TESTSTOOLS"/snaps-state install-local test-snapd-desktop tests.session -u test prepare restore: | diff -Nru snapd-2.47.1+20.10.1build1/tests/main/snap-run/task.yaml snapd-2.48+21.04/tests/main/snap-run/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/snap-run/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/snap-run/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -5,10 +5,8 @@ systems: [-*-s390x] prepare: | - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh - install_local basic-run - install_local test-snapd-sh + "$TESTSTOOLS"/snaps-state install-local basic-run + "$TESTSTOOLS"/snaps-state install-local test-snapd-sh debug: | if [ -f stderr ]; then diff -Nru snapd-2.47.1+20.10.1build1/tests/main/snap-run-alias/task.yaml snapd-2.48+21.04/tests/main/snap-run-alias/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/snap-run-alias/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/snap-run-alias/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -9,9 +9,7 @@ ALIAS/testsnapdtoolscat: test_cat prepare: | - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh - install_local test-snapd-tools + "$TESTSTOOLS"/snaps-state install-local test-snapd-tools restore: | #shellcheck source=tests/lib/dirs.sh diff -Nru snapd-2.47.1+20.10.1build1/tests/main/snap-run-gdbserver/task.yaml snapd-2.48+21.04/tests/main/snap-run-gdbserver/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/snap-run-gdbserver/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/snap-run-gdbserver/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -4,9 +4,7 @@ systems: [ubuntu-16.04-*, ubuntu-18.04-*, ubuntu-2*] prepare: | - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh - install_local test-snapd-sh + "$TESTSTOOLS"/snaps-state install-local test-snapd-sh apt install -y gdbserver gdb restore: | @@ -18,7 +16,7 @@ # run the gdbserver command as a user # XXX: use "systemd-run --user -p StandardOutput=file:$(pwd)/stdout" # shellcheck disable=SC2016 - su -l -c 'snap run --experimental-gdbserver test-snapd-sh.sh -c "echo hello-hello-hello uid:$UID"' test >stdout & + su -c 'snap run --experimental-gdbserver test-snapd-sh.sh -c "echo hello-hello-hello uid:$UID"' test >stdout & # wait for the instructions that gdb is ready to get attached retry -n60 grep '^gdb -ex' stdout diff -Nru snapd-2.47.1+20.10.1build1/tests/main/snap-run-hook/task.yaml snapd-2.48+21.04/tests/main/snap-run-hook/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/snap-run-hook/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/snap-run-hook/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -8,9 +8,7 @@ ENVDUMP: /var/snap/basic-hooks/current/hooks-env prepare: | - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh - install_local basic-hooks + "$TESTSTOOLS"/snaps-state install-local basic-hooks execute: | # Note that `snap run` doesn't exit non-zero if the hook is missing, so we diff -Nru snapd-2.47.1+20.10.1build1/tests/main/snap-run-symlink/task.yaml snapd-2.48+21.04/tests/main/snap-run-symlink/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/snap-run-symlink/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/snap-run-symlink/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -7,9 +7,7 @@ APP/testsnapdtoolscat: test-snapd-tools.cat prepare: | - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh - install_local test-snapd-tools + "$TESTSTOOLS"/snaps-state install-local test-snapd-tools execute: | #shellcheck source=tests/lib/dirs.sh diff -Nru snapd-2.47.1+20.10.1build1/tests/main/snap-run-userdata-current/task.yaml snapd-2.48+21.04/tests/main/snap-run-userdata-current/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/snap-run-userdata-current/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/snap-run-userdata-current/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -3,9 +3,7 @@ systems: [-ubuntu-core-*] prepare: | - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh - install_local test-snapd-sh + "$TESTSTOOLS"/snaps-state install-local test-snapd-sh execute: | #shellcheck source=tests/lib/dirs.sh diff -Nru snapd-2.47.1+20.10.1build1/tests/main/snap-service/task.yaml snapd-2.48+21.04/tests/main/snap-service/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/snap-service/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/snap-service/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -4,9 +4,7 @@ execute: | echo "When the service snap is installed" - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh - install_local test-snapd-service + "$TESTSTOOLS"/snaps-state install-local test-snapd-service echo "We can see it running" systemctl status snap.test-snapd-service.test-snapd-service|MATCH "running" diff -Nru snapd-2.47.1+20.10.1build1/tests/main/snap-services/task.yaml snapd-2.48+21.04/tests/main/snap-services/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/snap-services/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/snap-services/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -4,11 +4,9 @@ rm -f ./*.out execute: | - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh - install_local test-snapd-service - install_local socket-activation - install_local test-snapd-timer-service + "$TESTSTOOLS"/snaps-state install-local test-snapd-service + "$TESTSTOOLS"/snaps-state install-local socket-activation + "$TESTSTOOLS"/snaps-state install-local test-snapd-timer-service snap services test-snapd-timer-service > timer-service.out MATCH '^test-snapd-timer-service.random-timer\s+ disabled\s+ (in)?active\s+ timer-activated$' < timer-service.out diff -Nru snapd-2.47.1+20.10.1build1/tests/main/snap-set/task.yaml snapd-2.48+21.04/tests/main/snap-set/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/snap-set/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/snap-set/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -1,17 +1,14 @@ summary: Check that `snap set` runs configure hook. prepare: | - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh - echo "Build basic test package (without hooks)" - install_local basic + "$TESTSTOOLS"/snaps-state install-local basic echo "Build failing hooks package" snap pack "$TESTSLIB"/snaps/failing-config-hooks echo "Build package with hook to run snapctl set" - install_local snapctl-hooks + "$TESTSTOOLS"/snaps-state install-local snapctl-hooks execute: | echo "Test that snap set fails without configure hook" diff -Nru snapd-2.47.1+20.10.1build1/tests/main/snapshot-basic/task.yaml snapd-2.48+21.04/tests/main/snapshot-basic/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/snapshot-basic/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/snapshot-basic/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -48,11 +48,17 @@ f2="$(tar -tf "${SET_ID}_export-2.snapshot" )" test "$f1" = "$f2" + # and is importable + RESTORE_ID=$( snap import-snapshot "${SET_ID}_export.snapshot" | head -n1 | cut -f2 -d'#' ) + # and is valid + snap check-snapshot "$RESTORE_ID" + # remove the canaries rm ~/snap/*/{current,common}/canary.txt - # remove the exports + # remove the exports and import rm "${SET_ID}"_export*.snapshot + snap forget "$RESTORE_ID" # restore one of them snap restore "$SET_ID" test-snapd-sh @@ -125,3 +131,16 @@ # the io.Copy uses a 32k buffer, so extra memory usage should be limited. # The threshold in this test is set to about 40MB test "$(cat memory-kb.txt)" -lt 40000 + + # check that snapshot set id from the filename has authority + snap install test-snapd-sh + snap save test-snapd-sh + SNAPSHOT_FILE=$(ls /var/lib/snapd/snapshots/*test-snapd-sh*.zip) + NEWSNAPSHOT_FILE=$(echo "$SNAPSHOT_FILE" | sed -e's/[1-9]\+_/123_/') + # rename the snapshot file to force a new set id. + mv "$SNAPSHOT_FILE" "$NEWSNAPSHOT_FILE" + snap saved | MATCH "123 +test-snapd-sh" + # make sure there is just one such snapshot + [[ $(snap saved | grep -c test-snapd-sh) == "1" ]] + snap saved --id=123 | MATCH "123 .+test-snapd-sh" + snap restore 123 test-snapd-sh diff -Nru snapd-2.47.1+20.10.1build1/tests/main/snaps-state/task.yaml snapd-2.48+21.04/tests/main/snaps-state/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/snaps-state/task.yaml 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48+21.04/tests/main/snaps-state/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -0,0 +1,76 @@ +summary: smoke test for the snaps-state tool + +prepare: | + snap set system experimental.parallel-instances=true + +restore: | + snap set system experimental.parallel-instances=null + +execute: | + SNAP_NAME=test-snapd-tools + SNAP_CLASSIC=test-snapd-classic-confinement + SNAP_DEVMODE=test-snapd-devmode + SNAP_JAILMODE=test-devmode-cgroup + + # Check help + "$TESTSTOOLS"/snaps-state | MATCH "usage: pack-local " + "$TESTSTOOLS"/snaps-state -h | MATCH "usage: pack-local " + "$TESTSTOOLS"/snaps-state --help | MATCH "usage: pack-local " + + # Pack a local snap by using the pack-local subcommand + snap_path=$("$TESTSTOOLS"/snaps-state pack-local "$SNAP_NAME") + snap install --dangerous "${snap_path}" + test-snapd-tools.echo test123 | MATCH "test123" + snap remove "$SNAP_NAME" + + # Check the local snap file is already created + test -f "$TESTSLIB/snaps/${SNAP_NAME}/${SNAP_NAME}_1.0_all.snap" + rm -f "$TESTSLIB/snaps/${SNAP_NAME}/${SNAP_NAME}_1.0_all.snap" + + # Try to pack a local snap which does not exist + "$TESTSTOOLS"/snaps-state pack-local SNAP_NO_EXIST 2>&1 | MATCH "snap.yaml file not found for SNAP_NO_EXIST snap" + + # Make and install a snap by using the install-local subcommand + snap_path=$("$TESTSTOOLS"/snaps-state install-local "$SNAP_NAME") + test-snapd-tools.echo test123 | MATCH "test123" + snap remove "$SNAP_NAME" + + # Check the local snap file is already created + test -f "$TESTSLIB/snaps/${SNAP_NAME}/${SNAP_NAME}_1.0_all.snap" + + # Make and install a snap when snap file is already created + snap_path=$("$TESTSTOOLS"/snaps-state install-local "$SNAP_NAME") + test-snapd-tools.echo test123 | MATCH "test123" + snap remove "$SNAP_NAME" + + # Check the local snap file is already created + test -f "$TESTSLIB/snaps/${SNAP_NAME}/${SNAP_NAME}_1.0_all.snap" + rm -f "$TESTSLIB/snaps/${SNAP_NAME}/${SNAP_NAME}_1.0_all.snap" + + # Make and install a snap by using the install-local-as subcommand + snap_path=$("$TESTSTOOLS"/snaps-state install-local-as "$SNAP_NAME" "$SNAP_NAME"_test) + test-snapd-tools_test.echo test123 | MATCH "test123" + snap remove "$SNAP_NAME"_test + rm -f "$TESTSLIB/snaps/${SNAP_NAME}/${SNAP_NAME}_test_1.0_all.snap" + + # Make and install a snap by using the install-local subcommand with --devmode + snap_path=$("$TESTSTOOLS"/snaps-state install-local "$SNAP_DEVMODE" --devmode) + snap list "$SNAP_DEVMODE" + snap remove "$SNAP_DEVMODE" + rm -f "$TESTSLIB/snaps/${SNAP_DEVMODE}/${SNAP_DEVMODE}_1.0_all.snap" + + # Make and install a snap by using the install-local subcommand with --classic + if snap debug sandbox-features --required=confinement-options:classic; then + snap_path=$("$TESTSTOOLS"/snaps-state install-local "$SNAP_CLASSIC" --classic) + snap list "$SNAP_CLASSIC" | MATCH 'classic$' + snap remove "$SNAP_CLASSIC" + rm -f "$TESTSLIB/snaps/${SNAP_CLASSIC}/${SNAP_CLASSIC}_1.0_all.snap" + fi + + # Make and install a snap by using the install-local subcommand with --jailmode + if [ "$(snap debug confinement)" = strict ] ; then + snap_path=$("$TESTSTOOLS"/snaps-state install-local "$SNAP_JAILMODE" --jailmode) + snap list "$SNAP_JAILMODE" | MATCH 'jailmode$' + snap remove "$SNAP_JAILMODE" + rm -f "$TESTSLIB/snaps/${SNAP_JAILMODE}/${SNAP_JAILMODE}_1.0_all.snap" + fi diff -Nru snapd-2.47.1+20.10.1build1/tests/main/snap-unset/task.yaml snapd-2.48+21.04/tests/main/snap-unset/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/snap-unset/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/snap-unset/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -1,11 +1,8 @@ summary: Check that `snap unset` works and removes config options. prepare: | - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh - echo "Build basic test snap" - install_local basic-hooks + "$TESTSTOOLS"/snaps-state install-local basic-hooks execute: | echo "Setting up initial configuration" diff -Nru snapd-2.47.1+20.10.1build1/tests/main/snap-update-ns/task.yaml snapd-2.48+21.04/tests/main/snap-update-ns/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/snap-update-ns/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/snap-update-ns/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -15,14 +15,12 @@ SLOT_SNAP: test-snapd-content-slot prepare: | - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh # NOTE: those are installed locally so that they are not connected because # of missing assertions. We are installing the slot before the plug snap so # that there's no attempt to load the default provider. Just in case # something changes we're disconnecting them so that tests are predictable. - install_local "$SLOT_SNAP" - install_local "$PLUG_SNAP" + "$TESTSTOOLS"/snaps-state install-local "$SLOT_SNAP" + "$TESTSTOOLS"/snaps-state install-local "$PLUG_SNAP" snap disconnect "$PLUG_SNAP:shared-content-plug" || : # Ensure there is no preserved mount namespace of the -plug snap. # (This one gets created because by connect hooks). diff -Nru snapd-2.47.1+20.10.1build1/tests/main/snap-userd-desktop-app-autostart/task.yaml snapd-2.48+21.04/tests/main/snap-userd-desktop-app-autostart/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/snap-userd-desktop-app-autostart/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/snap-userd-desktop-app-autostart/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -5,11 +5,8 @@ rm -f ~/snap/test-snapd-xdg-autostart/current/.config/autostart/foo.desktop execute: | - # shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB/snaps.sh" - echo "When the snap is installed" - install_local test-snapd-xdg-autostart + "$TESTSTOOLS"/snaps-state install-local test-snapd-xdg-autostart # run the app directly, it will dump a *.desktop file snap run test-snapd-xdg-autostart.foo diff -Nru snapd-2.47.1+20.10.1build1/tests/main/snap-user-service/task.yaml snapd-2.48+21.04/tests/main/snap-user-service/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/snap-user-service/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/snap-user-service/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -20,9 +20,7 @@ execute: | echo "When the service snap is installed" - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh - install_local test-snapd-user-service + "$TESTSTOOLS"/snaps-state install-local test-snapd-user-service echo "And the user mode systemd instance is started" tests.session -u test prepare diff -Nru snapd-2.47.1+20.10.1build1/tests/main/snap-user-service-restart-on-upgrade/task.yaml snapd-2.48+21.04/tests/main/snap-user-service-restart-on-upgrade/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/snap-user-service-restart-on-upgrade/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/snap-user-service-restart-on-upgrade/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -37,16 +37,14 @@ } echo "Install the a snap with user services while a user session is active" - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh - install_local test-snapd-user-service + "$TESTSTOOLS"/snaps-state install-local test-snapd-user-service echo "We can see the service running" systemctl_user is-active snap.test-snapd-user-service.test-snapd-user-service systemctl_user show -p MainPID snap.test-snapd-user-service.test-snapd-user-service > old-main.pid echo "When it is re-installed" - install_local test-snapd-user-service + "$TESTSTOOLS"/snaps-state install-local test-snapd-user-service echo "We can see the service running with a new PID" systemctl_user is-active snap.test-snapd-user-service.test-snapd-user-service diff -Nru snapd-2.47.1+20.10.1build1/tests/main/snap-user-service-socket-activation/task.yaml snapd-2.48+21.04/tests/main/snap-user-service-socket-activation/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/snap-user-service-socket-activation/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/snap-user-service-socket-activation/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -20,9 +20,7 @@ execute: | echo "When the service snap is installed" - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh - install_local test-snapd-user-service-sockets + "$TESTSTOOLS"/snaps-state install-local test-snapd-user-service-sockets echo "And the user mode systemd instance is started" tests.session -u test prepare diff -Nru snapd-2.47.1+20.10.1build1/tests/main/snap-user-service-start-on-install/task.yaml snapd-2.48+21.04/tests/main/snap-user-service-start-on-install/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/snap-user-service-start-on-install/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/snap-user-service-start-on-install/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -37,9 +37,7 @@ } echo "Install the a snap with user services while a user session is active" - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh - install_local test-snapd-user-service + "$TESTSTOOLS"/snaps-state install-local test-snapd-user-service echo "We can see the service running" systemctl_user status snap.test-snapd-user-service.test-snapd-user-service diff -Nru snapd-2.47.1+20.10.1build1/tests/main/snap-user-service-upgrade-failure/task.yaml snapd-2.48+21.04/tests/main/snap-user-service-upgrade-failure/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/snap-user-service-upgrade-failure/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/snap-user-service-upgrade-failure/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -37,9 +37,7 @@ } echo "Install the a snap with user services while a user session is active" - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh - install_local test-snapd-user-service + "$TESTSTOOLS"/snaps-state install-local test-snapd-user-service snap info test-snapd-user-service | MATCH '^installed:.* 1\.0 .*$' echo "We can see the service running" @@ -47,7 +45,7 @@ systemctl_user show -p MainPID snap.test-snapd-user-service.test-snapd-user-service > old-main.pid echo "When it is re-installed" - if install_local test-snapd-user-service-v2-bad; then + if "$TESTSTOOLS"/snaps-state install-local test-snapd-user-service-v2-bad; then echo "test-snapd-user-service v2 should not install cleanly, test broken" exit 1 fi diff -Nru snapd-2.47.1+20.10.1build1/tests/main/snap-wait/task.yaml snapd-2.48+21.04/tests/main/snap-wait/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/snap-wait/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/snap-wait/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -3,9 +3,7 @@ kill-timeout: 5m prepare: | - # shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB/snaps.sh" - install_local basic-hooks + "$TESTSTOOLS"/snaps-state install-local basic-hooks execute: | echo "Ensure snap wait for seeding works" diff -Nru snapd-2.47.1+20.10.1build1/tests/main/special-home-can-run-classic-snaps/task.yaml snapd-2.48+21.04/tests/main/special-home-can-run-classic-snaps/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/special-home-can-run-classic-snaps/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/special-home-can-run-classic-snaps/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -6,9 +6,7 @@ SPECIAL_USER_NAME/jenkins: jenkins SPECIAL_USER_NAME/postgres: postgres prepare: | - # shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB/snaps.sh" - install_local_classic test-snapd-classic-confinement + "$TESTSTOOLS"/snaps-state install-local test-snapd-classic-confinement --classic # Install the corresponding package that brings the special user account. # Specialize the code as required for a particular user. diff -Nru snapd-2.47.1+20.10.1build1/tests/main/stale-base-snap/task.yaml snapd-2.48+21.04/tests/main/stale-base-snap/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/stale-base-snap/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/stale-base-snap/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -22,9 +22,7 @@ prepare: | # We will need the Swiss-army-knife shell snap. We want to use it in # devmode so that we can look at /customized file which is non-standard. - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh - install_local_devmode test-snapd-sh + "$TESTSTOOLS"/snaps-state install-local test-snapd-sh --devmode # We will also need to prepare a hacked core snap that just differs from # our own in some detail that we can measure. @@ -32,6 +30,8 @@ touch squashfs-root/customized sed -i -e 's/version: .*/version: customized/' squashfs-root/meta/snap.yaml + #shellcheck source=tests/lib/snaps.sh + . "$TESTSLIB"/snaps.sh mksnap_fast squashfs-root core-customized.snap restore: | diff -Nru snapd-2.47.1+20.10.1build1/tests/main/sudo-env/task.yaml snapd-2.48+21.04/tests/main/sudo-env/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/sudo-env/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/sudo-env/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -7,8 +7,8 @@ environment: # list of regular expressions that match systems where sudo is set up to use - # secure_path - SECURE_PATH_SUDO: "fedora-.* centos-.* amazon-linux-2-64 opensuse-.* debian-.*" + # secure_path without snap bindir + SECURE_PATH_SUDO_NO_SNAP: "centos-.* amazon-linux-2-64 opensuse-.* debian-.*" # ubuntu-14.04: no support for user sessions used by test helpers systems: [ -ubuntu-14.04-* ] @@ -35,7 +35,7 @@ tests.session -u test exec sudo --login sh -c 'echo :$PATH:' > sudo-login.path secure_path=no - for regex in $SECURE_PATH_SUDO ; do + for regex in $SECURE_PATH_SUDO_NO_SNAP ; do if echo "$SPREAD_SYSTEM" | grep -Eq "$regex" ; then secure_path=yes break diff -Nru snapd-2.47.1+20.10.1build1/tests/main/systemd-service/task.yaml snapd-2.48+21.04/tests/main/systemd-service/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/systemd-service/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/systemd-service/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -7,11 +7,8 @@ rm -f ./*.snap execute: | - # shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB/snaps.sh" - echo "Given a service snap is installed" - install_local test-snapd-service + "$TESTSTOOLS"/snaps-state install-local test-snapd-service echo "When the service state is reported as active" while ! systemctl show -p ActiveState "$SERVICE_NAME" | grep -Pq "ActiveState=active"; do sleep 0.5; done diff -Nru snapd-2.47.1+20.10.1build1/tests/main/system-usernames/task.yaml snapd-2.48+21.04/tests/main/system-usernames/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/system-usernames/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/system-usernames/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -23,9 +23,7 @@ prepare: | echo "Install helper snaps with default confinement" - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh - install_local test-snapd-sh + "$TESTSTOOLS"/snaps-state install-local test-snapd-sh restore: | # make sure this snap is removed in case it was installed diff -Nru snapd-2.47.1+20.10.1build1/tests/main/try/task.yaml snapd-2.48+21.04/tests/main/try/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/try/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/try/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -1,25 +1,22 @@ summary: Check that try command works # s390x does not have /dev/kmsg -systems: [-ubuntu-core-*, -fedora-*, -opensuse-*, -arch-*, -ubuntu-*-s390x, -centos-*] +# ubuntu-14.04: systemd-run not supported +systems: [-ubuntu-core-*, -fedora-*, -opensuse-*, -arch-*, -ubuntu-*-s390x, -centos-*, -ubuntu-14.04*] environment: PORT: 8081 - SERVICE_FILE: "./service.sh" READABLE_FILE: "/var/snap/test-snapd-tools/x1/file.txt" SERVICE_NAME: "test-service" prepare: | # shellcheck source=tests/lib/network.sh . "$TESTSLIB"/network.sh - make_network_service "$SERVICE_NAME" "$SERVICE_FILE" "$PORT" + make_network_service "$SERVICE_NAME" "$PORT" restore: | - #shellcheck source=tests/lib/systemd.sh - . "$TESTSLIB"/systemd.sh - #shellcheck disable=SC2153 - systemd_stop_and_destroy_unit "$SERVICE_NAME" - rm -f "$SERVICE_FILE" "$READABLE_FILE" + systemctl stop "$SERVICE_NAME" + rm -f "$READABLE_FILE" execute: | echo "Given a buildable snap in a known directory" diff -Nru snapd-2.47.1+20.10.1build1/tests/main/uc20-create-partitions/task.yaml snapd-2.48+21.04/tests/main/uc20-create-partitions/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/uc20-create-partitions/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/uc20-create-partitions/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -61,12 +61,6 @@ file -s "${LOOP}p3" | MATCH 'ext4 filesystem data,.* volume name "ubuntu-boot"' file -s "${LOOP}p4" | MATCH 'ext4 filesystem data,.* volume name "ubuntu-data"' - echo "Check if attribute bits were set for new partitions" - sfdisk -d "$LOOP" | not MATCH "${LOOP}p1.*attrs=\"GUID:59\"" - sfdisk -d "$LOOP" | not MATCH "${LOOP}p2.*attrs=\"GUID:59\"" - sfdisk -d "$LOOP" | MATCH "${LOOP}p3.*attrs=\"GUID:59\"" - sfdisk -d "$LOOP" | MATCH "${LOOP}p4.*attrs=\"GUID:59\"" - echo "Check that the filesystems were not auto-mounted" mount | not MATCH /run/mnt/ubuntu-seed mount | not MATCH /run/mnt/ubuntu-boot @@ -107,7 +101,6 @@ mount "${LOOP}p3" ./mnt ls ./mnt/EFI/boot/grubx64.efi ls ./mnt/EFI/boot/bootx64.efi - ls ./mnt/EFI/ubuntu/grub.cfg # remove a file rm ./mnt/EFI/boot/grubx64.efi umount ./mnt diff -Nru snapd-2.47.1+20.10.1build1/tests/main/uc20-create-partitions-encrypt/task.yaml snapd-2.48+21.04/tests/main/uc20-create-partitions-encrypt/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/uc20-create-partitions-encrypt/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/uc20-create-partitions-encrypt/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -100,6 +100,8 @@ echo "Check that the key file was created" test "$(stat --printf=%s unsealed-key)" -eq 64 + # recovery key is 16 bytes long + test "$(stat --printf=%s recovery-key)" -eq 16 echo "Check that the partitions are created" sfdisk -d "$LOOP" | MATCH "^${LOOP}p1 .*size=\s*2048, type=21686148-6449-6E6F-744E-656564454649,.*BIOS Boot" @@ -128,7 +130,7 @@ # Test the recovery key echo "Ensure that we can open the encrypted device using the recovery key" - cryptsetup open --key-file /run/mnt/ubuntu-data/system-data/var/lib/snapd/device/fde/recovery.key "${LOOP}p4" test-recovery + cryptsetup open --key-file recovery-key "${LOOP}p4" test-recovery mount /dev/mapper/test-recovery ./mnt umount ./mnt cryptsetup close /dev/mapper/test-recovery diff -Nru snapd-2.47.1+20.10.1build1/tests/main/uc20-create-partitions-reinstall/task.yaml snapd-2.48+21.04/tests/main/uc20-create-partitions-reinstall/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/uc20-create-partitions-reinstall/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/uc20-create-partitions-reinstall/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -16,6 +16,9 @@ if [ -f loop.txt ]; then losetup -d "$(cat loop.txt)" fi + umount ./ubuntu-seed || true + umount ./ubuntu-boot || true + umount ./ubuntu-data || true prepare: | echo "Create a fake block device image that looks like an image from u-i" @@ -61,11 +64,15 @@ file -s "${LOOP}p3" | MATCH 'ext4 filesystem data,.* volume name "ubuntu-boot"' file -s "${LOOP}p4" | MATCH 'ext4 filesystem data,.* volume name "ubuntu-data"' - echo "Check if attribute bits were set for new partitions" - sfdisk -d "$LOOP" | not MATCH "${LOOP}p1.*attrs=\"GUID:59\"" - sfdisk -d "$LOOP" | not MATCH "${LOOP}p2.*attrs=\"GUID:59\"" - sfdisk -d "$LOOP" | MATCH "${LOOP}p3.*attrs=\"GUID:59\"" - sfdisk -d "$LOOP" | MATCH "${LOOP}p4.*attrs=\"GUID:59\"" + echo "Create canary files on the ubuntu-{seed,boot,data} partitions" + mkdir ./ubuntu-seed ./ubuntu-boot ./ubuntu-data + mount "${LOOP}p2" ./ubuntu-seed + mount "${LOOP}p3" ./ubuntu-boot + mount "${LOOP}p4" ./ubuntu-data + for label in ubuntu-seed ubuntu-boot ubuntu-data; do + echo "$label" > ./"$label"/canary.txt + umount ./"$label" + done # re-create partitions on a new install attempt echo "Run the snap-bootstrap again" @@ -77,3 +84,13 @@ sfdisk -l "$LOOP" | not MATCH "${LOOP}p[56789]" file -s "${LOOP}p3" | MATCH 'ext4 filesystem data,.* volume name "ubuntu-boot"' file -s "${LOOP}p4" | MATCH 'ext4 filesystem data,.* volume name "ubuntu-data"' + + echo "Mount partitions again" + mount "${LOOP}p2" ./ubuntu-seed + mount "${LOOP}p3" ./ubuntu-boot + mount "${LOOP}p4" ./ubuntu-data + echo "The ubuntu-seed partition is still there untouched" + test -e ./ubuntu-seed/canary.txt + echo "But ubuntu-{boot,data} got re-created" + not test -e ./ubuntu-boot/canary.txt + not test -e ./ubuntu-data/canary.txt diff -Nru snapd-2.47.1+20.10.1build1/tests/main/umask/task.yaml snapd-2.48+21.04/tests/main/umask/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/umask/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/umask/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -3,9 +3,7 @@ The setting of umask is inherited across the snap-{run,confine,exec} chain but does not hinder execution of snap-confine itself. prepare: | - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh - install_local test-snapd-sh + "$TESTSTOOLS"/snaps-state install-local test-snapd-sh restore: | snap remove test-snapd-sh rm -rf ~/snap diff -Nru snapd-2.47.1+20.10.1build1/tests/main/user-mounts/task.yaml snapd-2.48+21.04/tests/main/user-mounts/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/user-mounts/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/user-mounts/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -15,9 +15,7 @@ - -ubuntu-14.04-* # no tests.session prepare: | - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh - install_local test-snapd-desktop + "$TESTSTOOLS"/snaps-state install-local test-snapd-desktop snap disconnect test-snapd-desktop:desktop tests.session -u test prepare diff -Nru snapd-2.47.1+20.10.1build1/tests/main/xdg-open/task.yaml snapd-2.48+21.04/tests/main/xdg-open/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/xdg-open/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/xdg-open/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -22,9 +22,7 @@ fi prepare: | - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB/snaps.sh" - install_local test-snapd-desktop + "$TESTSTOOLS"/snaps-state install-local test-snapd-desktop tests.session -u test prepare @@ -53,8 +51,6 @@ execute: | #shellcheck source=tests/lib/dirs.sh . "$TESTSLIB/dirs.sh" - #shellcheck source=tests/lib/systems.sh - . "$TESTSLIB"/systems.sh ensure_xdg_open_output() { rm -f /tmp/xdg-open-output diff -Nru snapd-2.47.1+20.10.1build1/tests/main/xdg-open-compat/task.yaml snapd-2.48+21.04/tests/main/xdg-open-compat/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/xdg-open-compat/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/xdg-open-compat/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -30,9 +30,7 @@ fi prepare: | - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB/snaps.sh" - install_local test-snapd-desktop + "$TESTSTOOLS"/snaps-state install-local test-snapd-desktop # make a backup of the original xdg-open if [ -e /usr/bin/xdg-open ]; then diff -Nru snapd-2.47.1+20.10.1build1/tests/main/xdg-open-portal/task.yaml snapd-2.48+21.04/tests/main/xdg-open-portal/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/xdg-open-portal/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/xdg-open-portal/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -25,10 +25,7 @@ setup_portals tests.session -u test prepare - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB/snaps.sh" - - install_local test-snapd-desktop + "$TESTSTOOLS"/snaps-state install-local test-snapd-desktop # Configure fake web browser tests.session -u test exec mkdir -p ~test/.local/share/applications diff -Nru snapd-2.47.1+20.10.1build1/tests/main/xdg-settings/task.yaml snapd-2.48+21.04/tests/main/xdg-settings/task.yaml --- snapd-2.47.1+20.10.1build1/tests/main/xdg-settings/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/main/xdg-settings/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -15,9 +15,7 @@ rm -f stderr.log prepare: | - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB/snaps.sh" - install_local test-snapd-xdg-settings + "$TESTSTOOLS"/snaps-state install-local test-snapd-xdg-settings tests.session -u test prepare diff -Nru snapd-2.47.1+20.10.1build1/tests/nested/classic/hotplug/task.yaml snapd-2.48+21.04/tests/nested/classic/hotplug/task.yaml --- snapd-2.47.1+20.10.1build1/tests/nested/classic/hotplug/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/nested/classic/hotplug/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -31,6 +31,7 @@ nested_exec "snap list" nested_exec "dmesg" nested_exec 'sudo jq -r ".data[\"hotplug-slots\"]" /var/lib/snapd/state.json' + set -e execute: | #shellcheck source=tests/lib/nested.sh diff -Nru snapd-2.47.1+20.10.1build1/tests/nested/core/hotplug/task.yaml snapd-2.48+21.04/tests/nested/core/hotplug/task.yaml --- snapd-2.47.1+20.10.1build1/tests/nested/core/hotplug/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/nested/core/hotplug/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -26,6 +26,7 @@ nested_exec "snap list" nested_exec "dmesg" nested_exec 'sudo jq -r ".data[\"hotplug-slots\"]" /var/lib/snapd/state.json' + set -e execute: | #shellcheck source=tests/lib/nested.sh diff -Nru snapd-2.47.1+20.10.1build1/tests/nested/core20/basic/task.yaml snapd-2.48+21.04/tests/nested/core20/basic/task.yaml --- snapd-2.47.1+20.10.1build1/tests/nested/core20/basic/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/nested/core20/basic/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -31,3 +31,12 @@ echo "Ensure 'snap list' works and test-snapd-sh snap is removed" nested_exec "! snap list test-snapd-sh" + + echo "Ensure 'snap debug show-keys' works as root" + nested_exec "sudo snap recovery --show-keys" | MATCH 'recovery:\s+[0-9]{5}-[0-9]{5}-[0-9]{5}-[0-9]{5}-[0-9]{5}-[0-9]{5}-[0-9]{5}-[0-9]{5}' + nested_exec "sudo snap recovery --show-keys" | MATCH 'reinstall:\s+[0-9]{5}-[0-9]{5}-[0-9]{5}-[0-9]{5}-[0-9]{5}-[0-9]{5}-[0-9]{5}-[0-9]{5}' + echo "But not as user (normal file permissions prevent this)" + if nested_exec "snap recovery --show-key"; then + echo "snap recovery --show-key should not work as a user" + exit 1 + fi diff -Nru snapd-2.47.1+20.10.1build1/tests/nested/core20/degraded/task.yaml snapd-2.48+21.04/tests/nested/core20/degraded/task.yaml --- snapd-2.47.1+20.10.1build1/tests/nested/core20/degraded/task.yaml 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48+21.04/tests/nested/core20/degraded/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -0,0 +1,49 @@ +summary: Transition to recover mode with things missing so we use degraded mode + +environment: + DEGRADED_JSON: /run/snapd/snap-bootstrap/degraded.json + +execute: | + #shellcheck source=tests/lib/nested.sh + . "$TESTSLIB/nested.sh" + + # wait for the system to be seeded first + + nested_wait_for_snap_command + nested_exec "sudo snap wait system seed.loaded" + + echo "Install jq in the host environment" + snap install jq + + echo "Move the run key for ubuntu-save out of the way so we use the fallback key to unlock ubuntu-save" + nested_exec "sudo mv /run/mnt/data/system-data/var/lib/snapd/device/fde/ubuntu-save.key /run/mnt/data/system-data/var/lib/snapd/device/fde/ubuntu-save.key.bk" + + recoverySystem=$(nested_exec "sudo snap recovery | grep -v Notes | grep -Po '^[0-9]+'") + + echo "Transition to recover mode" + nested_uc20_transition_to_system_mode "$recoverySystem" recover + + nested_wait_for_snap_command + nested_exec "sudo snap wait system seed.loaded" + + echo "Check degraded.json exists and has the unlock-key for ubuntu-save as the fallback key" + nested_exec "test -f $DEGRADED_JSON" + test "$(nested_exec "cat $DEGRADED_JSON" | jq -r '."ubuntu-save" | ."unlock-key"')" = fallback + + echo "Move the run object key for ubuntu-save back and go back to run mode" + nested_exec "sudo mv /run/mnt/host/ubuntu-data/system-data/var/lib/snapd/device/fde/ubuntu-save.key.bk /run/mnt/host/ubuntu-data/system-data/var/lib/snapd/device/fde/ubuntu-save.key" + nested_uc20_transition_to_system_mode "$recoverySystem" run + + nested_wait_for_snap_command + nested_exec "sudo snap wait system seed.loaded" + + echo "Now move the run object key on ubuntu-boot out of the way so we use the fallback key to unlock ubuntu-data" + nested_exec "sudo mv /run/mnt/ubuntu-boot/device/fde/ubuntu-data.sealed-key /run/mnt/ubuntu-boot/device/fde/ubuntu-data.sealed-key.bk" + nested_uc20_transition_to_system_mode "$recoverySystem" recover + + nested_wait_for_snap_command + nested_exec "sudo snap wait system seed.loaded" + + echo "Check degraded.json exists and has the unlock-key for ubuntu-data as the fallback key" + nested_exec "test -f $DEGRADED_JSON" + test "$(nested_exec "cat $DEGRADED_JSON" | jq -r '."ubuntu-data" | ."unlock-key"')" = fallback diff -Nru snapd-2.47.1+20.10.1build1/tests/nested/core20/gadget-reseal/task.yaml snapd-2.48+21.04/tests/nested/core20/gadget-reseal/task.yaml --- snapd-2.47.1+20.10.1build1/tests/nested/core20/gadget-reseal/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/nested/core20/gadget-reseal/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -5,7 +5,7 @@ execute: | # shellcheck source=tests/lib/nested.sh . "$TESTSLIB/nested.sh" - SEALED_KEY_MTIME_1="$(nested_exec sudo stat --format="%Y" /run/mnt/ubuntu-seed/device/fde/ubuntu-data.sealed-key)" + SEALED_KEY_MTIME_1="$(nested_exec sudo stat --format="%Y" /run/mnt/ubuntu-boot/device/fde/ubuntu-data.sealed-key)" RESEAL_COUNT_1="$(nested_exec sudo cat /var/lib/snapd/device/fde/boot-chains | python3 -m json.tool | grep reseal-count|cut -f2 -d: | tr ',' ' ')" # Install new (unasserted) gadget without changes and wait for change without reboot boot_id="$( nested_get_boot_id )" @@ -14,7 +14,7 @@ nested_exec sudo snap watch "${REMOTE_CHG_ID}" # nothing in the gadget has changed so no reseal was needed - SEALED_KEY_MTIME_2="$(nested_exec sudo stat --format="%Y" /run/mnt/ubuntu-seed/device/fde/ubuntu-data.sealed-key)" + SEALED_KEY_MTIME_2="$(nested_exec sudo stat --format="%Y" /run/mnt/ubuntu-boot/device/fde/ubuntu-data.sealed-key)" test "$SEALED_KEY_MTIME_2" -eq "$SEALED_KEY_MTIME_1" RESEAL_COUNT_2="$(nested_exec sudo cat /var/lib/snapd/device/fde/boot-chains | python3 -m json.tool | grep reseal-count|cut -f2 -d: | tr ',' ' ')" test "$RESEAL_COUNT_2" = "$RESEAL_COUNT_1" @@ -25,8 +25,8 @@ SNAKEOIL_CERT="$PWD/$KEY_NAME.pem" # ensure clean pc-gadget dir rm -rf pc-gadget - snap download --basename=pc --channel="20/edge" pc - unsquashfs -d pc-gadget pc.snap + GADGET_SNAP="$(ls "$NESTED_ASSETS_DIR"/pc_*.snap)" + unsquashfs -d pc-gadget "$GADGET_SNAP" # change a few bytes in the compat header and ensure sed worked sed -i 's/This program cannot be run in DOS mode/This program cannot be run in XXX mode/' pc-gadget/grubx64.efi grep -q -a "This program cannot be run in XXX mode" pc-gadget/grubx64.efi @@ -35,8 +35,8 @@ mv modified_gadget.yaml pc-gadget/meta/gadget.yaml # resign both assets - nested_secboot_sign_file pc-gadget "$SNAKEOIL_KEY" "$SNAKEOIL_CERT" "shim.efi.signed" - nested_secboot_sign_file pc-gadget "$SNAKEOIL_KEY" "$SNAKEOIL_CERT" "grubx64.efi" + nested_secboot_sign_file pc-gadget/shim.efi.signed "$SNAKEOIL_KEY" "$SNAKEOIL_CERT" + nested_secboot_sign_file pc-gadget/grubx64.efi "$SNAKEOIL_KEY" "$SNAKEOIL_CERT" rm -f "$SNAKEOIL_KEY" "$SNAKEOIL_CERT" snap pack pc-gadget/ @@ -46,8 +46,11 @@ nested_wait_for_reboot "${boot_id}" nested_exec sudo snap watch "${REMOTE_CHG_ID}" + # sanity check that the gadget asset was changed + nested_exec sudo grep -q -a "This program cannot be run in XXX mode" /run/mnt/ubuntu-boot/EFI/boot/grubx64.efi + # the gadget has changed, we should see resealing - SEALED_KEY_MTIME_3="$(nested_exec sudo stat --format="%Y" /run/mnt/ubuntu-seed/device/fde/ubuntu-data.sealed-key)" + SEALED_KEY_MTIME_3="$(nested_exec sudo stat --format="%Y" /run/mnt/ubuntu-boot/device/fde/ubuntu-data.sealed-key)" test "$SEALED_KEY_MTIME_3" -gt "$SEALED_KEY_MTIME_2" RESEAL_COUNT_3="$(nested_exec sudo cat /var/lib/snapd/device/fde/boot-chains | python3 -m json.tool | grep reseal-count|cut -f2 -d: | tr ',' ' ')" test "$RESEAL_COUNT_3" -gt "1" diff -Nru snapd-2.47.1+20.10.1build1/tests/nested/core20/kernel-failover/task.yaml snapd-2.48+21.04/tests/nested/core20/kernel-failover/task.yaml --- snapd-2.47.1+20.10.1build1/tests/nested/core20/kernel-failover/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/nested/core20/kernel-failover/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -15,7 +15,6 @@ # running system, we should look into that, but for now just # re-download the snap snap download pc-kernel --channel=20/edge --basename=pc-kernel - rm -rf repacked-kernel uc20_build_initramfs_kernel_snap pc-kernel.snap "$PWD" --inject-kernel-panic-in-initramfs mv pc-kernel_*.snap panicking-initramfs-kernel.snap diff -Nru snapd-2.47.1+20.10.1build1/tests/nested/core20/kernel-reseal/task.yaml snapd-2.48+21.04/tests/nested/core20/kernel-reseal/task.yaml --- snapd-2.47.1+20.10.1build1/tests/nested/core20/kernel-reseal/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/nested/core20/kernel-reseal/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -6,17 +6,22 @@ # shellcheck source=tests/lib/nested.sh . "$TESTSLIB/nested.sh" - snap download pc-kernel --channel=20/edge --basename=pc-kernel - unsquashfs -d pc-kernel pc-kernel.snap + # we cannot use the kernel from store as it may have a version of + # snap-bootstrap that will not be able to unseal the keys and unlock the + # encrypted volumes, instead use the kernel we repacked when building the UC20 + # image + KERNEL_SNAP="$(ls "$NESTED_ASSETS_DIR"/pc-kernel_*.snap)" + unsquashfs -d pc-kernel "$KERNEL_SNAP" # ensure we really have the header we expect grep -q -a "This program cannot be run in DOS mode" pc-kernel/kernel.efi # modify the kernel so that the hash changes - sed -i 's/This program cannot be run in DOS mode/This program cannot be run in D0S mode/' pc-kernel/kernel.efi + sed -i 's/This program cannot be run in DOS mode/This program cannot be run in XXX mode/' pc-kernel/kernel.efi + grep -q -a "This program cannot be run in XXX mode" pc-kernel/kernel.efi KEY_NAME=$(nested_get_snakeoil_key) SNAKEOIL_KEY="$PWD/$KEY_NAME.key" SNAKEOIL_CERT="$PWD/$KEY_NAME.pem" - nested_secboot_sign_file "$PWD/pc-kernel" "$SNAKEOIL_KEY" "$SNAKEOIL_CERT" kernel.efi + nested_secboot_sign_file "$PWD/pc-kernel/kernel.efi" "$SNAKEOIL_KEY" "$SNAKEOIL_CERT" snap pack pc-kernel rm -rf pc-kernel @@ -27,7 +32,7 @@ # shellcheck source=tests/lib/nested.sh . "$TESTSLIB/nested.sh" - SEALED_KEY_MTIME_1="$(nested_exec sudo stat --format="%Y" /run/mnt/ubuntu-seed/device/fde/ubuntu-data.sealed-key)" + SEALED_KEY_MTIME_1="$(nested_exec sudo stat --format="%Y" /run/mnt/ubuntu-boot/device/fde/ubuntu-data.sealed-key)" RESEAL_COUNT_1="$(nested_exec sudo cat /var/lib/snapd/device/fde/boot-chains | python3 -m json.tool | grep reseal-count|cut -f2 -d: | tr ',' ' ')" # Install new (unasserted) kernel and wait for reboot/change finishing @@ -36,8 +41,11 @@ nested_wait_for_reboot "${boot_id}" nested_exec sudo snap watch "${REMOTE_CHG_ID}" + # sanity check that we are using the right kernel + nested_exec sudo grep -q -a "This program cannot be run in XXX mode" /boot/grub/kernel.efi + # ensure ubuntu-data.sealed-key mtime is newer - SEALED_KEY_MTIME_2="$(nested_exec sudo stat --format="%Y" /run/mnt/ubuntu-seed/device/fde/ubuntu-data.sealed-key)" + SEALED_KEY_MTIME_2="$(nested_exec sudo stat --format="%Y" /run/mnt/ubuntu-boot/device/fde/ubuntu-data.sealed-key)" test "$SEALED_KEY_MTIME_2" -gt "$SEALED_KEY_MTIME_1" # check that we have boot chains diff -Nru snapd-2.47.1+20.10.1build1/tests/nested/core20/save/task.yaml snapd-2.48+21.04/tests/nested/core20/save/task.yaml --- snapd-2.47.1+20.10.1build1/tests/nested/core20/save/task.yaml 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48+21.04/tests/nested/core20/save/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -0,0 +1,41 @@ +summary: Check that ubuntu-save is set up in a UC20 device + +description: | + This test checks that ubuntu-save is preset and set up correctly in a UC20 + device + +execute: | + #shellcheck source=tests/lib/nested.sh + . "$TESTSLIB/nested.sh" + + # check that ubuntu-save is mounted and has a reasonable amount of free + # space, example df output: + # Filesystem 1B-blocks Used Available Use% Mounted on + # /dev/mapper/ubuntu-save-0bed13ef-f71f-418f-b046-b1ce32dd04a7 5079040 28672 4390912 1% /run/mnt/ubuntu-save + save_out="$(nested_exec "df -B1 /run/mnt/ubuntu-save | tail -1")" + echo "$save_out" | MATCH '^/dev/mapper/ubuntu-save-[0-9a-z-]+\s+' + save_size="$(echo "$save_out" | awk '{print $4}')" + echo "check there is at least 6MB of free space available on ubuntu-save" + test "$save_size" -gt "$((6*1024*1024))" + + # leave a canary + nested_exec "sudo touch /run/mnt/ubuntu-save/canary" + + nested_exec mountpoint /var/lib/snapd/save + # we know that save is mounted using a systemd unit + nested_exec systemctl status var-lib-snapd-save.mount + # and a canary exists + nested_exec "test -f /var/lib/snapd/save/canary" + + # transition to recovery mode and check again + boot_id="$(nested_get_boot_id)" + # shellcheck disable=SC2016 + nested_exec 'sudo snap reboot --recover $(sudo snap recovery | grep -v Label | awk "{print \$1}")' + nested_wait_for_reboot "${boot_id}" + # verify in recover mode + nested_exec 'sudo cat /proc/cmdline' | MATCH snapd_recovery_mode=recover + + recover_save_out="$(nested_exec "df -B1 /run/mnt/ubuntu-save | tail -1")" + echo "$recover_save_out" | MATCH '^/dev/mapper/ubuntu-save-[0-9a-z-]+\s+' + # and a canary exists + nested_exec "test -f /run/mnt/ubuntu-save/canary" diff -Nru snapd-2.47.1+20.10.1build1/tests/nested/core20/tpm/task.yaml snapd-2.48+21.04/tests/nested/core20/tpm/task.yaml --- snapd-2.47.1+20.10.1build1/tests/nested/core20/tpm/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/nested/core20/tpm/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -21,6 +21,10 @@ echo "and has the expected owner and permissions" nested_exec "stat --printf='%u:%g %a' /var/lib/snapd/device/fde/recovery.key" | MATCH '^0:0 600$' + echo "and the tpm-{policy-auth-key,lockout-auth} files are in ubuntu-save" + nested_exec "test -e /var/lib/snapd/save/device/fde/tpm-policy-auth-key" + nested_exec "test -e /var/lib/snapd/save/device/fde/tpm-lockout-auth" + # grab modeenv content nested_exec "cat /var/lib/snapd/modeenv" > modeenv # and checksums diff -Nru snapd-2.47.1+20.10.1build1/tests/nested/manual/cloud-init-never-used-not-vuln/seed/meta-data snapd-2.48+21.04/tests/nested/manual/cloud-init-never-used-not-vuln/seed/meta-data --- snapd-2.47.1+20.10.1build1/tests/nested/manual/cloud-init-never-used-not-vuln/seed/meta-data 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/nested/manual/cloud-init-never-used-not-vuln/seed/meta-data 1970-01-01 00:00:00.000000000 +0000 @@ -1,2 +0,0 @@ -instance-id: iid-local01 -local-hostname: cloudimg diff -Nru snapd-2.47.1+20.10.1build1/tests/nested/manual/cloud-init-never-used-not-vuln/seed/user-data snapd-2.48+21.04/tests/nested/manual/cloud-init-never-used-not-vuln/seed/user-data --- snapd-2.47.1+20.10.1build1/tests/nested/manual/cloud-init-never-used-not-vuln/seed/user-data 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/nested/manual/cloud-init-never-used-not-vuln/seed/user-data 1970-01-01 00:00:00.000000000 +0000 @@ -1,7 +0,0 @@ -#cloud-config -users: - - default - - name: attacker-user - sudo: ALL=(ALL) NOPASSWD:ALL - lock_passwd: false - passwd: $6$rounds=4096$ftDwPSSVP0Jq9$4hXIcusbcZMxbSnfv8D/vp/bdzgVAds9qGcFjeBvv1Ths9mLiNPKAxW8/1zOtGLPKsEcorUOzl16hn9jxswDz0 diff -Nru snapd-2.47.1+20.10.1build1/tests/nested/manual/cloud-init-never-used-not-vuln/task.yaml snapd-2.48+21.04/tests/nested/manual/cloud-init-never-used-not-vuln/task.yaml --- snapd-2.47.1+20.10.1build1/tests/nested/manual/cloud-init-never-used-not-vuln/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/nested/manual/cloud-init-never-used-not-vuln/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -16,12 +16,18 @@ #shellcheck source=tests/lib/nested.sh . "$TESTSLIB/nested.sh" - nested_create_core_vm - nested_start_core_vm - - # build the cloud-init NoCloud cdrom drive - cd seed - genisoimage -output ../seed1.iso -volid cidata -joliet -rock user-data meta-data + # build an unrelated empty cdrom drive to provide to first boot with no + # real files on it to use as a placeholder in qemu args + nested_build_seed_cdrom "$TESTSLIB/cloud-init-seeds/emptykthxbai" seed.iso notcidata emptykthxbai + + # build the attacker cloud-init NoCloud cdrom drive + nested_build_seed_cdrom "$TESTSLIB/cloud-init-seeds/attacker-user" seed2.iso cidata user-data meta-data + + "$TESTSTOOLS"/nested-state build-image core + + # first boot will use seed1 which is empty, but the same file name will be + # replace while the VM is shutdown to use the second attacker iso + "$TESTSTOOLS"/nested-state create-vm core --param-cdrom "-cdrom $(pwd)/seed.iso" debug: | if [ -f snapd-before-reboot.logs ]; then @@ -40,11 +46,9 @@ #shellcheck source=tests/lib/snaps.sh . "$TESTSLIB/snaps.sh" - # the VM here will not ever had used cloud-init so snapd should disable # cloud-init when it is installed - # wait for done seeding nested_exec "sudo snap wait system seed.loaded" @@ -106,24 +110,15 @@ # gracefully shutdown so that we don't have file corruption echo "Gracefully shutting down the nested VM to prepare a simulated attack" - nested_exec "sudo shutdown -h now" || true - nested_wait_for_no_ssh + boot_id="$(nested_get_boot_id)" + "$TESTSTOOLS"/nested-state stop-vm + + # replace the empty seed.iso with the new attacker iso + mv seed2.iso seed.iso - # now if we reboot and add a cloud-init drive, it will not be imported onto - # the system - nested_force_stop_vm - - # add the cloud-init drive as a CD-ROM drive - NESTED_PARAM_CD="-cdrom $(pwd)/seed1.iso" - - # start the unit so that it re-uses the existing drive and doesn't re-setup - # ssh - # TODO: this interaction is a bit awkward because we want to change the - # args to qemu, but those are put into a systemd unit file in /run, which - # is awkward to modify programmatically, so instead this will just re-create - # the same systemd unit with different args echo "Restarting nested VM with attacker cloud-init CD-ROM drive" - nested_start_core_vm_unit "$NESTED_IMAGES_DIR/$(nested_get_current_image_name)" + "$TESTSTOOLS"/nested-state start-vm + nested_wait_for_reboot "${boot_id}" # cloud-init should not actually run, since it was disabled, but in case the # test fails, for a better error, we will wait for cloud-init to report that diff -Nru snapd-2.47.1+20.10.1build1/tests/nested/manual/cloud-init-nocloud-not-vuln/seed1/meta-data snapd-2.48+21.04/tests/nested/manual/cloud-init-nocloud-not-vuln/seed1/meta-data --- snapd-2.47.1+20.10.1build1/tests/nested/manual/cloud-init-nocloud-not-vuln/seed1/meta-data 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/nested/manual/cloud-init-nocloud-not-vuln/seed1/meta-data 1970-01-01 00:00:00.000000000 +0000 @@ -1,2 +0,0 @@ -instance-id: iid-local01 -local-hostname: cloudimg diff -Nru snapd-2.47.1+20.10.1build1/tests/nested/manual/cloud-init-nocloud-not-vuln/seed1/user-data snapd-2.48+21.04/tests/nested/manual/cloud-init-nocloud-not-vuln/seed1/user-data --- snapd-2.47.1+20.10.1build1/tests/nested/manual/cloud-init-nocloud-not-vuln/seed1/user-data 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/nested/manual/cloud-init-nocloud-not-vuln/seed1/user-data 1970-01-01 00:00:00.000000000 +0000 @@ -1,7 +0,0 @@ -#cloud-config -users: - - default - - name: normal-user - sudo: ALL=(ALL) NOPASSWD:ALL - lock_passwd: false - passwd: $6$rounds=4096$ftDwPSSVP0Jq9$4hXIcusbcZMxbSnfv8D/vp/bdzgVAds9qGcFjeBvv1Ths9mLiNPKAxW8/1zOtGLPKsEcorUOzl16hn9jxswDz0 diff -Nru snapd-2.47.1+20.10.1build1/tests/nested/manual/cloud-init-nocloud-not-vuln/seed2/meta-data snapd-2.48+21.04/tests/nested/manual/cloud-init-nocloud-not-vuln/seed2/meta-data --- snapd-2.47.1+20.10.1build1/tests/nested/manual/cloud-init-nocloud-not-vuln/seed2/meta-data 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/nested/manual/cloud-init-nocloud-not-vuln/seed2/meta-data 1970-01-01 00:00:00.000000000 +0000 @@ -1,2 +0,0 @@ -instance-id: iid-local02 -local-hostname: cloudimg diff -Nru snapd-2.47.1+20.10.1build1/tests/nested/manual/cloud-init-nocloud-not-vuln/seed2/user-data snapd-2.48+21.04/tests/nested/manual/cloud-init-nocloud-not-vuln/seed2/user-data --- snapd-2.47.1+20.10.1build1/tests/nested/manual/cloud-init-nocloud-not-vuln/seed2/user-data 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/nested/manual/cloud-init-nocloud-not-vuln/seed2/user-data 1970-01-01 00:00:00.000000000 +0000 @@ -1,7 +0,0 @@ -#cloud-config -users: - - default - - name: attacker-user - sudo: ALL=(ALL) NOPASSWD:ALL - lock_passwd: false - passwd: $6$rounds=4096$ftDwPSSVP0Jq9$4hXIcusbcZMxbSnfv8D/vp/bdzgVAds9qGcFjeBvv1Ths9mLiNPKAxW8/1zOtGLPKsEcorUOzl16hn9jxswDz0 diff -Nru snapd-2.47.1+20.10.1build1/tests/nested/manual/cloud-init-nocloud-not-vuln/task.yaml snapd-2.48+21.04/tests/nested/manual/cloud-init-nocloud-not-vuln/task.yaml --- snapd-2.47.1+20.10.1build1/tests/nested/manual/cloud-init-nocloud-not-vuln/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/nested/manual/cloud-init-nocloud-not-vuln/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -20,25 +20,17 @@ #shellcheck source=tests/lib/nested.sh . "$TESTSLIB/nested.sh" - # build the cloud-init NoCloud cdrom drives - ( - # for first-boot - cd seed1 - genisoimage -output ../seed1.iso -volid cidata -joliet -rock user-data meta-data - ) - - ( - # for second boot - cd seed2 - genisoimage -output ../seed2.iso -volid cidata -joliet -rock user-data meta-data - ) + # first boot - legit NoCloud usage + nested_build_seed_cdrom "$TESTSLIB/cloud-init-seeds/normal-user" seed.iso cidata user-data meta-data - # first boot will use seed1 to create the normal-user in addition to the - # system-user assertion - NESTED_PARAM_CD="-cdrom $(pwd)/seed1.iso" + # second boot - attacker drive + nested_build_seed_cdrom "$TESTSLIB/cloud-init-seeds/attacker-user" seed2.iso cidata user-data meta-data + + "$TESTSTOOLS"/nested-state build-image core - nested_create_core_vm - nested_start_core_vm + # first boot uses seed1 to create the normal-user in addition to the + # system-user assertion + "$TESTSTOOLS"/nested-state create-vm core --param-cdrom "-cdrom $(pwd)/seed.iso" debug: | if [ -f snapd-before-reboot.logs ]; then @@ -57,7 +49,6 @@ #shellcheck source=tests/lib/snaps.sh . "$TESTSLIB/snaps.sh" - # the VM here will use both system-user assertions and cloud-init to create # users on the system, the system-user assertion is for the user1 user, and # the legitimate cloud-init user is the normal-user user @@ -127,24 +118,15 @@ # gracefully shutdown so that we don't have file corruption echo "Gracefully shutting down the nested VM to prepare a simulated attack" - nested_exec "sudo shutdown -h now" || true - nested_wait_for_no_ssh + boot_id="$(nested_get_boot_id)" + "$TESTSTOOLS"/nested-state stop-vm + + # replace the seed.iso with the new attacker iso + mv seed2.iso seed.iso - # now if we reboot and add a cloud-init drive, it will not be imported onto - # the system - nested_force_stop_vm - - # add the second, attacker cloud-init drive as a CD-ROM drive - NESTED_PARAM_CD="-cdrom $(pwd)/seed2.iso" - - # start the unit so that it re-uses the existing drive and doesn't re-setup - # ssh - # TODO: this interaction is a bit awkward because we want to change the - # args to qemu, but those are put into a systemd unit file in /run, which - # is awkward to modify programmatically, so instead this will just re-create - # the same systemd unit with different args echo "Restarting nested VM with attacker cloud-init CD-ROM drive" - nested_start_core_vm_unit "$NESTED_IMAGES_DIR/$(nested_get_current_image_name)" + "$TESTSTOOLS"/nested-state start-vm + nested_wait_for_reboot "${boot_id}" # cloud-init will actually still run because it was not disabled and we # provided a cloud-init drive but importantly, it will not import the drive, diff -Nru snapd-2.47.1+20.10.1build1/tests/nested/manual/core20-early-config/task.yaml snapd-2.48+21.04/tests/nested/manual/core20-early-config/task.yaml --- snapd-2.47.1+20.10.1build1/tests/nested/manual/core20-early-config/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/nested/manual/core20-early-config/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -23,6 +23,9 @@ snap download --basename=pc --channel="20/edge" pc unsquashfs -d pc-gadget pc.snap + # ensure ubuntu-save is there + nested_ensure_ubuntu_save pc-gadget + cat defaults.yaml >> pc-gadget/meta/gadget.yaml mkdir -p pc-gadget/meta/hooks cp install pc-gadget/meta/hooks/ @@ -32,8 +35,8 @@ rm -f "$SNAKEOIL_KEY" "$SNAKEOIL_CERT" - nested_create_core_vm - nested_start_core_vm + "$TESTSTOOLS"/nested-state build-image core + "$TESTSTOOLS"/nested-state create-vm core execute: | #shellcheck source=tests/lib/nested.sh diff -Nru snapd-2.47.1+20.10.1build1/tests/nested/manual/core-early-config/task.yaml snapd-2.48+21.04/tests/nested/manual/core-early-config/task.yaml --- snapd-2.47.1+20.10.1build1/tests/nested/manual/core-early-config/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/nested/manual/core-early-config/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -28,7 +28,7 @@ KERNEL_SNAP=$(ls pc-kernel_*.snap) mv "$KERNEL_SNAP" extra-snaps/ - nested_create_core_vm + "$TESTSTOOLS"/nested-state build-image core # Modify seed to use devmode for pc gadget snap. This is needed for the # install hook to have access to /etc/systemd. Ideally we would use @@ -43,7 +43,7 @@ kpartx -d "$NESTED_IMAGES_DIR/$IMAGE_NAME" rmdir "$tmp" - nested_start_core_vm + "$TESTSTOOLS"/nested-state create-vm core execute: | #shellcheck source=tests/lib/nested.sh diff -Nru snapd-2.47.1+20.10.1build1/tests/nested/manual/grade-signed-above-testkeys-boot/defaults.yaml snapd-2.48+21.04/tests/nested/manual/grade-signed-above-testkeys-boot/defaults.yaml --- snapd-2.47.1+20.10.1build1/tests/nested/manual/grade-signed-above-testkeys-boot/defaults.yaml 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48+21.04/tests/nested/manual/grade-signed-above-testkeys-boot/defaults.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -0,0 +1,6 @@ +defaults: + system: + refresh: + hold: "HOLD-TIME" + journal: + persistent: true diff -Nru snapd-2.47.1+20.10.1build1/tests/nested/manual/grade-signed-above-testkeys-boot/prepare-device snapd-2.48+21.04/tests/nested/manual/grade-signed-above-testkeys-boot/prepare-device --- snapd-2.47.1+20.10.1build1/tests/nested/manual/grade-signed-above-testkeys-boot/prepare-device 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48+21.04/tests/nested/manual/grade-signed-above-testkeys-boot/prepare-device 2020-11-19 16:51:02.000000000 +0000 @@ -0,0 +1,3 @@ +#!/bin/sh +# 10.0.2.2 is the host from a nested VM +snapctl set device-service.url=http://10.0.2.2:11029 diff -Nru snapd-2.47.1+20.10.1build1/tests/nested/manual/grade-signed-above-testkeys-boot/task.yaml snapd-2.48+21.04/tests/nested/manual/grade-signed-above-testkeys-boot/task.yaml --- snapd-2.47.1+20.10.1build1/tests/nested/manual/grade-signed-above-testkeys-boot/task.yaml 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48+21.04/tests/nested/manual/grade-signed-above-testkeys-boot/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -0,0 +1,136 @@ +summary: Test that snapd with testkeys on UC20 can boot a model with grade signed. + +systems: [ubuntu-20.04-64] + +environment: + # use tpm + secure boot to get full disk encryption, this is explicitly needed + # for grade: secured + NESTED_ENABLE_TPM: true + NESTED_ENABLE_SECURE_BOOT: true + + # use snapd from the spread run so that we have testkeys trusted in the snapd + # run + NESTED_BUILD_SNAPD_FROM_CURRENT: true + + # don't use cloud-init, that will be a separate test, we only use sys-user + # assertions to create the user for this test + NESTED_USE_CLOUD_INIT: false + + # sign all the snaps we build for the image with fakestore + NESTED_SIGN_SNAPS_FAKESTORE: true + + # use the testrootorg auto-import assertion + # TODO: commit the Go code used to create this assertion from the json file + NESTED_CUSTOM_AUTO_IMPORT_ASSERTION: $TESTSLIB/assertions/developer1-auto-import.assert + + # two variants, for signed and secured grades + MODEL_GRADE/secured: secured + MODEL_GRADE/signed: signed + + NESTED_CUSTOM_MODEL: $TESTSLIB/assertions/developer1-20-${MODEL_GRADE}.model + NESTED_IMAGE_ID: testkeys-${MODEL_GRADE} + + # for the fake store + NESTED_FAKESTORE_BLOB_DIR: $(pwd)/fake-store-blobdir + NESTED_UBUNTU_IMAGE_SNAPPY_FORCE_SAS_URL: http://localhost:11028 + + # unset this otherwise ubuntu-image complains about overriding the channel for + # a model with grade higher than dangerous when building the image + NESTED_CORE_CHANNEL: "" + +prepare: | + if [ "$TRUST_TEST_KEYS" = "false" ]; then + echo "This test needs test keys to be trusted" + exit + fi + + #shellcheck source=tests/lib/nested.sh + . "$TESTSLIB/nested.sh" + + #shellcheck source=tests/lib/store.sh + . "$TESTSLIB"/store.sh + + # setup the fakestore, but don't use it for our snapd here on the host VM, so + # tear down the staging_store immediately afterwards so that only the SAS is + # running and our snapd is not pointed at it, ubuntu-image is the only thing + # that actually needs to use the fakestore, and we will manually point it at + # the fakestore below using NESTED_UBUNTU_IMAGE_SNAPPY_FORCE_SAS_URL + setup_fake_store "$NESTED_FAKESTORE_BLOB_DIR" + teardown_staging_store + + echo Expose the needed assertions through the fakestore + cp "$TESTSLIB"/assertions/developer1.account "$NESTED_FAKESTORE_BLOB_DIR/asserts" + cp "$TESTSLIB"/assertions/developer1.account-key "$NESTED_FAKESTORE_BLOB_DIR/asserts" + + # modify and repack gadget snap to add a defaults section and use our own + # prepare-device hook to use the fakedevicesvc + mkdir -p "$(nested_get_extra_snaps_path)" + + # Get the snakeoil key and cert for signing gadget assets (shim) + KEY_NAME=$(nested_get_snakeoil_key) + SNAKEOIL_KEY="$PWD/$KEY_NAME.key" + SNAKEOIL_CERT="$PWD/$KEY_NAME.pem" + + snap download --basename=pc --channel="20/edge" pc + unsquashfs -d pc-gadget pc.snap + + # ensure ubuntu-save is there + nested_ensure_ubuntu_save pc-gadget + + # delay all refreshes for a week from now, as otherwise refreshes for our + # snaps (which are asserted by the testrootorg authority-id) may happen, which + # will break things because the signing keys won't match, etc. and + # specifically snap-bootstrap in the kernel snap from the store won't trust + # the seed keys to unlock the encrypted data partition in the initramfs + sed defaults.yaml -e "s/HOLD-TIME/$(date --date="next week" +%Y-%m-%dT%H:%M:%S%:z)/" >> \ + pc-gadget/meta/gadget.yaml + + # TODO: enable this bit when things are ready to use a testkeys signed model + # assertion + # copy the prepare-device hook to use our fakedevicesvc + # mkdir -p pc-gadget/meta/hooks/ + # cp prepare-device pc-gadget/meta/hooks/ + + nested_secboot_sign_gadget pc-gadget "$SNAKEOIL_KEY" "$SNAKEOIL_CERT" + snap pack pc-gadget/ "$(nested_get_extra_snaps_path)" + rm -rf pc-gadget/ + + rm -f "$SNAKEOIL_KEY" "$SNAKEOIL_CERT" + + # TODO: enable when ready + # start fake device svc + # #shellcheck disable=SC2148 + # systemd-run --unit fakedevicesvc fakedevicesvc localhost:11029 + + "$TESTSTOOLS"/nested-state build-image core + "$TESTSTOOLS"/nested-state create-vm core + +restore: | + if [ "$TRUST_TEST_KEYS" = "false" ]; then + echo "This test needs test keys to be trusted" + exit + fi + + # stop fake device svc + # systemctl stop fakedevicesvc + + #shellcheck source=tests/lib/store.sh + . "$TESTSLIB"/store.sh + teardown_fake_store "$NESTED_FAKESTORE_BLOB_DIR" + +execute: | + if [ "$TRUST_TEST_KEYS" = "false" ]; then + echo "This test needs test keys to be trusted" + exit + fi + + #shellcheck source=tests/lib/nested.sh + . "$TESTSLIB/nested.sh" + + # we have the right model from snap model + nested_exec "sudo snap model --verbose" | MATCH "model:\s+testkeys-snapd-${MODEL_GRADE}-core-20-amd64" + nested_exec "sudo snap model --verbose" | MATCH "grade:\s+${MODEL_GRADE}" + + # TODO: check that we got a serial assertion via the fakedevicesvc + # for now we just don't get a serial assertion which is fine for the purposes + # of this test diff -Nru snapd-2.47.1+20.10.1build1/tests/nested/manual/grade-signed-cloud-init-testkeys/defaults.yaml snapd-2.48+21.04/tests/nested/manual/grade-signed-cloud-init-testkeys/defaults.yaml --- snapd-2.47.1+20.10.1build1/tests/nested/manual/grade-signed-cloud-init-testkeys/defaults.yaml 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48+21.04/tests/nested/manual/grade-signed-cloud-init-testkeys/defaults.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -0,0 +1,6 @@ +defaults: + system: + refresh: + hold: "@HOLD-TIME@" + journal: + persistent: true diff -Nru snapd-2.47.1+20.10.1build1/tests/nested/manual/grade-signed-cloud-init-testkeys/prepare-device snapd-2.48+21.04/tests/nested/manual/grade-signed-cloud-init-testkeys/prepare-device --- snapd-2.47.1+20.10.1build1/tests/nested/manual/grade-signed-cloud-init-testkeys/prepare-device 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48+21.04/tests/nested/manual/grade-signed-cloud-init-testkeys/prepare-device 2020-11-19 16:51:02.000000000 +0000 @@ -0,0 +1,3 @@ +#!/bin/sh +# 10.0.2.2 is the host from a nested VM +snapctl set device-service.url=http://10.0.2.2:11029 diff -Nru snapd-2.47.1+20.10.1build1/tests/nested/manual/grade-signed-cloud-init-testkeys/task.yaml snapd-2.48+21.04/tests/nested/manual/grade-signed-cloud-init-testkeys/task.yaml --- snapd-2.47.1+20.10.1build1/tests/nested/manual/grade-signed-cloud-init-testkeys/task.yaml 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48+21.04/tests/nested/manual/grade-signed-cloud-init-testkeys/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -0,0 +1,173 @@ +summary: Test that snapd with testkeys on UC20 can boot a model with grade signed. + +systems: [ubuntu-20.04-64] + +environment: + # use tpm + secure boot to get full disk encryption, this is explicitly needed + # for grade: secured + NESTED_ENABLE_TPM: true + NESTED_ENABLE_SECURE_BOOT: true + + # use snapd from the spread run so that we have testkeys trusted in the snapd + # run + NESTED_BUILD_SNAPD_FROM_CURRENT: true + + # don't use cloud-init to create the user, we manually use cloud-init via + # NESTED_PARAM_CD in the test setup + NESTED_USE_CLOUD_INIT: false + + # sign all the snaps we build for the image with fakestore + NESTED_SIGN_SNAPS_FAKESTORE: true + + # use the testrootorg auto-import assertion + # TODO: commit the Go code used to create this assertion from the json file + NESTED_CUSTOM_AUTO_IMPORT_ASSERTION: $TESTSLIB/assertions/developer1-auto-import.assert + + NESTED_CUSTOM_MODEL: $TESTSLIB/assertions/developer1-20-signed.model + NESTED_IMAGE_ID: cloud-init-signed-testkeys + + # for the fake store + NESTED_FAKESTORE_BLOB_DIR: $(pwd)/fake-store-blobdir + NESTED_UBUNTU_IMAGE_SNAPPY_FORCE_SAS_URL: http://localhost:11028 + + # unset this otherwise ubuntu-image complains about overriding the channel for + # a model with grade higher than dangerous when building the image + NESTED_CORE_CHANNEL: "" + +prepare: | + if [ "$TRUST_TEST_KEYS" = "false" ]; then + echo "This test needs test keys to be trusted" + exit + fi + + #shellcheck source=tests/lib/nested.sh + . "$TESTSLIB/nested.sh" + + #shellcheck source=tests/lib/store.sh + . "$TESTSLIB"/store.sh + + # setup the fakestore, but don't use it for our snapd here on the host VM, so + # tear down the staging_store immediately afterwards so that only the SAS is + # running and our snapd is not pointed at it, ubuntu-image is the only thing + # that actually needs to use the fakestore, and we will manually point it at + # the fakestore below using NESTED_UBUNTU_IMAGE_SNAPPY_FORCE_SAS_URL + setup_fake_store "$NESTED_FAKESTORE_BLOB_DIR" + teardown_staging_store + + echo Expose the needed assertions through the fakestore + cp "$TESTSLIB"/assertions/developer1.account "$NESTED_FAKESTORE_BLOB_DIR/asserts" + cp "$TESTSLIB"/assertions/developer1.account-key "$NESTED_FAKESTORE_BLOB_DIR/asserts" + + # modify and repack gadget snap to add a defaults section and use our own + # prepare-device hook to use the fakedevicesvc + mkdir "$(nested_get_extra_snaps_path)" + + # Get the snakeoil key and cert for signing gadget assets (shim) + KEY_NAME=$(nested_get_snakeoil_key) + SNAKEOIL_KEY="$PWD/$KEY_NAME.key" + SNAKEOIL_CERT="$PWD/$KEY_NAME.pem" + + snap download --basename=pc --channel="20/edge" pc + unsquashfs -d pc-gadget pc.snap + + # ensure ubuntu-save is there + nested_ensure_ubuntu_save pc-gadget + + # delay all refreshes for a week from now, as otherwise refreshes for our + # snaps (which are asserted by the testrootorg authority-id) may happen, which + # will break things because the signing keys won't match, etc. and + # specifically snap-bootstrap in the kernel snap from the store won't trust + # the seed keys to unlock the encrypted data partition in the initramfs + sed defaults.yaml -e "s/@HOLD-TIME@/$(date --date='next week' +%Y-%m-%dT%H:%M:%S%:z)/" >> \ + pc-gadget/meta/gadget.yaml + + # TODO: enable this bit when things are ready to use a testkeys signed model + # assertion + # copy the prepare-device hook to use our fakedevicesvc + # mkdir -p pc-gadget/meta/hooks/ + # cp prepare-device pc-gadget/meta/hooks/ + + nested_secboot_sign_gadget pc-gadget "$SNAKEOIL_KEY" "$SNAKEOIL_CERT" + snap pack pc-gadget/ extra-snaps/ + rm -rf pc-gadget/ + + rm -f "$SNAKEOIL_KEY" "$SNAKEOIL_CERT" + + # TODO: enable this when ready, currently serial assertions requests don't + # work with the fakedevicesvc, needs a little bit of work somewhere + # start fake device svc + # #shellcheck disable=SC2148 + # systemd-run --unit fakedevicesvc fakedevicesvc localhost:11029 + + # first boot - legit NoCloud usage + nested_build_seed_cdrom "$TESTSLIB/cloud-init-seeds/normal-user" seed.iso cidata user-data meta-data + + # second boot - attacker drive + nested_build_seed_cdrom "$TESTSLIB/cloud-init-seeds/attacker-user" seed2.iso cidata user-data meta-data + + "$TESTSTOOLS"/nested-state build-image core + # first boot will use seed1 to create the normal-user in addition to the + # system-user assertion + "$TESTSTOOLS"/nested-state create-vm core --param-cdrom "-cdrom $(pwd)/seed.iso" + +restore: | + if [ "$TRUST_TEST_KEYS" = "false" ]; then + echo "This test needs test keys to be trusted" + exit + fi + + # stop fake device svc + # systemctl stop fakedevicesvc + + #shellcheck source=tests/lib/store.sh + . "$TESTSLIB"/store.sh + teardown_fake_store "$NESTED_FAKESTORE_BLOB_DIR" + +execute: | + if [ "$TRUST_TEST_KEYS" = "false" ]; then + echo "This test needs test keys to be trusted" + exit + fi + + #shellcheck source=tests/lib/nested.sh + . "$TESTSLIB/nested.sh" + + echo "The initial cloud-init user was created" + nested_exec "cat /var/lib/extrausers/passwd" | MATCH normal-user + + echo "And we can run things as the normal user" + nested_exec_as normal-user ubuntu "sudo true" + + # TODO: check that we got a serial assertion via the fakedevicesvc + # for now we just don't get a serial assertion which is fine for the purposes + # of this test + + # TODO: is there a better thing we can wait for here instead? maybe the log + # message from snapd directly via journalctl ? + echo "Waiting for snapd to react to cloud-init" + sleep 60 + + echo "Ensuring that cloud-init got disabled after running" + nested_exec "cloud-init status" | MATCH "status: disabled" + nested_exec "test -f /etc/cloud/cloud-init.disabled" + nested_exec "! test -f /etc/cloud/cloud.cfg.d/zzzz_snapd.cfg" + + # gracefully shutdown so that we don't have file corruption + echo "Gracefully shutting down the nested VM to prepare a simulated attack" + boot_id="$(nested_get_boot_id)" + nested_shutdown + + # replace the seed.iso with the new attacker iso + mv seed2.iso seed.iso + + echo "Restarting nested VM with attacker cloud-init CD-ROM drive" + nested_force_start_vm + nested_wait_for_reboot "${boot_id}" + + echo "The cloud-init attacker user was not created" + nested_exec "cat /var/lib/extrausers/passwd" | NOMATCH attacker-user + + echo "cloud-init is still disabled" + nested_exec "cloud-init status" | MATCH "status: disabled" + nested_exec "test -f /etc/cloud/cloud-init.disabled" + nested_exec "! test -f /etc/cloud/cloud.cfg.d/zzzz_snapd.cfg" diff -Nru snapd-2.47.1+20.10.1build1/tests/nested/manual/minimal-smoke/task.yaml snapd-2.48+21.04/tests/nested/manual/minimal-smoke/task.yaml --- snapd-2.47.1+20.10.1build1/tests/nested/manual/minimal-smoke/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/nested/manual/minimal-smoke/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -6,7 +6,7 @@ #shellcheck source=tests/lib/nested.sh . "$TESTSLIB/nested.sh" nested_fetch_spread - nested_create_core_vm + "$TESTSTOOLS"/nested-state build-image core execute: | #shellcheck source=tests/lib/nested.sh @@ -14,23 +14,22 @@ # see https://docs.ubuntu.com/core/en/#advantages-for-iot for minimum # requirements + MINIMAL_MEM=256 + if nested_is_core_20_system ; then - # TODO:UC20: due to https://bugs.launchpad.net/snapd/+bug/1878541 once fixed - # it should be 256 as well - MINIMAL_MEM=1536 NESTED_SPREAD_SYSTEM=ubuntu-core-20-64 + # TODO:UC20: this should written down in the official docs + MINIMAL_MEM=384 elif nested_is_core_18_system; then NESTED_SPREAD_SYSTEM=ubuntu-core-18-64 - MINIMAL_MEM=256 elif nested_is_core_16_system; then NESTED_SPREAD_SYSTEM=ubuntu-core-16-64 - MINIMAL_MEM=256 else echo "unsupported nested system" exit 1 fi - NESTED_PARAM_MEM="-m $MINIMAL_MEM" nested_start_core_vm + "$TESTSTOOLS"/nested-state create-vm core --param-mem "-m $MINIMAL_MEM" set +x export SPREAD_EXTERNAL_ADDRESS=localhost:8022 diff -Nru snapd-2.47.1+20.10.1build1/tests/nested/manual/preseed/task.yaml snapd-2.48+21.04/tests/nested/manual/preseed/task.yaml --- snapd-2.47.1+20.10.1build1/tests/nested/manual/preseed/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/nested/manual/preseed/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -25,7 +25,7 @@ dpkg -i "$SPREAD_PATH"/../snapd_*.deb # create a VM and mount a cloud image - nested_create_classic_vm + "$TESTSTOOLS"/nested-state build-image classic mkdir -p "$IMAGE_MOUNTPOINT" IMAGE_NAME=$(nested_get_image_name classic) mount_ubuntu_image "$NESTED_IMAGES_DIR/$IMAGE_NAME" "$IMAGE_MOUNTPOINT" @@ -41,6 +41,13 @@ mv snapd-from-deb.snap snapd.snap inject_snap_into_seed "$IMAGE_MOUNTPOINT" snapd + # inject a snap that uses system-usernames into the seed to confirm that works + # as expected + # TODO: replace this snap with a simpler one instead that is smaller, this one + # is 37M, but test-snapd-daemon-user does not have a daemon yet + snap download --edge --basename=test-postgres-system-usernames test-postgres-system-usernames + inject_snap_into_seed "$IMAGE_MOUNTPOINT" test-postgres-system-usernames + # for images that are already preseeded, we need to undo the preseeding there echo "Running preseed --reset for already preseeded cloud images" SNAPD_DEBUG=1 /usr/lib/snapd/snap-preseed --reset "$IMAGE_MOUNTPOINT" @@ -75,21 +82,36 @@ . "$TESTSLIB/preseed.sh" umount_ubuntu_image "$IMAGE_MOUNTPOINT" + "$TESTSTOOLS"/nested-state create-vm classic + #shellcheck source=tests/lib/nested.sh . "$TESTSLIB/nested.sh" - nested_start_classic_vm - + echo "Waiting for firstboot seeding to finish" nested_exec "sudo snap wait system seed.loaded" nested_exec "snap changes" | MATCH "Done .+ Initialize system state" echo "Checking that the system-key after first boot is the same as that from preseeding" - # note, this doesn't actually test the functionality, but acts as a canary: - # the test is run against a vm image with ubuntu release matching that from spread host; - # system-key check can fail if the nested vm image differs too much from the spread host system, - # e.g. when the list of apparmor features differs due to significant kernel update. - nested_exec "cat /var/lib/snapd/system-key" > system-key.real - diff -u -w system-key.real system-key.preseeded + + # TODO: re-enable the system-key check when we are using the same kernel for + # the host VM as the nested VM, currently we are not, and as such there is a + # diff between the preseed apparmor-features and the nested VM actual + # system-key + if [ "$SPREAD_SYSTEM" != "ubuntu-20.04-64" ]; then + # note, this doesn't actually test the functionality, but acts as a canary: + # the test is run against a vm image with ubuntu release matching that from spread host; + # system-key check can fail if the nested vm image differs too much from the spread host system, + # e.g. when the list of apparmor features differs due to significant kernel update. + nested_exec "cat /var/lib/snapd/system-key" > system-key.real + diff -u -w system-key.real system-key.preseeded + + # also check the system-key diff using snap debug seeding + + # we should not have had any system-key difference as per above, so we + # shouldn't output the preseed system-key or the seed-restart-system-key + nested_exec "snap debug seeding" | NOMATCH "preseed-system-key:" + nested_exec "snap debug seeding" | NOMATCH "seed-restart-system-key:" + fi nested_exec "snap debug seeding" | MATCH "preseeded:\s+true" nested_exec "snap debug seeding" | MATCH "seeded:\s+true" @@ -99,14 +121,30 @@ nested_exec "snap debug seeding" | MATCH "image-preseeding:\s+[0-9]+\.[0-9]+s" nested_exec "snap debug seeding" | MATCH "seed-completion:\s+[0-9]+\.[0-9]+s" - # we should not have had any system-key difference as per above, so we - # shouldn't output the preseed system-key or the seed-restart-system-key - nested_exec "snap debug seeding" | NOMATCH "preseed-system-key:" - nested_exec "snap debug seeding" | NOMATCH "seed-restart-system-key:" - echo "Checking that lxd snap is operational" nested_exec "snap list" | not MATCH "broken" nested_exec "snap services" | MATCH "lxd.activate +enabled +inactive" nested_exec "snap services" | MATCH "lxd.daemon +enabled +inactive +socket-activated" nested_exec "sudo lxd init --auto" nested_exec "snap services" | MATCH "+lxd.daemon +enabled +active +socket-activated" + + echo "Checking that the test-postgres-system-usernames snap is operational" + nested_exec "sudo snap start --enable test-postgres-system-usernames.postgres" + # wait for postgres to come online + sleep 10 + nested_exec "snap services" | MATCH "+test-postgres-system-usernames.postgres +enabled +active" + + echo "Checking that mark-seeded task was executed last" + # snap debug timings are sorts by read-time, mark-seeded should be last + nested_exec "sudo snap debug timings 1" | tail -2 | MATCH "Mark system seeded" + # no task should have ready time after mark-seeded + # shellcheck disable=SC2046 + MARK_SEEDED_TIME=$(date -d $(snap change 1 --abs-time | grep "Mark system seeded" | awk '{print $3}') "+%s") + for RT in $(snap change 1 --abs-time | grep Done | awk '{print $3}' ) + do + READY_TIME=$(date -d "$RT" "+%s") + if [ "$READY_TIME" -gt "$MARK_SEEDED_TIME" ]; then + echo "Unexpected ready time greater than mark-seeded ready" + snap change 1 + fi + done diff -Nru snapd-2.47.1+20.10.1build1/tests/nested/manual/refresh-revert-fundamentals/task.yaml snapd-2.48+21.04/tests/nested/manual/refresh-revert-fundamentals/task.yaml --- snapd-2.47.1+20.10.1build1/tests/nested/manual/refresh-revert-fundamentals/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/nested/manual/refresh-revert-fundamentals/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -11,8 +11,11 @@ NESTED_CORE_REFRESH_CHANNEL: edge NESTED_BUILD_SNAPD_FROM_CURRENT: false NESTED_USE_CLOUD_INIT: true - NESTED_ENABLE_SECURE_BOOT: true - NESTED_ENABLE_TPM: true + # TODO:UC20: temporarily disable secure boot and encryption support. The + # location of encryption keys has changed, thus the nested VM will not boot + # until the kernel snap is rebuilt with snapd 2.48. + NESTED_ENABLE_SECURE_BOOT: false + NESTED_ENABLE_TPM: false SNAP/kernel: pc-kernel TRACK/kernel: 20 @@ -39,8 +42,8 @@ exit fi - nested_create_core_vm - nested_start_core_vm + "$TESTSTOOLS"/nested-state build-image core + "$TESTSTOOLS"/nested-state create-vm core restore: | if [ -f skip.test ]; then diff -Nru snapd-2.47.1+20.10.1build1/tests/nested/manual/snapd-refresh-from-old/task.yaml snapd-2.48+21.04/tests/nested/manual/snapd-refresh-from-old/task.yaml --- snapd-2.47.1+20.10.1build1/tests/nested/manual/snapd-refresh-from-old/task.yaml 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48+21.04/tests/nested/manual/snapd-refresh-from-old/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -0,0 +1,71 @@ +summary: Test snapd refresh from a very old snapd snap. +details: | + Test that a refresh from a very old snapd and core18 to a recent one succeeds. + +systems: [ubuntu-18.04-*] + +environment: + # test variants: + # latest_only refreshes to snapd from current source tree only, + # edge_first refreshes to snapd/core18 from edge, then to current snapd. + VARIANT/latest_only: latest_only + VARIANT/edge_first: edge_first + NESTED_BUILD_SNAPD_FROM_CURRENT: false + NESTED_IMAGE_ID: snapd-refresh-from-old + SNAPD_SNAP_URL: https://storage.googleapis.com/spread-snapd-tests/snaps/snapd_2.45.2_5760.snap + CORE18_SNAP_URL: https://storage.googleapis.com/spread-snapd-tests/snaps/core18_20191126_1279.snap + +prepare: | + #shellcheck source=tests/lib/nested.sh + . "$TESTSLIB/nested.sh" + + #shellcheck source=tests/lib/snaps.sh + . "$TESTSLIB"/snaps.sh + + mkdir extra-snaps + wget -P extra-snaps "$SNAPD_SNAP_URL" "$CORE18_SNAP_URL" + + # create core image with old snapd & core18 + "$TESTSTOOLS"/nested-state build-image core + "$TESTSTOOLS"/nested-state create-vm core + + # for refresh in later step of the test + repack_snapd_deb_into_snapd_snap "$PWD" + +execute: | + #shellcheck source=tests/lib/nested.sh + . "$TESTSLIB/nested.sh" + + nested_exec "sudo snap wait system seed.loaded" + nested_exec "snap list" | MATCH "snapd.*5760" + nested_exec "snap list" | MATCH "core18.*1279" + + INITIAL_BOOT_ID=$(nested_get_boot_id) + + if [ "$SPREAD_VARIANT" = "edge_first" ]; then + # refresh to latest snapd from store, this will drop from ssh. + echo "Refreshing snapd and core18 from the store" + nested_exec "sudo snap refresh" || true + + nested_wait_for_reboot "$INITIAL_BOOT_ID" + if nested_exec "snap list snapd" | MATCH "snapd.*5760"; then + echo "unexpected snapd rev 5760" + exit 1 + fi + + # this change is not immediately done and needs a retry + for _ in $(seq 1 10); do + if nested_exec "snap changes" | MATCH ".* Done .* Refresh snaps.*\"snapd\""; then + break + fi + sleep 1 + done + + nested_exec "snap changes" | MATCH ".* Done .* Refresh snaps.*\"snapd\"" + nested_exec "snap changes" | MATCH ".* Done .* Refresh snaps.*\"core18\"" + fi + + echo "Now refresh snapd with current tree" + nested_copy "snapd-from-deb.snap" + nested_exec "sudo snap install snapd-from-deb.snap --dangerous" || true + nested_exec "snap list snapd" | MATCH "snapd .* x1 " diff -Nru snapd-2.47.1+20.10.1build1/tests/nightly/sbuild/task.yaml snapd-2.48+21.04/tests/nightly/sbuild/task.yaml --- snapd-2.47.1+20.10.1build1/tests/nightly/sbuild/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/nightly/sbuild/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -16,9 +16,9 @@ echo "Allow test user to run sbuild" sbuild-adduser test - BUILD_PARAM="" + BUILD_PARAM="--verbose --debug" if [ "$BUILD_MODE" == "any" ]; then - BUILD_PARAM="--arch-any" + BUILD_PARAM="$BUILD_PARAM --arch-any" fi echo "Build mode: $BUILD_MODE" diff -Nru snapd-2.47.1+20.10.1build1/tests/regression/lp-1595444/task.yaml snapd-2.48+21.04/tests/regression/lp-1595444/task.yaml --- snapd-2.47.1+20.10.1build1/tests/regression/lp-1595444/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/regression/lp-1595444/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -10,9 +10,7 @@ prepare: | echo "Having installed the test snap" - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB/snaps.sh" - install_local test-snapd-sh + "$TESTSTOOLS"/snaps-state install-local test-snapd-sh echo "Hanving created a directory not present in the core snap" mkdir -p "/foo" diff -Nru snapd-2.47.1+20.10.1build1/tests/regression/lp-1597839/task.yaml snapd-2.48+21.04/tests/regression/lp-1597839/task.yaml --- snapd-2.47.1+20.10.1build1/tests/regression/lp-1597839/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/regression/lp-1597839/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -9,9 +9,7 @@ prepare: | echo "Having installed the test snap" - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB/snaps.sh" - install_local test-snapd-sh + "$TESTSTOOLS"/snaps-state install-local test-snapd-sh execute: | echo "We can ensure that the /lib/modules/$(uname -r) directory exists" diff -Nru snapd-2.47.1+20.10.1build1/tests/regression/lp-1597842/task.yaml snapd-2.48+21.04/tests/regression/lp-1597842/task.yaml --- snapd-2.47.1+20.10.1build1/tests/regression/lp-1597842/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/regression/lp-1597842/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -10,10 +10,8 @@ prepare: | echo "Having installed the test snap" - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB/snaps.sh" # NOTE: devmode is required because there's no interface for reading /usr/src/.canary - install_local_devmode test-snapd-tools + "$TESTSTOOLS"/snaps-state install-local test-snapd-tools --devmode echo "Having prepared a canary file in /usr/src/.canary" mv /usr/src /usr/src.orig || true mkdir -p /usr/src diff -Nru snapd-2.47.1+20.10.1build1/tests/regression/lp-1606277/task.yaml snapd-2.48+21.04/tests/regression/lp-1606277/task.yaml --- snapd-2.47.1+20.10.1build1/tests/regression/lp-1606277/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/regression/lp-1606277/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -9,10 +9,8 @@ even if the log-observe interface is being used. prepare: | - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB/snaps.sh" echo "Having installed a test snap" - install_local log-observe-consumer + "$TESTSTOOLS"/snaps-state install-local log-observe-consumer echo "And having connected the log-observe interface" snap connect log-observe-consumer:log-observe :log-observe diff -Nru snapd-2.47.1+20.10.1build1/tests/regression/lp-1607796/task.yaml snapd-2.48+21.04/tests/regression/lp-1607796/task.yaml --- snapd-2.47.1+20.10.1build1/tests/regression/lp-1607796/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/regression/lp-1607796/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -2,9 +2,7 @@ prepare: | echo "Having installed a test snap in devmode" - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB/snaps.sh" - install_local_devmode test-snapd-tools + "$TESTSTOOLS"/snaps-state install-local test-snapd-tools --devmode echo "Having added a canary file in /root" echo "test" > /root/canary diff -Nru snapd-2.47.1+20.10.1build1/tests/regression/lp-1615113/task.yaml snapd-2.48+21.04/tests/regression/lp-1615113/task.yaml --- snapd-2.47.1+20.10.1build1/tests/regression/lp-1615113/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/regression/lp-1615113/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -2,10 +2,8 @@ prepare: | echo "Having installed a pair of snaps that share content" - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB/snaps.sh" - install_local test-snapd-content-slot - install_local test-snapd-content-plug + "$TESTSTOOLS"/snaps-state install-local test-snapd-content-slot + "$TESTSTOOLS"/snaps-state install-local test-snapd-content-plug echo "We can connect them together" snap connect test-snapd-content-plug:shared-content-plug test-snapd-content-slot:shared-content-slot diff -Nru snapd-2.47.1+20.10.1build1/tests/regression/lp-1618683/task.yaml snapd-2.48+21.04/tests/regression/lp-1618683/task.yaml --- snapd-2.47.1+20.10.1build1/tests/regression/lp-1618683/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/regression/lp-1618683/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -8,9 +8,7 @@ prepare: | echo "Having installed a test snap in devmode" - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB/snaps.sh" - install_local_devmode test-snapd-tools + "$TESTSTOOLS"/snaps-state install-local test-snapd-tools --devmode if [[ "$SPREAD_SYSTEM" == centos-* ]]; then # RHEL/Centos 7.4+ set this to 0 by default diff -Nru snapd-2.47.1+20.10.1build1/tests/regression/lp-1630479/task.yaml snapd-2.48+21.04/tests/regression/lp-1630479/task.yaml --- snapd-2.47.1+20.10.1build1/tests/regression/lp-1630479/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/regression/lp-1630479/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -9,9 +9,7 @@ prepare: | echo "Having installed a test snap" - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB/snaps.sh" - install_local_devmode test-snapd-tools + "$TESTSTOOLS"/snaps-state install-local test-snapd-tools --devmode execute: | #shellcheck source=tests/lib/dirs.sh diff -Nru snapd-2.47.1+20.10.1build1/tests/regression/lp-1644439/task.yaml snapd-2.48+21.04/tests/regression/lp-1644439/task.yaml --- snapd-2.47.1+20.10.1build1/tests/regression/lp-1644439/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/regression/lp-1644439/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -19,9 +19,7 @@ prepare: | echo "Having installed the test snap in devmode" - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB/snaps.sh" - install_local_devmode test-snapd-tools + "$TESTSTOOLS"/snaps-state install-local test-snapd-tools --devmode debug: | # Kernel version is an important input in understing failures of this test diff -Nru snapd-2.47.1+20.10.1build1/tests/regression/lp-1665004/task.yaml snapd-2.48+21.04/tests/regression/lp-1665004/task.yaml --- snapd-2.47.1+20.10.1build1/tests/regression/lp-1665004/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/regression/lp-1665004/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -7,9 +7,7 @@ the user. prepare: | - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB/snaps.sh" - install_local test-snapd-sh + "$TESTSTOOLS"/snaps-state install-local test-snapd-sh if [ -d /var/lib/snapd/hostfs ]; then rmdir /var/lib/snapd/hostfs; fi diff -Nru snapd-2.47.1+20.10.1build1/tests/regression/lp-1704860/task.yaml snapd-2.48+21.04/tests/regression/lp-1704860/task.yaml --- snapd-2.47.1+20.10.1build1/tests/regression/lp-1704860/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/regression/lp-1704860/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -19,9 +19,7 @@ this is safe to do. execute: | - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh - install_local_classic test-snapd-classic-confinement + "$TESTSTOOLS"/snaps-state install-local test-snapd-classic-confinement --classic # We don't want to see SNAP_DID_REEXEC being set. if snap run --shell test-snapd-classic-confinement ./snap-env-query.sh | grep 'SNAP_DID_REEXEC='; then echo "SNAP_DID_REEXEC environment is set - it should *not* be set ever" diff -Nru snapd-2.47.1+20.10.1build1/tests/regression/lp-1732555/task.yaml snapd-2.48+21.04/tests/regression/lp-1732555/task.yaml --- snapd-2.47.1+20.10.1build1/tests/regression/lp-1732555/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/regression/lp-1732555/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -5,9 +5,7 @@ contained unknown interfaces in either plugs or slots. prepare: | - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB/snaps.sh" - install_local_devmode test-snapd-unknown-interfaces + "$TESTSTOOLS"/snaps-state install-local test-snapd-unknown-interfaces --devmode execute: | echo "Snapd did not die on us" diff -Nru snapd-2.47.1+20.10.1build1/tests/regression/lp-1797556/task.yaml snapd-2.48+21.04/tests/regression/lp-1797556/task.yaml --- snapd-2.47.1+20.10.1build1/tests/regression/lp-1797556/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/regression/lp-1797556/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -4,9 +4,7 @@ user. To avoid hijacking commands and allow sandbox escape, writing to this directory is denied. prepare: | - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB/snaps.sh" - install_local test-snapd-sh + "$TESTSTOOLS"/snaps-state install-local test-snapd-sh su -l -c 'mkdir -p /home/test/bin/' test su -l -c 'mkdir -p /home/test/snap/test-snapd-sh/common/' test su -l -c 'touch /home/test/snap/test-snapd-sh/common/evil-2' test diff -Nru snapd-2.47.1+20.10.1build1/tests/regression/lp-1802581/task.yaml snapd-2.48+21.04/tests/regression/lp-1802581/task.yaml --- snapd-2.47.1+20.10.1build1/tests/regression/lp-1802581/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/regression/lp-1802581/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -32,7 +32,7 @@ # shellcheck source=tests/lib/systemd.sh . "$TESTSLIB/systemd.sh" echo "Create/enable fake gpio" - systemd_create_and_start_persistent_unit fake-gpio "$TESTSLIB/fakegpio/fake-gpio.py" '[Unit]\nBefore=snap.core.interface.gpio-100.service\n[Service]\nType=notify' + systemd_create_and_start_unit fake-gpio "$TESTSLIB/fakegpio/fake-gpio.py" '[Unit]\nBefore=snap.core.interface.gpio-100.service\n[Service]\nType=notify' restore: | # Core image that were created using spread will have a fake "gpio-pin". @@ -45,7 +45,7 @@ # shellcheck source=tests/lib/systemd.sh . "$TESTSLIB/systemd.sh" - system_stop_and_remove_persistent_unit fake-gpio + systemd_stop_and_remove_unit fake-gpio # for good measure, fake-gpio.py does this umount already on exit umount /sys/class/gpio || true @@ -62,9 +62,7 @@ fi echo "Install a snap that uses the gpio consumer" - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh - install_local gpio-consumer + "$TESTSTOOLS"/snaps-state install-local gpio-consumer echo "And connect the gpio pin" snap connect gpio-consumer:gpio :gpio-pin diff -Nru snapd-2.47.1+20.10.1build1/tests/regression/lp-1803535/task.yaml snapd-2.48+21.04/tests/regression/lp-1803535/task.yaml --- snapd-2.47.1+20.10.1build1/tests/regression/lp-1803535/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/regression/lp-1803535/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -1,8 +1,6 @@ summary: regression test for https://bugs.launchpad.net/snapd/+bug/1803535 prepare: | - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB/snaps.sh" - install_local test-snapd-lp-1803535 + "$TESTSTOOLS"/snaps-state install-local test-snapd-lp-1803535 execute: | # If we can construct the layout and execute /bin/true we are fine. test-snapd-lp-1803535.sh -c /bin/true diff -Nru snapd-2.47.1+20.10.1build1/tests/regression/lp-1803542/task.yaml snapd-2.48+21.04/tests/regression/lp-1803542/task.yaml --- snapd-2.47.1+20.10.1build1/tests/regression/lp-1803542/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/regression/lp-1803542/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -5,9 +5,7 @@ VARIANT/shared: shared VARIANT/private: private prepare: | - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB/snaps.sh" - install_local test-snapd-sh + "$TESTSTOOLS"/snaps-state install-local test-snapd-sh # Ensure that every snap namespace is discarded. for name in $(snap list | awk '!/Name/ {print $1}'); do snapd.tool exec snap-discard-ns "$name" diff -Nru snapd-2.47.1+20.10.1build1/tests/regression/lp-1805838/task.yaml snapd-2.48+21.04/tests/regression/lp-1805838/task.yaml --- snapd-2.47.1+20.10.1build1/tests/regression/lp-1805838/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/regression/lp-1805838/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -6,9 +6,7 @@ prepare: | #shellcheck source=tests/lib/dirs.sh . "$TESTSLIB"/dirs.sh - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh - install_local network-consumer + "$TESTSTOOLS"/snaps-state install-local network-consumer version_info="$(sed -n '2 s/# //p' < /var/lib/snapd/seccomp/bpf/snap.network-consumer.network-consumer.src)" test -n "$version_info" diff -Nru snapd-2.47.1+20.10.1build1/tests/regression/lp-1813963/task.yaml snapd-2.48+21.04/tests/regression/lp-1813963/task.yaml --- snapd-2.47.1+20.10.1build1/tests/regression/lp-1813963/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/regression/lp-1813963/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -5,12 +5,10 @@ backends: [-external] prepare: | - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh #shellcheck source=tests/lib/dirs.sh . "$TESTSLIB"/dirs.sh # Install the versatile test snap. - install_local test-snapd-sh + "$TESTSTOOLS"/snaps-state install-local test-snapd-sh # Discard the mount namespace in case the snap has any hooks. We rely on # the mount namespace to be absent and about to be constructed in the test # below. @@ -21,7 +19,7 @@ # without anything special about it. # # For the purpose of the test we want the service to be off. - install_local test-snapd-simple-service + "$TESTSTOOLS"/snaps-state install-local test-snapd-simple-service systemctl stop snap.test-snapd-simple-service.test-snapd-simple-service.service snapd.tool exec snap-discard-ns test-snapd-simple-service diff -Nru snapd-2.47.1+20.10.1build1/tests/regression/lp-1815722/task.yaml snapd-2.48+21.04/tests/regression/lp-1815722/task.yaml --- snapd-2.47.1+20.10.1build1/tests/regression/lp-1815722/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/regression/lp-1815722/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -1,9 +1,6 @@ summary: regression test for https://bugs.launchpad.net/snapd/+bug/1815722 execute: | - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB/snaps.sh" - - install_local snap-hooks-bad-install || true + "$TESTSTOOLS"/snaps-state install-local snap-hooks-bad-install || true test ! -e /var/lib/snapd/ns/snap-hooks-bad-install.mnt test ! -e /var/lib/snapd/ns/snap.snap-hooks-bad-install.fstab diff -Nru snapd-2.47.1+20.10.1build1/tests/regression/lp-1884849/task.yaml snapd-2.48+21.04/tests/regression/lp-1884849/task.yaml --- snapd-2.47.1+20.10.1build1/tests/regression/lp-1884849/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/regression/lp-1884849/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -4,10 +4,8 @@ unmounting it again does not result in an error. systems: [ubuntu-16.04-64] prepare: | - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh # Having test-snapd-desktop installed, with the desktop plug connected - install_local test-snapd-desktop + "$TESTSTOOLS"/snaps-state install-local test-snapd-desktop snap install test-snapd-desktop snap connect test-snapd-desktop:desktop # Having constructed the mount namespace as the test user diff -Nru snapd-2.47.1+20.10.1build1/tests/regression/lp-1886786/task.yaml snapd-2.48+21.04/tests/regression/lp-1886786/task.yaml --- snapd-2.47.1+20.10.1build1/tests/regression/lp-1886786/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/regression/lp-1886786/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -1,9 +1,7 @@ summary: regression test for LP:#1886786 prepare: | - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB"/snaps.sh - install_local test-snapd-app-with-test-name + "$TESTSTOOLS"/snaps-state install-local test-snapd-app-with-test-name execute: | echo "running the command with .test suffix does not panic" diff -Nru snapd-2.47.1+20.10.1build1/tests/regression/lp-1899664/task.yaml snapd-2.48+21.04/tests/regression/lp-1899664/task.yaml --- snapd-2.47.1+20.10.1build1/tests/regression/lp-1899664/task.yaml 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48+21.04/tests/regression/lp-1899664/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -0,0 +1,17 @@ +summary: Test that read-only filesystem on /etc/dbus-1/session.d doesn't prevent snapd refresh. +systems: [ubuntu-core-18-64] + +prepare: | + mount -t tmpfs tmptfs -o ro /etc/dbus-1/session.d + +execute: | + # we are running current snapd build, re-install it to trigger core18 + # wrappers to be recreated. + snap install --dangerous /var/lib/snapd/seed/snaps/snapd_x1.snap + journalctl -u snapd | MATCH "appears to be read-only, could not write snapd dbus config files" + +restore: | + umount /etc/dbus-1/session.d + + # restore snapd installed originally + snap revert snapd diff -Nru snapd-2.47.1+20.10.1build1/tests/smoke/remove/task.yaml snapd-2.48+21.04/tests/smoke/remove/task.yaml --- snapd-2.47.1+20.10.1build1/tests/smoke/remove/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/smoke/remove/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -1,9 +1,7 @@ summary: Check that install/remove works execute: | - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB/snaps.sh" - install_local test-snapd-sh + "$TESTSTOOLS"/snaps-state install-local test-snapd-sh #shellcheck source=tests/lib/dirs.sh . "$TESTSLIB/dirs.sh" diff -Nru snapd-2.47.1+20.10.1build1/tests/smoke/sandbox/task.yaml snapd-2.48+21.04/tests/smoke/sandbox/task.yaml --- snapd-2.47.1+20.10.1build1/tests/smoke/sandbox/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/smoke/sandbox/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -16,9 +16,7 @@ exit 0 fi - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB/snaps.sh" - install_local test-snapd-sandbox + "$TESTSTOOLS"/snaps-state install-local test-snapd-sandbox # home is not auto-connected on core if grep -q ID=ubuntu-core /etc/os-release; then diff -Nru snapd-2.47.1+20.10.1build1/tests/unit/spread-shellcheck/can-fail snapd-2.48+21.04/tests/unit/spread-shellcheck/can-fail --- snapd-2.47.1+20.10.1build1/tests/unit/spread-shellcheck/can-fail 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/unit/spread-shellcheck/can-fail 1970-01-01 00:00:00.000000000 +0000 @@ -1 +0,0 @@ - diff -Nru snapd-2.47.1+20.10.1build1/tests/unit/spread-shellcheck/task.yaml snapd-2.48+21.04/tests/unit/spread-shellcheck/task.yaml --- snapd-2.47.1+20.10.1build1/tests/unit/spread-shellcheck/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/unit/spread-shellcheck/task.yaml 1970-01-01 00:00:00.000000000 +0000 @@ -1,15 +0,0 @@ -summary: Verify spread task scripts with shellcheck - -# need a recent shellcheck version -systems: [ubuntu-18.04-64] - -prepare: | - # need to install shellcheck in devmode, the tests are run by 'root', the - # source code is under /home/gopath, all file accesses to the source code - # will end up getting DENIED even though shellcheck uses home interface - snap install --beta --devmode shellcheck - -execute: | - testdir=$PWD - cd "$SPREAD_PATH" || exit 1 - CAN_FAIL="$testdir/can-fail" ./spread-shellcheck spread.yaml tests diff -Nru snapd-2.47.1+20.10.1build1/tests/upgrade/basic/task.yaml snapd-2.48+21.04/tests/upgrade/basic/task.yaml --- snapd-2.47.1+20.10.1build1/tests/upgrade/basic/task.yaml 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/tests/upgrade/basic/task.yaml 2020-11-19 16:51:02.000000000 +0000 @@ -76,7 +76,7 @@ do_classic=no if is_classic_confinement_supported ; then - install_local_classic test-snapd-classic-confinement + "$TESTSTOOLS"/snaps-state install-local test-snapd-classic-confinement --classic do_classic=yes # Preserve the state across reboots if necessary touch do-classic diff -Nru snapd-2.47.1+20.10.1build1/testutil/containschecker_test.go snapd-2.48+21.04/testutil/containschecker_test.go --- snapd-2.47.1+20.10.1build1/testutil/containschecker_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/testutil/containschecker_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -137,7 +137,7 @@ } func (*containsCheckerSuite) TestContainsUncomparableType(c *check.C) { - if runtime.Compiler != "go" { + if runtime.Compiler != "gc" { c.Skip("this test only works on go (not gccgo)") } diff -Nru snapd-2.47.1+20.10.1build1/update-pot snapd-2.48+21.04/update-pot --- snapd-2.47.1+20.10.1build1/update-pot 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/update-pot 2020-11-19 16:51:02.000000000 +0000 @@ -1,7 +1,7 @@ #!/bin/sh # -*- Mode: sh; indent-tabs-mode: t -*- -set -e +set -eu # In LP#1758684 we got reports that the pot file generation # is broken. To get to the bottom of this add checks here @@ -25,24 +25,24 @@ HERE="$(readlink -f "$(dirname "$0")")" OUTPUT="$HERE/po/snappy.pot" -if [ -n "$1" ]; then +if [ -n "${1:-}" ]; then OUTPUT="$1" fi # ensure we have our xgettext-go go install github.com/snapcore/snapd/i18n/xgettext-go +tmpdir="$(mktemp -d)" +trap 'rm -rf "$tmpdir"' EXIT + # exclude vendor and _build subdir -I18N_FILES="$(mktemp -d)/i18n.files" -find "$HERE" -type d \( -name "vendor" -o -name "_build" \) -prune -o -name "*.go" -type f -print > "$I18N_FILES" -# shellcheck disable=SC2064 -trap "rm -rf $(dirname "$I18N_FILES")" EXIT +find "$HERE" -type d \( -name "vendor" -o -name "_build" -o -name ".git" \) -prune -o -name "*.go" -type f -printf "%P\n" > "$tmpdir/go.files" "${GOPATH%%:*}/bin/xgettext-go" \ - -f "$I18N_FILES" \ + -f "$tmpdir/go.files" \ + -D "$HERE" \ -o "$OUTPUT" \ --add-comments-tag=TRANSLATORS: \ - --no-location \ --sort-output \ --package-name=snappy\ --msgid-bugs-address=snappy-devel@lists.ubuntu.com \ @@ -54,12 +54,28 @@ sed -i 's/charset=CHARSET/charset=UTF-8/' "$OUTPUT" +find "$HERE" -path "$HERE/data/desktop/*.desktop.in" -type f -printf "%P\n" > "$tmpdir/desktop.files" +# we need the || true because Ubuntu 14.04's xgettext does not support +# extracting from desktop files. +xgettext \ + -f "$tmpdir/desktop.files" \ + -D "$HERE" \ + -o "$OUTPUT" \ + --language=Desktop \ + --sort-output \ + --package-name=snappy \ + --msgid-bugs-address=snappy-devel@lists.ubuntu.com \ + --join-existing || true + +find "$HERE" -path "$HERE/data/polkit/*.policy" -type f -printf "%P\n" > "$tmpdir/polkit.files" # we need the || true because of # https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=891347 -xgettext "$HERE"/data/polkit/*.policy \ +xgettext \ + -f "$tmpdir/polkit.files" \ + -D "$HERE" \ -o "$OUTPUT" \ --its="$HERE/po/its/polkit.its" \ - --no-location \ + --sort-output \ --package-name=snappy \ --msgid-bugs-address=snappy-devel@lists.ubuntu.com \ --join-existing || true diff -Nru snapd-2.47.1+20.10.1build1/usersession/agent/export_test.go snapd-2.48+21.04/usersession/agent/export_test.go --- snapd-2.47.1+20.10.1build1/usersession/agent/export_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/usersession/agent/export_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -25,8 +25,9 @@ ) var ( - SessionInfoCmd = sessionInfoCmd - ServiceControlCmd = serviceControlCmd + SessionInfoCmd = sessionInfoCmd + ServiceControlCmd = serviceControlCmd + PendingRefreshNotificationCmd = pendingRefreshNotificationCmd ) func MockStopTimeouts(stop, kill time.Duration) (restore func()) { diff -Nru snapd-2.47.1+20.10.1build1/usersession/agent/rest_api.go snapd-2.48+21.04/usersession/agent/rest_api.go --- snapd-2.47.1+20.10.1build1/usersession/agent/rest_api.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/usersession/agent/rest_api.go 2020-11-19 16:51:02.000000000 +0000 @@ -21,13 +21,20 @@ import ( "encoding/json" + "fmt" "mime" "net/http" + "path/filepath" "strings" "sync" "time" + "github.com/mvo5/goconfigparser" + + "github.com/snapcore/snapd/dbusutil" + "github.com/snapcore/snapd/desktop/notification" "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/i18n" "github.com/snapcore/snapd/systemd" "github.com/snapcore/snapd/timeout" ) @@ -36,6 +43,7 @@ rootCmd, sessionInfoCmd, serviceControlCmd, + pendingRefreshNotificationCmd, } var ( @@ -53,6 +61,11 @@ Path: "/v1/service-control", POST: postServiceControl, } + + pendingRefreshNotificationCmd = &Command{ + Path: "/v1/notifications/pending-refresh", + POST: postPendingRefreshNotification, + } ) func sessionInfo(c *Command, r *http.Request) Response { @@ -195,6 +208,110 @@ // Prevent multiple systemd actions from being carried out simultaneously systemdLock.Lock() defer systemdLock.Unlock() - sysd := systemd.New(dirs.GlobalRootDir, systemd.UserMode, dummyReporter{}) + sysd := systemd.New(systemd.UserMode, dummyReporter{}) return impl(&inst, sysd) } + +func postPendingRefreshNotification(c *Command, r *http.Request) Response { + contentType := r.Header.Get("Content-Type") + mediaType, params, err := mime.ParseMediaType(contentType) + if err != nil { + return BadRequest("cannot parse content type: %v", err) + } + + if mediaType != "application/json" { + return BadRequest("unknown content type: %s", contentType) + } + + charset := strings.ToUpper(params["charset"]) + if charset != "" && charset != "UTF-8" { + return BadRequest("unknown charset in content type: %s", contentType) + } + + decoder := json.NewDecoder(r.Body) + + // pendingSnapRefreshInfo holds information about pending snap refresh provided by snapd. + type pendingSnapRefreshInfo struct { + InstanceName string `json:"instance-name"` + TimeRemaining time.Duration `json:"time-remaining,omitempty"` + BusyAppName string `json:"busy-app-name,omitempty"` + BusyAppDesktopEntry string `json:"busy-app-desktop-entry,omitempty"` + } + var refreshInfo pendingSnapRefreshInfo + if err := decoder.Decode(&refreshInfo); err != nil { + return BadRequest("cannot decode request body into pending snap refresh info: %v", err) + } + + // TODO: use c.a.bus once https://github.com/snapcore/snapd/pull/9497 is merged. + conn, err := dbusutil.SessionBus() + // Note that since the connection is shared, we are not closing it. + if err != nil { + return SyncResponse(&resp{ + Type: ResponseTypeError, + Status: 500, + Result: &errorResult{ + Message: fmt.Sprintf("cannot connect to the session bus: %v", err), + }, + }) + } + + // TODO: support desktop-specific notification APIs if they provide a better + // experience. For example, the GNOME notification API. + notifySrv := notification.New(conn) + + // TODO: this message needs to be crafted better as it's the only thing guaranteed to be delivered. + summary := fmt.Sprintf(i18n.G("Pending update of %q snap"), refreshInfo.InstanceName) + var urgencyLevel notification.Urgency + var body, icon string + var hints []notification.Hint + + plzClose := i18n.G("Close the app to avoid disruptions") + if daysLeft := int(refreshInfo.TimeRemaining.Truncate(time.Hour).Hours() / 24); daysLeft > 0 { + urgencyLevel = notification.LowUrgency + body = fmt.Sprintf("%s (%s)", plzClose, fmt.Sprintf( + i18n.NG("%d day left", "%d days left", daysLeft), daysLeft)) + } else if hoursLeft := int(refreshInfo.TimeRemaining.Truncate(time.Minute).Minutes() / 60); hoursLeft > 0 { + urgencyLevel = notification.NormalUrgency + body = fmt.Sprintf("%s (%s)", plzClose, fmt.Sprintf( + i18n.NG("%d hour left", "%d hours left", hoursLeft), hoursLeft)) + } else if minutesLeft := int(refreshInfo.TimeRemaining.Truncate(time.Minute).Minutes()); minutesLeft > 0 { + urgencyLevel = notification.CriticalUrgency + body = fmt.Sprintf("%s (%s)", plzClose, fmt.Sprintf( + i18n.NG("%d minute left", "%d minutes left", minutesLeft), minutesLeft)) + } else { + summary = fmt.Sprintf(i18n.G("Snap %q is refreshing now!"), refreshInfo.InstanceName) + urgencyLevel = notification.CriticalUrgency + } + hints = append(hints, notification.WithUrgency(urgencyLevel)) + // The notification is provided by snapd session agent. + hints = append(hints, notification.WithDesktopEntry("io.snapcraft.SessionAgent")) + // But if we have a desktop file of the busy application, use that apps's icon. + if refreshInfo.BusyAppDesktopEntry != "" { + parser := goconfigparser.New() + desktopFilePath := filepath.Join(dirs.SnapDesktopFilesDir, refreshInfo.BusyAppDesktopEntry+".desktop") + if err := parser.ReadFile(desktopFilePath); err == nil { + icon, _ = parser.Get("Desktop Entry", "Icon") + } + } + + msg := ¬ification.Message{ + AppName: refreshInfo.BusyAppName, + Summary: summary, + Icon: icon, + Body: body, + Hints: hints, + } + + // TODO: silently ignore error returned when the notification server does not exist. + // TODO: track returned notification ID and respond to actions, if supported. + if _, err := notifySrv.SendNotification(msg); err != nil { + return SyncResponse(&resp{ + Type: ResponseTypeError, + Status: 500, + Result: &errorResult{ + Message: fmt.Sprintf("cannot send notification message: %v", err), + }, + }) + } + return SyncResponse(nil) +} diff -Nru snapd-2.47.1+20.10.1build1/usersession/agent/rest_api_test.go snapd-2.48+21.04/usersession/agent/rest_api_test.go --- snapd-2.47.1+20.10.1build1/usersession/agent/rest_api_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/usersession/agent/rest_api_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -23,17 +23,24 @@ "bytes" "encoding/json" "fmt" + "io/ioutil" "net/http" "net/http/httptest" "os" + "path/filepath" "time" . "gopkg.in/check.v1" + "github.com/godbus/dbus" + "github.com/snapcore/snapd/dbusutil" + "github.com/snapcore/snapd/dbusutil/dbustest" + "github.com/snapcore/snapd/desktop/notification" "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/systemd" "github.com/snapcore/snapd/testutil" "github.com/snapcore/snapd/usersession/agent" + "github.com/snapcore/snapd/usersession/client" ) type restSuite struct { @@ -379,3 +386,288 @@ {"--user", "stop", "snap.bar.service"}, }) } + +func (s *restSuite) TestPostPendingRefreshNotificationMalformedContentType(c *C) { + _, err := agent.New() + c.Assert(err, IsNil) + req, err := http.NewRequest("POST", "/v1/notifications/pending-refresh", bytes.NewBufferString("")) + req.Header.Set("Content-Type", "text/plain/joke") + c.Assert(err, IsNil) + rec := httptest.NewRecorder() + agent.PendingRefreshNotificationCmd.POST(agent.PendingRefreshNotificationCmd, req).ServeHTTP(rec, req) + c.Check(rec.Code, Equals, 400) + c.Check(rec.HeaderMap.Get("Content-Type"), Equals, "application/json") + + var rsp resp + c.Assert(json.Unmarshal(rec.Body.Bytes(), &rsp), IsNil) + c.Check(rsp.Type, Equals, agent.ResponseTypeError) + c.Check(rsp.Result, DeepEquals, map[string]interface{}{"message": "cannot parse content type: mime: unexpected content after media subtype"}) +} + +func (s *restSuite) TestPostPendingRefreshNotificationUnsupportedContentType(c *C) { + _, err := agent.New() + c.Assert(err, IsNil) + req, err := http.NewRequest("POST", "/v1/notifications/pending-refresh", bytes.NewBufferString("")) + req.Header.Set("Content-Type", "text/plain") + c.Assert(err, IsNil) + rec := httptest.NewRecorder() + agent.PendingRefreshNotificationCmd.POST(agent.PendingRefreshNotificationCmd, req).ServeHTTP(rec, req) + c.Check(rec.Code, Equals, 400) + c.Check(rec.HeaderMap.Get("Content-Type"), Equals, "application/json") + + var rsp resp + c.Assert(json.Unmarshal(rec.Body.Bytes(), &rsp), IsNil) + c.Check(rsp.Type, Equals, agent.ResponseTypeError) + c.Check(rsp.Result, DeepEquals, map[string]interface{}{"message": "unknown content type: text/plain"}) +} + +func (s *restSuite) TestPostPendingRefreshNotificationUnsupportedContentEncoding(c *C) { + _, err := agent.New() + c.Assert(err, IsNil) + req, err := http.NewRequest("POST", "/v1/notifications/pending-refresh", bytes.NewBufferString("")) + req.Header.Set("Content-Type", "application/json; charset=EBCDIC") + c.Assert(err, IsNil) + rec := httptest.NewRecorder() + agent.PendingRefreshNotificationCmd.POST(agent.PendingRefreshNotificationCmd, req).ServeHTTP(rec, req) + c.Check(rec.Code, Equals, 400) + c.Check(rec.HeaderMap.Get("Content-Type"), Equals, "application/json") + + var rsp resp + c.Assert(json.Unmarshal(rec.Body.Bytes(), &rsp), IsNil) + c.Check(rsp.Type, Equals, agent.ResponseTypeError) + c.Check(rsp.Result, DeepEquals, map[string]interface{}{"message": "unknown charset in content type: application/json; charset=EBCDIC"}) +} + +func (s *restSuite) TestPostPendingRefreshNotificationMalformedRequestBody(c *C) { + _, err := agent.New() + c.Assert(err, IsNil) + req, err := http.NewRequest("POST", "/v1/notifications/pending-refresh", + bytes.NewBufferString(`{"instance-name":syntaxerror}`)) + req.Header.Set("Content-Type", "application/json") + c.Assert(err, IsNil) + rec := httptest.NewRecorder() + agent.PendingRefreshNotificationCmd.POST(agent.PendingRefreshNotificationCmd, req).ServeHTTP(rec, req) + c.Check(rec.Code, Equals, 400) + c.Check(rec.HeaderMap.Get("Content-Type"), Equals, "application/json") + + var rsp resp + c.Assert(json.Unmarshal(rec.Body.Bytes(), &rsp), IsNil) + c.Check(rsp.Type, Equals, agent.ResponseTypeError) + c.Check(rsp.Result, DeepEquals, map[string]interface{}{"message": "cannot decode request body into pending snap refresh info: invalid character 's' looking for beginning of value"}) +} + +func (s *restSuite) TestPostPendingRefreshNotificationNoSessionBus(c *C) { + noDBus := func() (*dbus.Conn, error) { + return nil, fmt.Errorf("cannot find bus") + } + restore := dbusutil.MockConnections(noDBus, noDBus) + defer restore() + + _, err := agent.New() + c.Assert(err, IsNil) + req, err := http.NewRequest("POST", "/v1/notifications/pending-refresh", + bytes.NewBufferString(`{"instance-name":"pkg"}`)) + req.Header.Set("Content-Type", "application/json") + c.Assert(err, IsNil) + rec := httptest.NewRecorder() + agent.PendingRefreshNotificationCmd.POST(agent.PendingRefreshNotificationCmd, req).ServeHTTP(rec, req) + c.Check(rec.Code, Equals, 500) + c.Check(rec.HeaderMap.Get("Content-Type"), Equals, "application/json") + + var rsp resp + c.Assert(json.Unmarshal(rec.Body.Bytes(), &rsp), IsNil) + c.Check(rsp.Type, Equals, agent.ResponseTypeError) + c.Check(rsp.Result, DeepEquals, map[string]interface{}{"message": "cannot connect to the session bus: cannot find bus"}) +} + +func (s *restSuite) testPostPendingRefreshNotificationBody(c *C, refreshInfo *client.PendingSnapRefreshInfo, checkMsg func(c *C, msg *dbus.Message)) { + conn, err := dbustest.Connection(func(msg *dbus.Message, n int) ([]*dbus.Message, error) { + if checkMsg != nil { + checkMsg(c, msg) + } + responseSig := dbus.SignatureOf(uint32(0)) + response := &dbus.Message{ + Type: dbus.TypeMethodReply, + Headers: map[dbus.HeaderField]dbus.Variant{ + dbus.FieldReplySerial: dbus.MakeVariant(msg.Serial()), + dbus.FieldSender: dbus.MakeVariant(":1"), // This does not matter. + // dbus.FieldDestination is provided automatically by DBus test helper. + dbus.FieldSignature: dbus.MakeVariant(responseSig), + }, + Body: []interface{}{uint32(7)}, // NotificationID (ignored for now) + } + return []*dbus.Message{response}, nil + }) + c.Assert(err, IsNil) + restore := dbusutil.MockOnlySessionBusAvailable(conn) + defer restore() + + _, err = agent.New() + c.Assert(err, IsNil) + + reqBody, err := json.Marshal(refreshInfo) + c.Assert(err, IsNil) + req, err := http.NewRequest("POST", "/v1/notifications/pending-refresh", bytes.NewBuffer(reqBody)) + req.Header.Set("Content-Type", "application/json") + c.Assert(err, IsNil) + rec := httptest.NewRecorder() + agent.PendingRefreshNotificationCmd.POST(agent.PendingRefreshNotificationCmd, req).ServeHTTP(rec, req) + c.Check(rec.Code, Equals, 200) + c.Check(rec.HeaderMap.Get("Content-Type"), Equals, "application/json") + + var rsp resp + c.Assert(json.Unmarshal(rec.Body.Bytes(), &rsp), IsNil) + c.Check(rsp.Type, Equals, agent.ResponseTypeSync) + c.Check(rsp.Result, IsNil) +} + +func (s *restSuite) TestPostPendingRefreshNotificationHappeningNow(c *C) { + refreshInfo := &client.PendingSnapRefreshInfo{InstanceName: "pkg"} + s.testPostPendingRefreshNotificationBody(c, refreshInfo, func(c *C, msg *dbus.Message) { + c.Check(msg.Body[0], Equals, "") + c.Check(msg.Body[1], Equals, uint32(0)) + c.Check(msg.Body[2], Equals, "") + c.Check(msg.Body[3], Equals, `Snap "pkg" is refreshing now!`) + c.Check(msg.Body[4], Equals, "") + c.Check(msg.Body[5], HasLen, 0) + c.Check(msg.Body[6], DeepEquals, map[string]dbus.Variant{ + "urgency": dbus.MakeVariant(byte(notification.CriticalUrgency)), + "desktop-entry": dbus.MakeVariant("io.snapcraft.SessionAgent"), + }) + c.Check(msg.Body[7], Equals, int32(0)) + }) +} + +func (s *restSuite) TestPostPendingRefreshNotificationFewDays(c *C) { + refreshInfo := &client.PendingSnapRefreshInfo{ + InstanceName: "pkg", + TimeRemaining: time.Hour * 72, + } + s.testPostPendingRefreshNotificationBody(c, refreshInfo, func(c *C, msg *dbus.Message) { + c.Check(msg.Body[3], Equals, `Pending update of "pkg" snap`) + c.Check(msg.Body[4], Equals, "Close the app to avoid disruptions (3 days left)") + c.Check(msg.Body[6], DeepEquals, map[string]dbus.Variant{ + "urgency": dbus.MakeVariant(byte(notification.LowUrgency)), + "desktop-entry": dbus.MakeVariant("io.snapcraft.SessionAgent"), + }) + c.Check(msg.Body[7], Equals, int32(0)) + }) +} + +func (s *restSuite) TestPostPendingRefreshNotificationFewHours(c *C) { + refreshInfo := &client.PendingSnapRefreshInfo{ + InstanceName: "pkg", + TimeRemaining: time.Hour * 7, + } + s.testPostPendingRefreshNotificationBody(c, refreshInfo, func(c *C, msg *dbus.Message) { + // boring stuff is checked above + c.Check(msg.Body[3], Equals, `Pending update of "pkg" snap`) + c.Check(msg.Body[4], Equals, "Close the app to avoid disruptions (7 hours left)") + c.Check(msg.Body[6], DeepEquals, map[string]dbus.Variant{ + "urgency": dbus.MakeVariant(byte(notification.NormalUrgency)), + "desktop-entry": dbus.MakeVariant("io.snapcraft.SessionAgent"), + }) + }) +} + +func (s *restSuite) TestPostPendingRefreshNotificationFewMinutes(c *C) { + refreshInfo := &client.PendingSnapRefreshInfo{ + InstanceName: "pkg", + TimeRemaining: time.Minute * 15, + } + s.testPostPendingRefreshNotificationBody(c, refreshInfo, func(c *C, msg *dbus.Message) { + // boring stuff is checked above + c.Check(msg.Body[3], Equals, `Pending update of "pkg" snap`) + c.Check(msg.Body[4], Equals, "Close the app to avoid disruptions (15 minutes left)") + c.Check(msg.Body[6], DeepEquals, map[string]dbus.Variant{ + "urgency": dbus.MakeVariant(byte(notification.CriticalUrgency)), + "desktop-entry": dbus.MakeVariant("io.snapcraft.SessionAgent"), + }) + }) +} + +func (s *restSuite) TestPostPendingRefreshNotificationBusyAppDesktopFile(c *C) { + refreshInfo := &client.PendingSnapRefreshInfo{ + InstanceName: "pkg", + BusyAppName: "app", + BusyAppDesktopEntry: "pkg_app", + } + err := os.MkdirAll(dirs.SnapDesktopFilesDir, 0755) + c.Assert(err, IsNil) + desktopFilePath := filepath.Join(dirs.SnapDesktopFilesDir, "pkg_app.desktop") + err = ioutil.WriteFile(desktopFilePath, []byte(` +[Desktop Entry] +Icon=app.png + `), 0644) + c.Assert(err, IsNil) + + s.testPostPendingRefreshNotificationBody(c, refreshInfo, func(c *C, msg *dbus.Message) { + // boring stuff is checked above + c.Check(msg.Body[2], Equals, "app.png") + c.Check(msg.Body[6], DeepEquals, map[string]dbus.Variant{ + "urgency": dbus.MakeVariant(byte(notification.CriticalUrgency)), + "desktop-entry": dbus.MakeVariant("io.snapcraft.SessionAgent"), + }) + }) +} + +func (s *restSuite) TestPostPendingRefreshNotificationBusyAppMalformedDesktopFile(c *C) { + refreshInfo := &client.PendingSnapRefreshInfo{ + InstanceName: "pkg", + BusyAppName: "app", + BusyAppDesktopEntry: "pkg_app", + } + err := os.MkdirAll(dirs.SnapDesktopFilesDir, 0755) + c.Assert(err, IsNil) + desktopFilePath := filepath.Join(dirs.SnapDesktopFilesDir, "pkg_app.desktop") + err = ioutil.WriteFile(desktopFilePath, []byte(`garbage!`), 0644) + c.Assert(err, IsNil) + + s.testPostPendingRefreshNotificationBody(c, refreshInfo, func(c *C, msg *dbus.Message) { + // boring stuff is checked above + c.Check(msg.Body[2], Equals, "") // Icon is not provided + c.Check(msg.Body[6], DeepEquals, map[string]dbus.Variant{ + "desktop-entry": dbus.MakeVariant("io.snapcraft.SessionAgent"), + "urgency": dbus.MakeVariant(byte(notification.CriticalUrgency)), + }) + }) +} + +func (s *restSuite) TestPostPendingRefreshNotificationNoNotificationServer(c *C) { + conn, err := dbustest.Connection(func(msg *dbus.Message, n int) ([]*dbus.Message, error) { + response := &dbus.Message{ + Type: dbus.TypeError, + Headers: map[dbus.HeaderField]dbus.Variant{ + dbus.FieldReplySerial: dbus.MakeVariant(msg.Serial()), + dbus.FieldSender: dbus.MakeVariant(":1"), // This does not matter. + // dbus.FieldDestination is provided automatically by DBus test helper. + dbus.FieldErrorName: dbus.MakeVariant("org.freedesktop.DBus.Error.NameHasNoOwner"), + }, + } + return []*dbus.Message{response}, nil + }) + c.Assert(err, IsNil) + restore := dbusutil.MockOnlySessionBusAvailable(conn) + defer restore() + + _, err = agent.New() + c.Assert(err, IsNil) + + refreshInfo := &client.PendingSnapRefreshInfo{ + InstanceName: "pkg", + } + reqBody, err := json.Marshal(refreshInfo) + c.Assert(err, IsNil) + req, err := http.NewRequest("POST", "/v1/notifications/pending-refresh", bytes.NewBuffer(reqBody)) + req.Header.Set("Content-Type", "application/json") + c.Assert(err, IsNil) + rec := httptest.NewRecorder() + agent.PendingRefreshNotificationCmd.POST(agent.PendingRefreshNotificationCmd, req).ServeHTTP(rec, req) + c.Check(rec.Code, Equals, 500) + c.Check(rec.HeaderMap.Get("Content-Type"), Equals, "application/json") + + var rsp resp + c.Assert(json.Unmarshal(rec.Body.Bytes(), &rsp), IsNil) + c.Check(rsp.Type, Equals, agent.ResponseTypeError) + c.Check(rsp.Result, DeepEquals, map[string]interface{}{"message": "cannot send notification message: org.freedesktop.DBus.Error.NameHasNoOwner"}) +} diff -Nru snapd-2.47.1+20.10.1build1/usersession/client/client.go snapd-2.48+21.04/usersession/client/client.go --- snapd-2.47.1+20.10.1build1/usersession/client/client.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/usersession/client/client.go 2020-11-19 16:51:02.000000000 +0000 @@ -32,6 +32,7 @@ "path/filepath" "strconv" "sync" + "time" "github.com/snapcore/snapd/dirs" ) @@ -267,3 +268,22 @@ _, stopFailures, err = client.serviceControlCall(ctx, "stop", services) return stopFailures, err } + +// PendingSnapRefreshInfo holds information about pending snap refresh provided to userd. +type PendingSnapRefreshInfo struct { + InstanceName string `json:"instance-name"` + TimeRemaining time.Duration `json:"time-remaining,omitempty"` + BusyAppName string `json:"busy-app-name,omitempty"` + BusyAppDesktopEntry string `json:"busy-app-desktop-entry,omitempty"` +} + +// PendingRefreshNotification broadcasts information about a refresh. +func (client *Client) PendingRefreshNotification(ctx context.Context, refreshInfo *PendingSnapRefreshInfo) error { + headers := map[string]string{"Content-Type": "application/json"} + reqBody, err := json.Marshal(refreshInfo) + if err != nil { + return err + } + _, err = client.doMany(ctx, "POST", "/v1/notifications/pending-refresh", nil, headers, reqBody) + return err +} diff -Nru snapd-2.47.1+20.10.1build1/usersession/client/client_test.go snapd-2.48+21.04/usersession/client/client_test.go --- snapd-2.47.1+20.10.1build1/usersession/client/client_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/usersession/client/client_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -446,3 +446,14 @@ Error: "failed to stop", }) } + +func (s *clientSuite) TestPendingRefreshNotification(c *C) { + s.handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + c.Assert(r.URL.Path, Equals, "/v1/notifications/pending-refresh") + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + w.Write([]byte(`{"type": "sync"}`)) + }) + err := s.cli.PendingRefreshNotification(context.Background(), &client.PendingSnapRefreshInfo{}) + c.Assert(err, IsNil) +} diff -Nru snapd-2.47.1+20.10.1build1/usersession/userd/launcher.go snapd-2.48+21.04/usersession/userd/launcher.go --- snapd-2.47.1+20.10.1build1/usersession/userd/launcher.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/usersession/userd/launcher.go 2020-11-19 16:51:02.000000000 +0000 @@ -122,13 +122,13 @@ conn *dbus.Conn } -// Name returns the name of the interface this object implements -func (s *Launcher) Name() string { +// Interface returns the name of the interface this object implements +func (s *Launcher) Interface() string { return "io.snapcraft.Launcher" } -// BasePath returns the base path of the object -func (s *Launcher) BasePath() dbus.ObjectPath { +// ObjectPath returns the path that the object is exported as +func (s *Launcher) ObjectPath() dbus.ObjectPath { return "/io/snapcraft/Launcher" } diff -Nru snapd-2.47.1+20.10.1build1/usersession/userd/settings.go snapd-2.48+21.04/usersession/userd/settings.go --- snapd-2.47.1+20.10.1build1/usersession/userd/settings.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/usersession/userd/settings.go 2020-11-19 16:51:02.000000000 +0000 @@ -122,13 +122,13 @@ conn *dbus.Conn } -// Name returns the name of the interface this object implements -func (s *Settings) Name() string { +// Interface returns the name of the interface this object implements +func (s *Settings) Interface() string { return "io.snapcraft.Settings" } -// BasePath returns the base path of the object -func (s *Settings) BasePath() dbus.ObjectPath { +// ObjectPath returns the path that the object is exported as +func (s *Settings) ObjectPath() dbus.ObjectPath { return "/io/snapcraft/Settings" } diff -Nru snapd-2.47.1+20.10.1build1/usersession/userd/userd.go snapd-2.48+21.04/usersession/userd/userd.go --- snapd-2.47.1+20.10.1build1/usersession/userd/userd.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/usersession/userd/userd.go 2020-11-19 16:51:02.000000000 +0000 @@ -30,8 +30,8 @@ ) type dbusInterface interface { - Name() string - BasePath() dbus.ObjectPath + Interface() string + ObjectPath() dbus.ObjectPath IntrospectionData() string } @@ -41,6 +41,14 @@ dbusIfaces []dbusInterface } +// userdBusNames contains the list of bus names userd will acquire on +// the session bus. It is unnecessary (and undesirable) to add more +// names here when adding new interfaces to the daemon. +var userdBusNames = []string{ + "io.snapcraft.Launcher", + "io.snapcraft.Settings", +} + func dbusSessionBus() (*dbus.Conn, error) { // use a private connection to the session bus, this way we can manage // its lifetime without worrying of breaking other code @@ -77,18 +85,21 @@ // at the object level and the actual well-known object name // becoming available on the bus xml := "" + iface.IntrospectionData() + introspect.IntrospectDataString + "" - ud.conn.Export(iface, iface.BasePath(), iface.Name()) - ud.conn.Export(introspect.Introspectable(xml), iface.BasePath(), "org.freedesktop.DBus.Introspectable") + ud.conn.Export(iface, iface.ObjectPath(), iface.Interface()) + ud.conn.Export(introspect.Introspectable(xml), iface.ObjectPath(), "org.freedesktop.DBus.Introspectable") + + } + for _, name := range userdBusNames { // beyond this point the name is available and all handlers must // have been set up - reply, err := ud.conn.RequestName(iface.Name(), dbus.NameFlagDoNotQueue) + reply, err := ud.conn.RequestName(name, dbus.NameFlagDoNotQueue) if err != nil { return err } if reply != dbus.RequestNameReplyPrimaryOwner { - return fmt.Errorf("cannot obtain bus name '%s'", iface.Name()) + return fmt.Errorf("cannot obtain bus name '%s'", name) } } return nil diff -Nru snapd-2.47.1+20.10.1build1/vendor/github.com/canonical/tcglog-parser/constants.go snapd-2.48+21.04/vendor/github.com/canonical/tcglog-parser/constants.go --- snapd-2.47.1+20.10.1build1/vendor/github.com/canonical/tcglog-parser/constants.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48+21.04/vendor/github.com/canonical/tcglog-parser/constants.go 2020-10-28 17:13:27.000000000 +0000 @@ -0,0 +1,71 @@ +// Copyright 2019 Canonical Ltd. +// Licensed under the LGPLv3 with static-linking exception. +// See LICENCE file for details. + +package tcglog + +const ( + EventTypePrebootCert EventType = 0x00000000 // EV_PREBOOT_CERT + EventTypePostCode EventType = 0x00000001 // EV_POST_CODE + // EventTypeUnused = 0x00000002 + EventTypeNoAction EventType = 0x00000003 // EV_NO_ACTION + EventTypeSeparator EventType = 0x00000004 // EV_SEPARATOR + EventTypeAction EventType = 0x00000005 // EV_ACTION + EventTypeEventTag EventType = 0x00000006 // EV_EVENT_TAG + EventTypeSCRTMContents EventType = 0x00000007 // EV_S_CRTM_CONTENTS + EventTypeSCRTMVersion EventType = 0x00000008 // EV_S_CRTM_VERSION + EventTypeCPUMicrocode EventType = 0x00000009 // EV_CPU_MICROCODE + EventTypePlatformConfigFlags EventType = 0x0000000a // EV_PLATFORM_CONFIG_FLAGS + EventTypeTableOfDevices EventType = 0x0000000b // EV_TABLE_OF_DEVICES + EventTypeCompactHash EventType = 0x0000000c // EV_COMPACT_HASH + EventTypeIPL EventType = 0x0000000d // EV_IPL + EventTypeIPLPartitionData EventType = 0x0000000e // EV_IPL_PARTITION_DATA + EventTypeNonhostCode EventType = 0x0000000f // EV_NONHOST_CODE + EventTypeNonhostConfig EventType = 0x00000010 // EV_NONHOST_CONFIG + EventTypeNonhostInfo EventType = 0x00000011 // EV_NONHOST_INFO + EventTypeOmitBootDeviceEvents EventType = 0x00000012 // EV_OMIT_BOOT_DEVICE_EVENTS + + EventTypeEFIEventBase EventType = 0x80000000 // EV_EFI_EVENT_BASE + EventTypeEFIVariableDriverConfig EventType = 0x80000001 // EV_EFI_VARIABLE_DRIVER_CONFIG + EventTypeEFIVariableBoot EventType = 0x80000002 // EV_EFI_VARIABLE_BOOT + EventTypeEFIBootServicesApplication EventType = 0x80000003 // EV_EFI_BOOT_SERVICES_APPLICATION + EventTypeEFIBootServicesDriver EventType = 0x80000004 // EV_EFI_BOOT_SERVICES_DRIVER + EventTypeEFIRuntimeServicesDriver EventType = 0x80000005 // EV_EFI_RUNTIME_SERVICES_DRIVER + EventTypeEFIGPTEvent EventType = 0x80000006 // EV_EFI_GPT_EVENT + EventTypeEFIAction EventType = 0x80000007 // EV_EFI_ACTION + EventTypeEFIPlatformFirmwareBlob EventType = 0x80000008 // EV_EFI_PLATFORM_FIRMWARE_BLOB + EventTypeEFIHandoffTables EventType = 0x80000009 // EF_EFI_HANDOFF_TABLES + EventTypeEFIHCRTMEvent EventType = 0x80000010 // EF_EFI_HCRTM_EVENT + EventTypeEFIVariableAuthority EventType = 0x800000e0 // EV_EFI_VARIABLE_AUTHORITY +) + +const ( + AlgorithmSha1 AlgorithmId = 0x0004 // TPM_ALG_SHA1 + AlgorithmSha256 AlgorithmId = 0x000b // TPM_ALG_SHA256 + AlgorithmSha384 AlgorithmId = 0x000c // TPM_ALG_SHA384 + AlgorithmSha512 AlgorithmId = 0x000d // TPM_ALG_SHA512 +) + +const ( + // SpecUnknown indicates that the specification to which the log conforms is unknown because it doesn't + // start with a spec ID event. + SpecUnknown Spec = iota + + // SpecPCClient indicates that the log conforms to "TCG PC Client Specific Implementation Specification + // for Conventional BIOS". + // See https://www.trustedcomputinggroup.org/wp-content/uploads/TCG_PCClientImplementation_1-21_1_00.pdf + SpecPCClient + + // SpecEFI_1_2 indicates that the log conforms to "TCG EFI Platform Specification For TPM Family 1.1 or + // 1.2". + // See https://trustedcomputinggroup.org/wp-content/uploads/TCG_EFI_Platform_1_22_Final_-v15.pdf + SpecEFI_1_2 + + // SpecEFI_2 indicates that the log conforms to "TCG PC Client Platform Firmware Profile Specification" + // See https://trustedcomputinggroup.org/wp-content/uploads/TCG_PCClientSpecPlat_TPM_2p0_1p04_pub.pdf + SpecEFI_2 +) + +const ( + SeparatorEventErrorValue uint32 = 1 +) diff -Nru snapd-2.47.1+20.10.1build1/vendor/github.com/canonical/tcglog-parser/efi.go snapd-2.48+21.04/vendor/github.com/canonical/tcglog-parser/efi.go --- snapd-2.47.1+20.10.1build1/vendor/github.com/canonical/tcglog-parser/efi.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48+21.04/vendor/github.com/canonical/tcglog-parser/efi.go 2020-10-28 17:13:27.000000000 +0000 @@ -0,0 +1,806 @@ +// Copyright 2019 Canonical Ltd. +// Licensed under the LGPLv3 with static-linking exception. +// See LICENCE file for details. + +package tcglog + +import ( + "bytes" + "encoding/binary" + "errors" + "fmt" + "io" + "unicode/utf16" + "unicode/utf8" + + "golang.org/x/xerrors" +) + +var ( + surr1 uint16 = 0xd800 + surr2 uint16 = 0xdc00 + surr3 uint16 = 0xe000 +) + +// UEFI_VARIABLE_DATA specifies the number of *characters* for a UTF-16 sequence rather than the size of +// the buffer. Extract a UTF-16 sequence of the correct length, given a buffer and the number of characters. +// The returned buffer can be passed to utf16.Decode. +func extractUTF16Buffer(r io.ReadSeeker, nchars uint64) ([]uint16, error) { + var out []uint16 + + for i := nchars; i > 0; i-- { + var c uint16 + if err := binary.Read(r, binary.LittleEndian, &c); err != nil { + return nil, err + } + out = append(out, c) + if c >= surr1 && c < surr2 { + if err := binary.Read(r, binary.LittleEndian, &c); err != nil { + return nil, err + } + if c < surr2 || c >= surr3 { + // Invalid surrogate sequence. utf16.Decode doesn't consume this + // byte when inserting the replacement char + if _, err := r.Seek(-1, io.SeekCurrent); err != nil { + return nil, err + } + continue + } + // Valid surrogate sequence + out = append(out, c) + } + } + + return out, nil +} + +// EFIGUID corresponds to the EFI_GUID type +type EFIGUID [16]uint8 + +func (guid EFIGUID) String() string { + return fmt.Sprintf("{%08x-%04x-%04x-%04x-%012x}", + binary.LittleEndian.Uint32(guid[0:4]), + binary.LittleEndian.Uint16(guid[4:6]), + binary.LittleEndian.Uint16(guid[6:8]), + binary.BigEndian.Uint16(guid[8:10]), + guid[10:16]) +} + +// MakeEFIGUID makes a new EFIGUID from the supplied arguments. +func MakeEFIGUID(a uint32, b, c, d uint16, e [6]uint8) (out EFIGUID) { + binary.LittleEndian.PutUint32(out[0:4], a) + binary.LittleEndian.PutUint16(out[4:6], b) + binary.LittleEndian.PutUint16(out[6:8], c) + binary.BigEndian.PutUint16(out[8:10], d) + copy(out[10:], e[:]) + return +} + +// https://trustedcomputinggroup.org/wp-content/uploads/TCG_EFI_Platform_1_22_Final_-v15.pdf +// (section 7.4 "EV_NO_ACTION Event Types") +func parseEFI_1_2_SpecIdEvent(r io.Reader, eventData *SpecIdEvent) error { + eventData.Spec = SpecEFI_1_2 + + // TCG_EfiSpecIdEventStruct.vendorInfoSize + var vendorInfoSize uint8 + if err := binary.Read(r, binary.LittleEndian, &vendorInfoSize); err != nil { + return xerrors.Errorf("cannot read vendor info size: %w", err) + } + + // TCG_EfiSpecIdEventStruct.vendorInfo + eventData.VendorInfo = make([]byte, vendorInfoSize) + if _, err := io.ReadFull(r, eventData.VendorInfo); err != nil { + return xerrors.Errorf("cannot read vendor info: %w", err) + } + + return nil +} + +// https://trustedcomputinggroup.org/wp-content/uploads/TCG_PCClientSpecPlat_TPM_2p0_1p04_pub.pdf +// (secion 9.4.5.1 "Specification ID Version Event") +func parseEFI_2_SpecIdEvent(r io.Reader, eventData *SpecIdEvent) error { + eventData.Spec = SpecEFI_2 + + // TCG_EfiSpecIdEvent.numberOfAlgorithms + var numberOfAlgorithms uint32 + if err := binary.Read(r, binary.LittleEndian, &numberOfAlgorithms); err != nil { + return xerrors.Errorf("cannot read number of digest algorithms: %w", err) + } + + if numberOfAlgorithms < 1 { + return errors.New("numberOfAlgorithms is zero") + } + + // TCG_EfiSpecIdEvent.digestSizes + eventData.DigestSizes = make([]EFISpecIdEventAlgorithmSize, numberOfAlgorithms) + if err := binary.Read(r, binary.LittleEndian, eventData.DigestSizes); err != nil { + return xerrors.Errorf("cannot read digest algorithm sizes: %w", err) + } + for _, d := range eventData.DigestSizes { + if d.AlgorithmId.supported() && d.AlgorithmId.Size() != int(d.DigestSize) { + return fmt.Errorf("digestSize for algorithmId %v does not match expected size", d.AlgorithmId) + } + } + + // TCG_EfiSpecIdEvent.vendorInfoSize + var vendorInfoSize uint8 + if err := binary.Read(r, binary.LittleEndian, &vendorInfoSize); err != nil { + return xerrors.Errorf("cannot read vendor info size: %w", err) + } + + // TCG_EfiSpecIdEvent.vendorInfo + eventData.VendorInfo = make([]byte, vendorInfoSize) + if _, err := io.ReadFull(r, eventData.VendorInfo); err != nil { + return xerrors.Errorf("cannot read vendor info: %w", err) + } + + return nil +} + +// startupLocalityEventData is the event data for a StartupLocality EV_NO_ACTION event. +type startupLocalityEventData struct { + data []byte + signature string + locality uint8 +} + +func (e *startupLocalityEventData) String() string { + return fmt.Sprintf("EfiStartupLocalityEvent{ StartupLocality: %d }", e.locality) +} + +func (e *startupLocalityEventData) Bytes() []byte { + return e.data +} + +func (e *startupLocalityEventData) Type() NoActionEventType { + return StartupLocality +} + +func (e *startupLocalityEventData) Signature() string { + return e.signature +} + +// https://trustedcomputinggroup.org/wp-content/uploads/TCG_PCClientSpecPlat_TPM_2p0_1p04_pub.pdf +// (section 9.4.5.3 "Startup Locality Event") +func decodeStartupLocalityEvent(r io.Reader, signature string, data []byte) (*startupLocalityEventData, error) { + var locality uint8 + if err := binary.Read(r, binary.LittleEndian, &locality); err != nil { + return nil, err + } + + return &startupLocalityEventData{data: data, signature: signature, locality: locality}, nil +} + +type bimReferenceManifestEventData struct { + data []byte + signature string + vendorId uint32 + guid EFIGUID +} + +func (e *bimReferenceManifestEventData) String() string { + return fmt.Sprintf("Sp800_155_PlatformId_Event{ VendorId: %d, ReferenceManifestGuid: %s }", e.vendorId, &e.guid) +} + +func (e *bimReferenceManifestEventData) Bytes() []byte { + return e.data +} + +func (e *bimReferenceManifestEventData) Type() NoActionEventType { + return BiosIntegrityMeasurement +} + +func (e *bimReferenceManifestEventData) Signature() string { + return e.signature +} + +// https://trustedcomputinggroup.org/wp-content/uploads/TCG_PCClientSpecPlat_TPM_2p0_1p04_pub.pdf +// (section 9.4.5.2 "BIOS Integrity Measurement Reference Manifest Event") +// https://trustedcomputinggroup.org/wp-content/uploads/TCG_EFI_Platform_1_22_Final_-v15.pdf +// (section 7.4 "EV_NO_ACTION Event Types") +func decodeBIMReferenceManifestEvent(r io.Reader, signature string, data []byte) (*bimReferenceManifestEventData, error) { + var d struct { + VendorId uint32 + Guid EFIGUID + } + if err := binary.Read(r, binary.LittleEndian, &d); err != nil { + return nil, err + } + + return &bimReferenceManifestEventData{data: data, signature: signature, vendorId: d.VendorId, guid: d.Guid}, nil +} + +// EFIVariableData corresponds to the EFI_VARIABLE_DATA type and is the event data associated with the measurement of an +// EFI variable. +type EFIVariableData struct { + data []byte + consumedBytes int + VariableName EFIGUID + UnicodeName string + VariableData []byte +} + +func (e *EFIVariableData) String() string { + return fmt.Sprintf("UEFI_VARIABLE_DATA{ VariableName: %s, UnicodeName: \"%s\" }", e.VariableName, e.UnicodeName) +} + +func (e *EFIVariableData) Bytes() []byte { + return e.data +} + +// EncodeMeasuredBytes encodes this data in to the form in which it is hashed and measured by firmware or other bootloaders. +func (e *EFIVariableData) EncodeMeasuredBytes(w io.Writer) error { + if _, err := w.Write(e.VariableName[:]); err != nil { + return xerrors.Errorf("cannot write variable name: %w", err) + } + if err := binary.Write(w, binary.LittleEndian, uint64(utf8.RuneCount([]byte(e.UnicodeName)))); err != nil { + return xerrors.Errorf("cannot write unicode name length: %w", err) + } + if err := binary.Write(w, binary.LittleEndian, uint64(len(e.VariableData))); err != nil { + return xerrors.Errorf("cannot write variable data length: %w", err) + } + if err := binary.Write(w, binary.LittleEndian, convertStringToUtf16(e.UnicodeName)); err != nil { + return xerrors.Errorf("cannot write unicode name: %w", err) + } + if _, err := w.Write(e.VariableData); err != nil { + return xerrors.Errorf("cannot write variable data: %w", err) + } + return nil +} + +// TrailingBytes returns any trailing bytes that were not used during decoding. This indicates a bug in the software responsible +// for the event. See https://github.com/rhboot/shim/commit/7e4d3f1c8c730a5d3f40729cb285b5d8c7b241af and +// https://github.com/rhboot/shim/commit/8a27a4809a6a2b40fb6a4049071bf96d6ad71b50 for the types of bugs that might cause this. Note +// that trailing bytes that are measured must be taken in to account when using EncodeMeasuredBytes. +func (e *EFIVariableData) TrailingBytes() []byte { + return e.data[e.consumedBytes:] +} + +// https://trustedcomputinggroup.org/wp-content/uploads/TCG_EFI_Platform_1_22_Final_-v15.pdf (section 7.8 "Measuring EFI Variables") +// https://trustedcomputinggroup.org/wp-content/uploads/TCG_PCClientSpecPlat_TPM_2p0_1p04_pub.pdf (section 9.2.6 "Measuring UEFI Variables") +func decodeEventDataEFIVariable(data []byte, eventType EventType) (*EFIVariableData, error) { + r := bytes.NewReader(data) + + d := &EFIVariableData{data: data} + + if _, err := io.ReadFull(r, d.VariableName[:]); err != nil { + return nil, xerrors.Errorf("cannot read variable name: %w", err) + } + + var unicodeNameLength uint64 + if err := binary.Read(r, binary.LittleEndian, &unicodeNameLength); err != nil { + return nil, xerrors.Errorf("cannot read unicode name length: %w", err) + } + + var variableDataLength uint64 + if err := binary.Read(r, binary.LittleEndian, &variableDataLength); err != nil { + return nil, xerrors.Errorf("cannot read variable data length: %w", err) + } + + utf16Name, err := extractUTF16Buffer(r, unicodeNameLength) + if err != nil { + return nil, xerrors.Errorf("cannot extract unicode name buffer: %w", err) + } + d.UnicodeName = convertUtf16ToString(utf16Name) + + d.VariableData = make([]byte, variableDataLength) + if _, err := io.ReadFull(r, d.VariableData); err != nil { + return nil, xerrors.Errorf("cannot read variable data: %w", err) + } + + d.consumedBytes = int(r.Size()) - r.Len() + + return d, nil +} + +type efiDevicePathNodeType uint8 + +func (t efiDevicePathNodeType) String() string { + switch t { + case efiDevicePathNodeHardware: + return "HardwarePath" + case efiDevicePathNodeACPI: + return "AcpiPath" + case efiDevicePathNodeMsg: + return "Msg" + case efiDevicePathNodeMedia: + return "MediaPath" + case efiDevicePathNodeBBS: + return "BbsPath" + default: + return fmt.Sprintf("Path[%02x]", uint8(t)) + } +} + +const ( + efiDevicePathNodeHardware efiDevicePathNodeType = 0x01 + efiDevicePathNodeACPI = 0x02 + efiDevicePathNodeMsg = 0x03 + efiDevicePathNodeMedia = 0x04 + efiDevicePathNodeBBS = 0x05 + efiDevicePathNodeEoH = 0x7f +) + +const ( + efiHardwareDevicePathNodePCI = 0x01 + + efiACPIDevicePathNodeNormal = 0x01 + + efiMsgDevicePathNodeLU = 0x11 + efiMsgDevicePathNodeSATA = 0x12 + + efiMediaDevicePathNodeHardDrive = 0x01 + efiMediaDevicePathNodeFilePath = 0x04 + efiMediaDevicePathNodeFvFile = 0x06 + efiMediaDevicePathNodeFv = 0x07 + efiMediaDevicePathNodeRelOffsetRange = 0x08 +) + +func firmwareDevicePathNodeToString(subType uint8, data []byte) (string, error) { + r := bytes.NewReader(data) + + var name EFIGUID + if _, err := io.ReadFull(r, name[:]); err != nil { + return "", xerrors.Errorf("cannot read name: %w", err) + } + + var builder bytes.Buffer + switch subType { + case efiMediaDevicePathNodeFvFile: + builder.WriteString("\\FvFile") + case efiMediaDevicePathNodeFv: + builder.WriteString("\\Fv") + default: + return "", fmt.Errorf("invalid sub type for firmware device path node: %d", subType) + } + + fmt.Fprintf(&builder, "(%s)", name) + return builder.String(), nil +} + +func acpiDevicePathNodeToString(data []byte) (string, error) { + r := bytes.NewReader(data) + + var hid uint32 + if err := binary.Read(r, binary.LittleEndian, &hid); err != nil { + return "", xerrors.Errorf("cannot read HID: %w", err) + } + + var uid uint32 + if err := binary.Read(r, binary.LittleEndian, &uid); err != nil { + return "", xerrors.Errorf("cannot read UID: %w", err) + } + + if hid&0xffff == 0x41d0 { + switch hid >> 16 { + case 0x0a03: + return fmt.Sprintf("\\PciRoot(0x%x)", uid), nil + case 0x0a08: + return fmt.Sprintf("\\PcieRoot(0x%x)", uid), nil + case 0x0604: + return fmt.Sprintf("\\Floppy(0x%x)", uid), nil + default: + return fmt.Sprintf("\\Acpi(PNP%04x,0x%x)", hid>>16, uid), nil + } + } else { + return fmt.Sprintf("\\Acpi(0x%08x,0x%x)", hid, uid), nil + } +} + +func pciDevicePathNodeToString(data []byte) (string, error) { + r := bytes.NewReader(data) + + var function uint8 + if err := binary.Read(r, binary.LittleEndian, &function); err != nil { + return "", xerrors.Errorf("cannot read function: %w", err) + } + + var device uint8 + if err := binary.Read(r, binary.LittleEndian, &device); err != nil { + return "", xerrors.Errorf("cannot read device: %w", err) + } + + return fmt.Sprintf("\\Pci(0x%x,0x%x)", device, function), nil +} + +func luDevicePathNodeToString(data []byte) (string, error) { + r := bytes.NewReader(data) + + var lun uint8 + if err := binary.Read(r, binary.LittleEndian, &lun); err != nil { + return "", xerrors.Errorf("cannot read LUN: %w", err) + } + + return fmt.Sprintf("\\Unit(0x%x)", lun), nil +} + +func hardDriveDevicePathNodeToString(data []byte) (string, error) { + r := bytes.NewReader(data) + + var partNumber uint32 + if err := binary.Read(r, binary.LittleEndian, &partNumber); err != nil { + return "", xerrors.Errorf("cannot read partition number: %w", err) + } + + var partStart uint64 + if err := binary.Read(r, binary.LittleEndian, &partStart); err != nil { + return "", xerrors.Errorf("cannot read partition start: %w", err) + } + + var partSize uint64 + if err := binary.Read(r, binary.LittleEndian, &partSize); err != nil { + return "", xerrors.Errorf("cannot read partition size: %w", err) + } + + var sig EFIGUID + if _, err := io.ReadFull(r, sig[:]); err != nil { + return "", xerrors.Errorf("cannot read signature: %w", err) + } + + var partFormat uint8 + if err := binary.Read(r, binary.LittleEndian, &partFormat); err != nil { + return "", xerrors.Errorf("cannot read partition format: %w", err) + } + + var sigType uint8 + if err := binary.Read(r, binary.LittleEndian, &sigType); err != nil { + return "", xerrors.Errorf("cannot read signature type: %w", err) + } + + var builder bytes.Buffer + + switch sigType { + case 0x01: + fmt.Fprintf(&builder, "\\HD(%d,MBR,0x%08x,", partNumber, binary.LittleEndian.Uint32(sig[:])) + case 0x02: + fmt.Fprintf(&builder, "\\HD(%d,GPT,%s,", partNumber, sig) + default: + fmt.Fprintf(&builder, "\\HD(%d,%d,0,", partNumber, sigType) + } + + fmt.Fprintf(&builder, "0x%016x, 0x%016x)", partStart, partSize) + return builder.String(), nil +} + +func sataDevicePathNodeToString(data []byte) (string, error) { + r := bytes.NewReader(data) + + var hbaPortNumber uint16 + if err := binary.Read(r, binary.LittleEndian, &hbaPortNumber); err != nil { + return "", xerrors.Errorf("cannot read HBA port number: %w", err) + } + + var portMultiplierPortNumber uint16 + if err := binary.Read(r, binary.LittleEndian, &portMultiplierPortNumber); err != nil { + return "", xerrors.Errorf("cannot read port multiplier port number: %w", err) + } + + var lun uint16 + if err := binary.Read(r, binary.LittleEndian, &lun); err != nil { + return "", xerrors.Errorf("cannot read LUN: %w", err) + } + + return fmt.Sprintf("\\Sata(0x%x,0x%x,0x%x)", hbaPortNumber, portMultiplierPortNumber, lun), nil +} + +func filePathDevicePathNodeToString(data []byte) string { + u16 := make([]uint16, len(data)/2) + r := bytes.NewReader(data) + binary.Read(r, binary.LittleEndian, &u16) + + var buf bytes.Buffer + for _, r := range utf16.Decode(u16) { + buf.WriteRune(r) + } + return buf.String() +} + +func relOffsetRangePathNodeToString(data []byte) (string, error) { + r := bytes.NewReader(data) + + if _, err := r.Seek(4, io.SeekCurrent); err != nil { + return "", err + } + + var start uint64 + if err := binary.Read(r, binary.LittleEndian, &start); err != nil { + return "", xerrors.Errorf("cannot read start: %w", err) + } + + var end uint64 + if err := binary.Read(r, binary.LittleEndian, &end); err != nil { + return "", xerrors.Errorf("cannot read end: %w", err) + } + + return fmt.Sprintf("\\Offset(0x%x,0x%x)", start, end), nil +} + +func decodeDevicePathNode(r io.Reader) (string, error) { + var t efiDevicePathNodeType + if err := binary.Read(r, binary.LittleEndian, &t); err != nil { + return "", xerrors.Errorf("cannot read type: %w", err) + } + + if t == efiDevicePathNodeEoH { + return "", nil + } + + var subType uint8 + if err := binary.Read(r, binary.LittleEndian, &subType); err != nil { + return "", xerrors.Errorf("cannot read sub-type: %w", err) + } + + var length uint16 + if err := binary.Read(r, binary.LittleEndian, &length); err != nil { + return "", xerrors.Errorf("cannot read length: %w", err) + } + + if length < 4 { + return "", errors.New("unexpected length") + } + + data := make([]byte, length-4) + if _, err := io.ReadFull(r, data); err != nil { + return "", xerrors.Errorf("cannot read data: %w", err) + } + + switch t { + case efiDevicePathNodeMedia: + switch subType { + case efiMediaDevicePathNodeFvFile, efiMediaDevicePathNodeFv: + s, err := firmwareDevicePathNodeToString(subType, data) + if err != nil { + return "", xerrors.Errorf("cannot decode Fv or FvFile node: %w", err) + } + return s, nil + case efiMediaDevicePathNodeHardDrive: + s, err := hardDriveDevicePathNodeToString(data) + if err != nil { + return "", xerrors.Errorf("cannot decode HD node: %w", err) + } + return s, nil + case efiMediaDevicePathNodeFilePath: + return filePathDevicePathNodeToString(data), nil + case efiMediaDevicePathNodeRelOffsetRange: + s, err := relOffsetRangePathNodeToString(data) + if err != nil { + return "", xerrors.Errorf("cannot decode Offset node: %w", err) + } + return s, nil + } + case efiDevicePathNodeACPI: + switch subType { + case efiACPIDevicePathNodeNormal: + s, err := acpiDevicePathNodeToString(data) + if err != nil { + return "", xerrors.Errorf("cannot decode Acpi node: %w", err) + } + return s, nil + } + case efiDevicePathNodeHardware: + switch subType { + case efiHardwareDevicePathNodePCI: + s, err := pciDevicePathNodeToString(data) + if err != nil { + return "", xerrors.Errorf("cannot decode Pci node: %w", err) + } + return s, nil + } + case efiDevicePathNodeMsg: + switch subType { + case efiMsgDevicePathNodeLU: + s, err := luDevicePathNodeToString(data) + if err != nil { + return "", xerrors.Errorf("cannot decode Unit node: %w", err) + } + return s, nil + case efiMsgDevicePathNodeSATA: + s, err := sataDevicePathNodeToString(data) + if err != nil { + return "", xerrors.Errorf("cannot decode Sata node: %w", err) + } + return s, nil + } + + } + + var builder bytes.Buffer + fmt.Fprintf(&builder, "\\%s(%d", t, subType) + if len(data) > 0 { + fmt.Fprintf(&builder, ", 0x") + for _, b := range data { + fmt.Fprintf(&builder, "%02x", b) + } + } + fmt.Fprintf(&builder, ")") + return builder.String(), nil +} + +func decodeDevicePath(data []byte) (string, error) { + r := bytes.NewReader(data) + var builder bytes.Buffer + + for i := 0; ; i++ { + node, err := decodeDevicePathNode(r) + if err != nil { + return "", xerrors.Errorf("cannot decode node %d: %w", i, err) + } + if node == "" { + return builder.String(), nil + } + fmt.Fprintf(&builder, "%s", node) + } +} + +type efiImageLoadEventData struct { + data []byte + locationInMemory uint64 + lengthInMemory uint64 + linkTimeAddress uint64 + path string +} + +func (e *efiImageLoadEventData) String() string { + return fmt.Sprintf("UEFI_IMAGE_LOAD_EVENT{ ImageLocationInMemory: 0x%016x, ImageLengthInMemory: %d, "+ + "ImageLinkTimeAddress: 0x%016x, DevicePath: %s }", e.locationInMemory, e.lengthInMemory, + e.linkTimeAddress, e.path) +} + +func (e *efiImageLoadEventData) Bytes() []byte { + return e.data +} + +// https://trustedcomputinggroup.org/wp-content/uploads/TCG_EFI_Platform_1_22_Final_-v15.pdf (section 4 "Measuring PE/COFF Image Files") +// https://trustedcomputinggroup.org/wp-content/uploads/TCG_PCClientSpecPlat_TPM_2p0_1p04_pub.pdf (section 9.2.3 "UEFI_IMAGE_LOAD_EVENT Structure") +func decodeEventDataEFIImageLoad(data []byte) (*efiImageLoadEventData, error) { + r := bytes.NewReader(data) + + var locationInMemory uint64 + if err := binary.Read(r, binary.LittleEndian, &locationInMemory); err != nil { + return nil, xerrors.Errorf("cannot read location in memory: %w", err) + } + + var lengthInMemory uint64 + if err := binary.Read(r, binary.LittleEndian, &lengthInMemory); err != nil { + return nil, xerrors.Errorf("cannot read length in memory: %w", err) + } + + var linkTimeAddress uint64 + if err := binary.Read(r, binary.LittleEndian, &linkTimeAddress); err != nil { + return nil, xerrors.Errorf("cannot read link time address: %w", err) + } + + var devicePathLength uint64 + if err := binary.Read(r, binary.LittleEndian, &devicePathLength); err != nil { + return nil, xerrors.Errorf("cannot read device path length: %w", err) + } + + devicePathBuf := make([]byte, devicePathLength) + + if _, err := io.ReadFull(r, devicePathBuf); err != nil { + return nil, xerrors.Errorf("cannot read device path: %w", err) + } + + path, err := decodeDevicePath(devicePathBuf) + if err != nil { + return nil, xerrors.Errorf("cannot decode device path: %w", err) + } + + return &efiImageLoadEventData{data: data, + locationInMemory: locationInMemory, + lengthInMemory: lengthInMemory, + linkTimeAddress: linkTimeAddress, + path: path}, nil +} + +type efiGPTPartitionEntry struct { + typeGUID EFIGUID + uniqueGUID EFIGUID + name string +} + +func (p *efiGPTPartitionEntry) String() string { + return fmt.Sprintf("PartitionTypeGUID: %s, UniquePartitionGUID: %s, Name: \"%s\"", p.typeGUID, p.uniqueGUID, p.name) +} + +type efiGPTEventData struct { + data []byte + diskGUID EFIGUID + partitions []*efiGPTPartitionEntry +} + +func (e *efiGPTEventData) String() string { + var builder bytes.Buffer + fmt.Fprintf(&builder, "UEFI_GPT_DATA{ DiskGUID: %s, Partitions: [", e.diskGUID) + for i, part := range e.partitions { + if i > 0 { + fmt.Fprintf(&builder, ", ") + } + fmt.Fprintf(&builder, "{ %s }", part) + } + fmt.Fprintf(&builder, "] }") + return builder.String() +} + +func (e *efiGPTEventData) Bytes() []byte { + return e.data +} + +func decodeEventDataEFIGPT(data []byte) (*efiGPTEventData, error) { + r := bytes.NewReader(data) + + // Skip UEFI_GPT_DATA.UEFIPartitionHeader.{Header, MyLBA, AlternateLBA, FirstUsableLBA, LastUsableLBA} + if _, err := r.Seek(56, io.SeekCurrent); err != nil { + return nil, err + } + + d := &efiGPTEventData{data: data} + + // UEFI_GPT_DATA.UEFIPartitionHeader.DiskGUID + if _, err := io.ReadFull(r, d.diskGUID[:]); err != nil { + return nil, xerrors.Errorf("cannot read disk GUID: %w", err) + } + + // Skip UEFI_GPT_DATA.UEFIPartitionHeader.{PartitionEntryLBA, NumberOfPartitionEntries} + if _, err := r.Seek(12, io.SeekCurrent); err != nil { + return nil, err + } + + // UEFI_GPT_DATA.UEFIPartitionHeader.SizeOfPartitionEntry + var partEntrySize uint32 + if err := binary.Read(r, binary.LittleEndian, &partEntrySize); err != nil { + return nil, xerrors.Errorf("cannot read SizeOfPartitionEntry: %w", err) + } + + // Skip UEFI_GPT_DATA.UEFIPartitionHeader.PartitionEntryArrayCRC32 + if _, err := r.Seek(4, io.SeekCurrent); err != nil { + return nil, err + } + + // UEFI_GPT_DATA.NumberOfPartitions + var numberOfParts uint64 + if err := binary.Read(r, binary.LittleEndian, &numberOfParts); err != nil { + return nil, xerrors.Errorf("cannot read number of partitions: %w", err) + } + + for i := uint64(0); i < numberOfParts; i++ { + entryData := make([]byte, partEntrySize) + if _, err := io.ReadFull(r, entryData); err != nil { + return nil, xerrors.Errorf("cannot read partition entry data: %w", err) + } + + er := bytes.NewReader(entryData) + e := &efiGPTPartitionEntry{} + + if _, err := io.ReadFull(er, e.typeGUID[:]); err != nil { + return nil, xerrors.Errorf("cannot read partition type GUID: %w", err) + } + + if _, err := io.ReadFull(er, e.uniqueGUID[:]); err != nil { + return nil, xerrors.Errorf("cannot read partition unique GUID: %w", err) + } + + // Skip UEFI_GPT_DATA.Partitions[i].{StartingLBA, EndingLBA, Attributes} + if _, err := er.Seek(24, io.SeekCurrent); err != nil { + return nil, err + } + + nameUtf16 := make([]uint16, er.Len()/2) + if err := binary.Read(er, binary.LittleEndian, &nameUtf16); err != nil { + return nil, xerrors.Errorf("cannot read partition name: %w", err) + } + + var name bytes.Buffer + for _, r := range utf16.Decode(nameUtf16) { + if r == rune(0) { + break + } + name.WriteRune(r) + } + e.name = name.String() + + d.partitions = append(d.partitions, e) + } + + return d, nil +} diff -Nru snapd-2.47.1+20.10.1build1/vendor/github.com/canonical/tcglog-parser/eventdata.go snapd-2.48+21.04/vendor/github.com/canonical/tcglog-parser/eventdata.go --- snapd-2.47.1+20.10.1build1/vendor/github.com/canonical/tcglog-parser/eventdata.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48+21.04/vendor/github.com/canonical/tcglog-parser/eventdata.go 2020-10-28 17:13:27.000000000 +0000 @@ -0,0 +1,82 @@ +// Copyright 2019 Canonical Ltd. +// Licensed under the LGPLv3 with static-linking exception. +// See LICENCE file for details. + +package tcglog + +import ( + "fmt" +) + +// EventData represents all event data types that appear in a log. Some implementations of this are exported so that event data +// contents can be inspected programatically. +// +// If an error is encountered when decoding the data associated with an event, the event data will implement the error interface +// which can be used for obtaining information about the decoding error. +type EventData interface { + fmt.Stringer + + // Bytes returns the raw event data bytes as they appear in the event log from which this event data was decoded. + Bytes() []byte +} + +// invalidEventData corresponds to an event data blob that failed to decode correctly. +type invalidEventData struct { + data []byte + err error +} + +func (e *invalidEventData) String() string { + return fmt.Sprintf("Invalid event data: %v", e.err) +} + +func (e *invalidEventData) Bytes() []byte { + return e.data +} + +func (e *invalidEventData) Error() string { + return e.err.Error() +} + +func (e *invalidEventData) Unwrap() error { + return e.err +} + +// opaqueEventData is event data whose format is unknown or implementation defined. +type opaqueEventData struct { + data []byte +} + +func (e *opaqueEventData) String() string { + return "" +} + +func (e *opaqueEventData) Bytes() []byte { + return e.data +} + +func decodeEventData(pcrIndex PCRIndex, eventType EventType, digests DigestMap, data []byte, options *LogOptions) EventData { + if options.EnableGrub && (pcrIndex == 8 || pcrIndex == 9) { + if out := decodeEventDataGRUB(pcrIndex, eventType, data); out != nil { + return out + } + } + + if options.EnableSystemdEFIStub && pcrIndex == options.SystemdEFIStubPCR { + if out := decodeEventDataSystemdEFIStub(eventType, data); out != nil { + return out + } + + } + + out, err := decodeEventDataTCG(eventType, digests, data) + if err != nil { + return &invalidEventData{data: data, err: err} + } + + if out != nil { + return out + } + + return &opaqueEventData{data: data} +} diff -Nru snapd-2.47.1+20.10.1build1/vendor/github.com/canonical/tcglog-parser/grubeventdata.go snapd-2.48+21.04/vendor/github.com/canonical/tcglog-parser/grubeventdata.go --- snapd-2.47.1+20.10.1build1/vendor/github.com/canonical/tcglog-parser/grubeventdata.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48+21.04/vendor/github.com/canonical/tcglog-parser/grubeventdata.go 2020-10-28 17:13:27.000000000 +0000 @@ -0,0 +1,83 @@ +// Copyright 2019 Canonical Ltd. +// Licensed under the LGPLv3 with static-linking exception. +// See LICENCE file for details. + +package tcglog + +import ( + "fmt" + "io" + "strings" +) + +var ( + kernelCmdlinePrefix = "kernel_cmdline: " + grubCmdPrefix = "grub_cmd: " +) + +// GrubStringEventType indicates the type of data measured by GRUB in to a log by GRUB. +type GrubStringEventType int + +const ( + // GrubCmd indicates that the data measured by GRUB is associated with a GRUB command. + GrubCmd GrubStringEventType = iota + + // KernelCmdline indicates that the data measured by GRUB is associated with a kernel commandline. + KernelCmdline +) + +func grubEventTypeString(t GrubStringEventType) string { + switch t { + case GrubCmd: + return "grub_cmd" + case KernelCmdline: + return "kernel_cmdline" + } + panic("invalid value") +} + +// GrubStringEventData represents the data associated with an event measured by GRUB. +type GrubStringEventData struct { + data []byte + Type GrubStringEventType + Str string +} + +func (e *GrubStringEventData) String() string { + return fmt.Sprintf("%s{ %s }", grubEventTypeString(e.Type), e.Str) +} + +func (e *GrubStringEventData) Bytes() []byte { + return e.data +} + +// EncodeMeasuredBytes encodes this data to the form that would be hashed and measured by GRUB. +func (e *GrubStringEventData) EncodeMeasuredBytes(buf io.Writer) error { + if _, err := io.WriteString(buf, e.Str); err != nil { + return err + } + return nil +} + +func decodeEventDataGRUB(pcrIndex PCRIndex, eventType EventType, data []byte) EventData { + if eventType != EventTypeIPL { + return nil + } + + switch pcrIndex { + case 8: + str := string(data) + switch { + case strings.HasPrefix(str, kernelCmdlinePrefix): + return &GrubStringEventData{data, KernelCmdline, strings.TrimSuffix(strings.TrimPrefix(str, kernelCmdlinePrefix), "\x00")} + case strings.HasPrefix(str, grubCmdPrefix): + return &GrubStringEventData{data, GrubCmd, strings.TrimSuffix(strings.TrimPrefix(str, grubCmdPrefix), "\x00")} + default: + return nil + } + case 9: + return &asciiStringEventData{data: data} + default: + panic("unhandled PCR index") + } +} diff -Nru snapd-2.47.1+20.10.1build1/vendor/github.com/canonical/tcglog-parser/LICENSE snapd-2.48+21.04/vendor/github.com/canonical/tcglog-parser/LICENSE --- snapd-2.47.1+20.10.1build1/vendor/github.com/canonical/tcglog-parser/LICENSE 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48+21.04/vendor/github.com/canonical/tcglog-parser/LICENSE 2020-10-28 17:13:27.000000000 +0000 @@ -0,0 +1,191 @@ +All files in this repository are licensed as follows. If you contribute +to this repository, it is assumed that you license your contribution +under the same license unless you state otherwise. + +All files Copyright (C) 2019 Canonical Ltd. unless otherwise specified in the file. + +This software is licensed under the LGPLv3, included below. + +As a special exception to the GNU Lesser General Public License version 3 +("LGPL3"), the copyright holders of this Library give you permission to +convey to a third party a Combined Work that links statically or dynamically +to this Library without providing any Minimal Corresponding Source or +Minimal Application Code as set out in 4d or providing the installation +information set out in section 4e, provided that you comply with the other +provisions of LGPL3 and provided that you meet, for the Application the +terms and conditions of the license(s) which apply to the Application. + +Except as stated in this special exception, the provisions of LGPL3 will +continue to comply in full to this Library. If you modify this Library, you +may apply this exception to your version of this Library, but you are not +obliged to do so. If you do not wish to do so, delete this exception +statement from your version. This exception does not (and cannot) modify any +license terms which apply to the Application, with which you must still +comply. + + + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. diff -Nru snapd-2.47.1+20.10.1build1/vendor/github.com/canonical/tcglog-parser/log.go snapd-2.48+21.04/vendor/github.com/canonical/tcglog-parser/log.go --- snapd-2.47.1+20.10.1build1/vendor/github.com/canonical/tcglog-parser/log.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48+21.04/vendor/github.com/canonical/tcglog-parser/log.go 2020-10-28 17:13:27.000000000 +0000 @@ -0,0 +1,263 @@ +// Copyright 2019 Canonical Ltd. +// Licensed under the LGPLv3 with static-linking exception. +// See LICENCE file for details. + +package tcglog + +import ( + "encoding/binary" + "fmt" + "io" + + "golang.org/x/xerrors" +) + +// LogOptions allows the behaviour of Log to be controlled. +type LogOptions struct { + EnableGrub bool // Enable support for interpreting events recorded by GRUB + EnableSystemdEFIStub bool // Enable support for interpreting events recorded by systemd's EFI linux loader stub + SystemdEFIStubPCR PCRIndex // Specify the PCR that systemd's EFI linux loader stub measures to +} + +type parser interface { + readNextEvent() (*Event, error) +} + +func isPCRIndexInRange(index PCRIndex) bool { + const maxPCRIndex PCRIndex = 31 + return index <= maxPCRIndex +} + +type eventHeader_1_2 struct { + PCRIndex PCRIndex + EventType EventType +} + +type parser_1_2 struct { + r io.Reader + options *LogOptions +} + +// https://trustedcomputinggroup.org/wp-content/uploads/TCG_PCClientImplementation_1-21_1_00.pdf +// (section 11.1.1 "TCG_PCClientPCREventStruct Structure") +func (p *parser_1_2) readNextEvent() (*Event, error) { + var header eventHeader_1_2 + if err := binary.Read(p.r, binary.LittleEndian, &header); err != nil { + if err == io.EOF { + return nil, err + } + return nil, xerrors.Errorf("cannot read event header: %w", err) + } + + if !isPCRIndexInRange(header.PCRIndex) { + return nil, fmt.Errorf("log entry has an out-of-range PCR index (%d)", header.PCRIndex) + } + + digest := make(Digest, AlgorithmSha1.Size()) + if _, err := p.r.Read(digest); err != nil { + return nil, xerrors.Errorf("cannot read SHA-1 digest: %w", err) + } + digests := make(DigestMap) + digests[AlgorithmSha1] = digest + + var eventSize uint32 + if err := binary.Read(p.r, binary.LittleEndian, &eventSize); err != nil { + return nil, xerrors.Errorf("cannot read event size: %w", err) + } + + event := make([]byte, eventSize) + if _, err := io.ReadFull(p.r, event); err != nil { + return nil, xerrors.Errorf("cannot read event data: %w", err) + } + + return &Event{ + PCRIndex: header.PCRIndex, + EventType: header.EventType, + Digests: digests, + Data: decodeEventData(header.PCRIndex, header.EventType, digests, event, p.options), + }, nil +} + +type eventHeader_2 struct { + PCRIndex PCRIndex + EventType EventType + Count uint32 +} + +type parser_2 struct { + r io.Reader + options *LogOptions + algSizes []EFISpecIdEventAlgorithmSize +} + +// https://trustedcomputinggroup.org/wp-content/uploads/TCG_PCClientSpecPlat_TPM_2p0_1p04_pub.pdf +// (section 9.2.2 "TCG_PCR_EVENT2 Structure") +func (p *parser_2) readNextEvent() (*Event, error) { + var header eventHeader_2 + if err := binary.Read(p.r, binary.LittleEndian, &header); err != nil { + if err == io.EOF { + return nil, err + } + return nil, xerrors.Errorf("cannot read event header: %w", err) + } + + if !isPCRIndexInRange(header.PCRIndex) { + return nil, fmt.Errorf("log entry has an out-of-range PCR index (%d)", header.PCRIndex) + } + + digests := make(DigestMap) + + for i := uint32(0); i < header.Count; i++ { + var algorithmId AlgorithmId + if err := binary.Read(p.r, binary.LittleEndian, &algorithmId); err != nil { + return nil, xerrors.Errorf("cannot read algorithm ID: %w", err) + } + + var digestSize uint16 + var j int + for j = 0; j < len(p.algSizes); j++ { + if p.algSizes[j].AlgorithmId == algorithmId { + digestSize = p.algSizes[j].DigestSize + break + } + } + + if j == len(p.algSizes) { + return nil, fmt.Errorf("event contains a digest for an unrecognized algorithm (%v)", algorithmId) + } + + digest := make(Digest, digestSize) + if _, err := io.ReadFull(p.r, digest); err != nil { + return nil, xerrors.Errorf("cannot read digest for algorithm %v: %w", algorithmId, err) + } + + if _, exists := digests[algorithmId]; exists { + return nil, fmt.Errorf("event contains more than one digest value for algorithm %v", algorithmId) + } + digests[algorithmId] = digest + } + + for _, s := range p.algSizes { + if _, exists := digests[s.AlgorithmId]; !exists { + return nil, fmt.Errorf("event is missing a digest value for algorithm %v", s.AlgorithmId) + } + } + + for alg, _ := range digests { + if alg.supported() { + continue + } + delete(digests, alg) + } + + var eventSize uint32 + if err := binary.Read(p.r, binary.LittleEndian, &eventSize); err != nil { + return nil, xerrors.Errorf("cannot read event size: %w", err) + } + + event := make([]byte, eventSize) + if _, err := io.ReadFull(p.r, event); err != nil { + return nil, xerrors.Errorf("cannot read event data: %w", err) + } + + return &Event{ + PCRIndex: header.PCRIndex, + EventType: header.EventType, + Digests: digests, + Data: decodeEventData(header.PCRIndex, header.EventType, digests, event, p.options), + }, nil +} + +func fixupSpecIdEvent(event *Event, algorithms AlgorithmIdList) { + if event.Data.(*SpecIdEvent).Spec != SpecEFI_2 { + return + } + + for _, alg := range algorithms { + if alg == AlgorithmSha1 { + continue + } + + if _, ok := event.Digests[alg]; ok { + continue + } + + event.Digests[alg] = make(Digest, alg.Size()) + } +} + +func isSpecIdEvent(event *Event) (out bool) { + _, out = event.Data.(*SpecIdEvent) + return +} + +// Log corresponds to a parsed event log. +type Log struct { + Spec Spec // The specification to which this log conforms + Algorithms AlgorithmIdList // The digest algorithms that appear in the log + Events []*Event // The list of events in the log +} + +// ParseLog parses an event log read from r, using the supplied options. If an error occurs during parsing, this may return an +// incomplete list of events with the error. +func ParseLog(r io.Reader, options *LogOptions) (*Log, error) { + var parser parser = &parser_1_2{r: r, options: options} + event, err := parser.readNextEvent() + if err != nil { + return nil, err + } + + var spec Spec = SpecUnknown + var digestSizes []EFISpecIdEventAlgorithmSize + + switch d := event.Data.(type) { + case *SpecIdEvent: + spec = d.Spec + digestSizes = d.DigestSizes + } + + var algorithms AlgorithmIdList + + if spec == SpecEFI_2 { + for _, s := range digestSizes { + if s.AlgorithmId.supported() { + algorithms = append(algorithms, s.AlgorithmId) + } + } + parser = &parser_2{r: r, + options: options, + algSizes: digestSizes} + } else { + algorithms = AlgorithmIdList{AlgorithmSha1} + } + + indexTracker := make(map[PCRIndex]uint) + populateEventIndex := func(event *Event) { + var index uint + if i, ok := indexTracker[event.PCRIndex]; ok { + index = i + } + event.Index = index + indexTracker[event.PCRIndex] = index + 1 + } + + populateEventIndex(event) + if isSpecIdEvent(event) { + fixupSpecIdEvent(event, algorithms) + } + + log := &Log{Spec: spec, Algorithms: algorithms, Events: []*Event{event}} + + for { + event, err := parser.readNextEvent() + switch { + case err == io.EOF: + return log, nil + case err != nil: + return log, err + default: + populateEventIndex(event) + log.Events = append(log.Events, event) + } + } +} diff -Nru snapd-2.47.1+20.10.1build1/vendor/github.com/canonical/tcglog-parser/README.md snapd-2.48+21.04/vendor/github.com/canonical/tcglog-parser/README.md --- snapd-2.47.1+20.10.1build1/vendor/github.com/canonical/tcglog-parser/README.md 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48+21.04/vendor/github.com/canonical/tcglog-parser/README.md 2020-10-28 17:13:27.000000000 +0000 @@ -0,0 +1,11 @@ +# TCG Log Parser + +This repository contains a go library for parsing TCG event logs. Also included is a simple command line tool that prints details of log entries to the console. + +## Relevant specifications + +* [TCG PC Client Platform Firmware Profile Specification](https://trustedcomputinggroup.org/wp-content/uploads/TCG_PCClientSpecPlat_TPM_2p0_1p04_pub.pdf) +* [TCG EFI Platform Specification For TPM Family 1.1 or 1.2](https://trustedcomputinggroup.org/wp-content/uploads/TCG_EFI_Platform_1_22_Final_-v15.pdf) +* [TCG PC Client Specific Implementation Specification for Conventional BIOS](https://trustedcomputinggroup.org/wp-content/uploads/TCG_PCClientImplementation_1-21_1_00.pdf) +* [Unified Extensible Firmware Interface (UEFI) Specification](https://uefi.org/sites/default/files/resources/UEFI_Spec_2_8_final.pdf) +* [Platform Initialization (PI) Specification](https://uefi.org/sites/default/files/resources/PI_Spec_1_6.pdf) diff -Nru snapd-2.47.1+20.10.1build1/vendor/github.com/canonical/tcglog-parser/sdefistub.go snapd-2.48+21.04/vendor/github.com/canonical/tcglog-parser/sdefistub.go --- snapd-2.47.1+20.10.1build1/vendor/github.com/canonical/tcglog-parser/sdefistub.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48+21.04/vendor/github.com/canonical/tcglog-parser/sdefistub.go 2020-10-28 17:13:27.000000000 +0000 @@ -0,0 +1,51 @@ +// Copyright 2019 Canonical Ltd. +// Licensed under the LGPLv3 with static-linking exception. +// See LICENCE file for details. + +package tcglog + +import ( + "bytes" + "encoding/binary" + "fmt" + "io" +) + +// SystemdEFIStubEventData represents the data associated with a kernel commandline measured by the systemd EFI stub linux loader. +type SystemdEFIStubEventData struct { + data []byte + Str string +} + +func (e *SystemdEFIStubEventData) String() string { + return fmt.Sprintf("%s", e.Str) +} + +func (e *SystemdEFIStubEventData) Bytes() []byte { + return e.data +} + +// EncodeMeasured bytes encodes this data to the form that would be hashed and measured by the systemd EFI stub linux loader for the +// specified kernel commandline. Note that it assumes that the calling bootloader includes a UTF-16 NULL terminator at the end of +// LoadOptions, and sets LoadOptionsSize to StrLen(LoadOptions)+1 +func (e *SystemdEFIStubEventData) EncodeMeasuredBytes(w io.Writer) error { + // Both GRUB's chainloader and systemd's EFI bootloader include a UTF-16 NULL terminator at the end of LoadOptions and + // set LoadOptionsSize to StrLen(LoadOptions)+1. The EFI stub loader measures LoadOptionsSize number of bytes, meaning that + // the 2 NULL bytes are measured. Include those here. + return binary.Write(w, binary.LittleEndian, append(convertStringToUtf16(e.Str), 0)) +} + +func decodeEventDataSystemdEFIStub(eventType EventType, data []byte) EventData { + if eventType != EventTypeIPL { + return nil + } + + // data is a UTF-16 string in little-endian form terminated with a single zero byte. + // Omit the zero byte added by the EFI stub and then convert to native byte order. + reader := bytes.NewReader(data[:len(data)-1]) + + utf16Str := make([]uint16, len(data)/2) + binary.Read(reader, binary.LittleEndian, &utf16Str) + + return &SystemdEFIStubEventData{data: data, Str: convertUtf16ToString(utf16Str)} +} diff -Nru snapd-2.47.1+20.10.1build1/vendor/github.com/canonical/tcglog-parser/tcgeventdata.go snapd-2.48+21.04/vendor/github.com/canonical/tcglog-parser/tcgeventdata.go --- snapd-2.47.1+20.10.1build1/vendor/github.com/canonical/tcglog-parser/tcgeventdata.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48+21.04/vendor/github.com/canonical/tcglog-parser/tcgeventdata.go 2020-10-28 17:13:27.000000000 +0000 @@ -0,0 +1,324 @@ +// Copyright 2019 Canonical Ltd. +// Licensed under the LGPLv3 with static-linking exception. +// See LICENCE file for details. + +package tcglog + +import ( + "bytes" + "encoding/binary" + "fmt" + "io" + "math" + "strings" + "unsafe" + + "golang.org/x/xerrors" +) + +type invalidSpecIdEventError struct { + err error +} + +func (e invalidSpecIdEventError) Error() string { + return e.err.Error() +} + +func (e invalidSpecIdEventError) Unwrap() error { + return e.err +} + +// EFISpecIdEventAlgorithmSize represents a digest algorithm and its length and corresponds to the +// TCG_EfiSpecIdEventAlgorithmSize type. +type EFISpecIdEventAlgorithmSize struct { + AlgorithmId AlgorithmId + DigestSize uint16 +} + +// NoActionEventType corresponds to the type of a EV_NO_ACTION event. +type NoActionEventType int + +const ( + UnknownNoActionEvent NoActionEventType = iota // Unknown EV_NO_ACTION event type + SpecId // "Spec ID Event00", "Spec ID Event02" or "Spec ID Event03" event type + StartupLocality // "StartupLocality" event type + BiosIntegrityMeasurement // "SP800-155 Event" event type +) + +// NoActionEventData provides a mechanism to determine the type of a EV_NO_ACTION event from the decoded EventData. +type NoActionEventData interface { + Type() NoActionEventType + Spec() string +} + +// SpecIdEvent corresponds to the TCG_PCClientSpecIdEventStruct, TCG_EfiSpecIdEventStruct, and TCG_EfiSpecIdEvent types and is the +// event data for a Specification ID Version EV_NO_ACTION event. +type SpecIdEvent struct { + data []byte + signature string + Spec Spec + PlatformClass uint32 + SpecVersionMinor uint8 + SpecVersionMajor uint8 + SpecErrata uint8 + UintnSize uint8 + DigestSizes []EFISpecIdEventAlgorithmSize // The digest algorithms contained within this log + VendorInfo []byte +} + +func (e *SpecIdEvent) String() string { + var builder bytes.Buffer + switch e.Spec { + case SpecPCClient: + builder.WriteString("PCClientSpecIdEvent") + case SpecEFI_1_2, SpecEFI_2: + builder.WriteString("EfiSpecIDEvent") + } + + fmt.Fprintf(&builder, "{ spec=%d, platformClass=%d, specVersionMinor=%d, specVersionMajor=%d, "+ + "specErrata=%d", e.Spec, e.PlatformClass, e.SpecVersionMinor, e.SpecVersionMajor, e.SpecErrata) + if e.Spec == SpecEFI_2 { + builder.WriteString(", digestSizes=[") + for i, algSize := range e.DigestSizes { + if i > 0 { + builder.WriteString(", ") + } + fmt.Fprintf(&builder, "{ algorithmId=0x%04x, digestSize=%d }", + uint16(algSize.AlgorithmId), algSize.DigestSize) + } + builder.WriteString("]") + } + builder.WriteString(" }") + return builder.String() +} + +func (e *SpecIdEvent) Bytes() []byte { + return e.data +} + +func (e *SpecIdEvent) Type() NoActionEventType { + return SpecId +} + +func (e *SpecIdEvent) Signature() string { + return e.signature +} + +// https://trustedcomputinggroup.org/wp-content/uploads/TCG_PCClientImplementation_1-21_1_00.pdf +// (section 11.3.4.1 "Specification Event") +func parsePCClientSpecIdEvent(r io.Reader, eventData *SpecIdEvent) error { + eventData.Spec = SpecPCClient + + // TCG_PCClientSpecIdEventStruct.vendorInfoSize + var vendorInfoSize uint8 + if err := binary.Read(r, binary.LittleEndian, &vendorInfoSize); err != nil { + return xerrors.Errorf("cannot read vendor info size: %w", err) + } + + // TCG_PCClientSpecIdEventStruct.vendorInfo + eventData.VendorInfo = make([]byte, vendorInfoSize) + if _, err := io.ReadFull(r, eventData.VendorInfo); err != nil { + return xerrors.Errorf("cannot read vendor info: %w", err) + } + + return nil +} + +type specIdEventCommon struct { + PlatformClass uint32 + SpecVersionMinor uint8 + SpecVersionMajor uint8 + SpecErrata uint8 + UintnSize uint8 +} + +// https://trustedcomputinggroup.org/wp-content/uploads/TCG_PCClientImplementation_1-21_1_00.pdf +// (section 11.3.4.1 "Specification Event") +// https://trustedcomputinggroup.org/wp-content/uploads/TCG_EFI_Platform_1_22_Final_-v15.pdf +// (section 7.4 "EV_NO_ACTION Event Types") +// https://trustedcomputinggroup.org/wp-content/uploads/TCG_PCClientSpecPlat_TPM_2p0_1p04_pub.pdf +// (secion 9.4.5.1 "Specification ID Version Event") +func decodeSpecIdEvent(r io.Reader, signature string, data []byte, helper func(io.Reader, *SpecIdEvent) error) (*SpecIdEvent, error) { + var common struct { + PlatformClass uint32 + SpecVersionMinor uint8 + SpecVersionMajor uint8 + SpecErrata uint8 + UintnSize uint8 + } + if err := binary.Read(r, binary.LittleEndian, &common); err != nil { + return nil, invalidSpecIdEventError{xerrors.Errorf("cannot read common fields: %w", err)} + } + + eventData := &SpecIdEvent{ + data: data, + signature: signature, + PlatformClass: common.PlatformClass, + SpecVersionMinor: common.SpecVersionMinor, + SpecVersionMajor: common.SpecVersionMajor, + SpecErrata: common.SpecErrata, + UintnSize: common.UintnSize} + + if err := helper(r, eventData); err != nil { + return nil, invalidSpecIdEventError{err} + } + + return eventData, nil +} + +var ( + validNormalSeparatorValues = [...]uint32{0, math.MaxUint32} +) + +// asciiStringEventData corresponds to event data that is an ASCII string. The event data may be informational (it provides a hint +// as to what was measured as opposed to representing what was measured). +type asciiStringEventData struct { + data []byte +} + +func (e *asciiStringEventData) String() string { + return *(*string)(unsafe.Pointer(&e.data)) +} + +func (e *asciiStringEventData) Bytes() []byte { + return e.data +} + +// unknownNoActionEventData is the event data for a EV_NO_ACTION event with an unrecognized type. +type unknownNoActionEventData struct { + data []byte + signature string +} + +func (e *unknownNoActionEventData) String() string { + return "" +} + +func (e *unknownNoActionEventData) Bytes() []byte { + return e.data +} + +func (e *unknownNoActionEventData) Type() NoActionEventType { + return UnknownNoActionEvent +} + +func (e *unknownNoActionEventData) Signature() string { + return e.signature +} + +// https://trustedcomputinggroup.org/wp-content/uploads/TCG_PCClientImplementation_1-21_1_00.pdf +// (section 11.3.4 "EV_NO_ACTION Event Types") +// https://trustedcomputinggroup.org/wp-content/uploads/TCG_PCClientSpecPlat_TPM_2p0_1p04_pub.pdf +// (section 9.4.5 "EV_NO_ACTION Event Types") +func decodeEventDataNoAction(data []byte) (EventData, error) { + r := bytes.NewReader(data) + + // Signature field + var sig [16]byte + if _, err := io.ReadFull(r, sig[:]); err != nil { + return nil, xerrors.Errorf("cannot read signature: %w", err) + } + signature := strings.TrimRight(string(sig[:]), "\x00") + + switch signature { + case "Spec ID Event00": + out, err := decodeSpecIdEvent(r, signature, data, parsePCClientSpecIdEvent) + if err != nil { + return nil, xerrors.Errorf("cannot decode Spec ID Event00 data: %w", err) + } + return out, nil + case "Spec ID Event02": + out, err := decodeSpecIdEvent(r, signature, data, parseEFI_1_2_SpecIdEvent) + if err != nil { + return nil, xerrors.Errorf("cannot decode Spec ID Event02 data: %w", err) + } + return out, nil + case "Spec ID Event03": + out, err := decodeSpecIdEvent(r, signature, data, parseEFI_2_SpecIdEvent) + if err != nil { + return nil, xerrors.Errorf("cannot decode Spec ID Event03 data: %w", err) + } + return out, nil + case "SP800-155 Event": + out, err := decodeBIMReferenceManifestEvent(r, signature, data) + if err != nil { + return nil, xerrors.Errorf("cannot decode SP800-155 Event data: %w", err) + } + return out, nil + case "StartupLocality": + out, err := decodeStartupLocalityEvent(r, signature, data) + if err != nil { + return nil, xerrors.Errorf("cannot decode StartupLocality data: %w", err) + } + return out, nil + default: + return &unknownNoActionEventData{data: data, signature: signature}, nil + } +} + +// https://trustedcomputinggroup.org/wp-content/uploads/TCG_PCClientImplementation_1-21_1_00.pdf (section 11.3.3 "EV_ACTION event types") +// https://trustedcomputinggroup.org/wp-content/uploads/PC-ClientSpecific_Platform_Profile_for_TPM_2p0_Systems_v51.pdf (section 9.4.3 "EV_ACTION Event Types") +func decodeEventDataAction(data []byte) *asciiStringEventData { + return &asciiStringEventData{data: data} +} + +// SeparatorEventData is the event data associated with a EV_SEPARATOR event. +type SeparatorEventData struct { + data []byte + IsError bool // The event indicates an error condition +} + +func (e *SeparatorEventData) String() string { + if !e.IsError { + return "" + } + return "*ERROR*" +} + +func (e *SeparatorEventData) Bytes() []byte { + return e.data +} + +// https://trustedcomputinggroup.org/wp-content/uploads/TCG_PCClientImplementation_1-21_1_00.pdf +// (section 3.3.2.2 2 Error Conditions" , section 8.2.3 "Measuring Boot Events") +// https://trustedcomputinggroup.org/wp-content/uploads/PC-ClientSpecific_Platform_Profile_for_TPM_2p0_Systems_v51.pdf: +// (section 2.3.2 "Error Conditions", section 2.3.4 "PCR Usage", section 7.2 +// "Procedure for Pre-OS to OS-Present Transition") +func decodeEventDataSeparator(digests DigestMap, data []byte) *SeparatorEventData { + errorValue := make([]byte, 4) + binary.LittleEndian.PutUint32(errorValue, SeparatorEventErrorValue) + + var isError bool + for alg, digest := range digests { + isError = bytes.Equal(digest, alg.hash(errorValue)) + break + } + + return &SeparatorEventData{data: data, IsError: isError} +} + +// https://trustedcomputinggroup.org/wp-content/uploads/TCG_PCClientImplementation_1-21_1_00.pdf (section 11.3.1 "Event Types") +// https://trustedcomputinggroup.org/wp-content/uploads/TCG_EFI_Platform_1_22_Final_-v15.pdf (section 7.2 "Event Types") +// https://trustedcomputinggroup.org/wp-content/uploads/TCG_PCClientSpecPlat_TPM_2p0_1p04_pub.pdf (section 9.4.1 "Event Types") +func decodeEventDataTCG(eventType EventType, digests DigestMap, data []byte) (out EventData, err error) { + switch eventType { + case EventTypeNoAction: + out, err = decodeEventDataNoAction(data) + case EventTypeSeparator: + return decodeEventDataSeparator(digests, data), nil + case EventTypeAction, EventTypeEFIAction: + return decodeEventDataAction(data), nil + case EventTypeEFIVariableDriverConfig, EventTypeEFIVariableBoot, EventTypeEFIVariableAuthority: + out, err = decodeEventDataEFIVariable(data, eventType) + case EventTypeEFIBootServicesApplication, EventTypeEFIBootServicesDriver, EventTypeEFIRuntimeServicesDriver: + out, err = decodeEventDataEFIImageLoad(data) + case EventTypeEFIGPTEvent: + out, err = decodeEventDataEFIGPT(data) + default: + } + + if err != nil { + err = xerrors.Errorf("cannot decode %v event data: %w", eventType, err) + } + return +} diff -Nru snapd-2.47.1+20.10.1build1/vendor/github.com/canonical/tcglog-parser/types.go snapd-2.48+21.04/vendor/github.com/canonical/tcglog-parser/types.go --- snapd-2.47.1+20.10.1build1/vendor/github.com/canonical/tcglog-parser/types.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48+21.04/vendor/github.com/canonical/tcglog-parser/types.go 2020-10-28 17:13:27.000000000 +0000 @@ -0,0 +1,181 @@ +// Copyright 2019 Canonical Ltd. +// Licensed under the LGPLv3 with static-linking exception. +// See LICENCE file for details. + +package tcglog + +import ( + "crypto" + _ "crypto/sha1" + _ "crypto/sha256" + _ "crypto/sha512" + "fmt" +) + +// Spec corresponds to the TCG specification that an event log conforms to. +type Spec uint + +// PCRIndex corresponds to the index of a PCR on the TPM. +type PCRIndex uint32 + +// EventType corresponds to the type of an event in an event log. +type EventType uint32 + +// AlgorithmId corresponds to the algorithm of digests that appear in the log. The values are in sync with those +// in the TPM Library Specification for the TPM_ALG_ID type. +// See https://trustedcomputinggroup.org/wp-content/uploads/TPM-Rev-2.0-Part-2-Structures-01.38.pdf (Table 9) +type AlgorithmId uint16 + +func (a AlgorithmId) GetHash() crypto.Hash { + switch a { + case AlgorithmSha1: + return crypto.SHA1 + case AlgorithmSha256: + return crypto.SHA256 + case AlgorithmSha384: + return crypto.SHA384 + case AlgorithmSha512: + return crypto.SHA512 + default: + return 0 + } +} + +func (a AlgorithmId) supported() bool { + return a.GetHash() != crypto.Hash(0) +} + +func (a AlgorithmId) Size() int { + return a.GetHash().Size() +} + +func (a AlgorithmId) hash(data []byte) []byte { + h := a.GetHash().New() + h.Write(data) + return h.Sum(nil) +} + +// Digest is the result of hashing some data. +type Digest []byte + +// DigestMap is a map of algorithms to digests. +type DigestMap map[AlgorithmId]Digest + +func (e EventType) String() string { + switch e { + case EventTypePrebootCert: + return "EV_PREBOOT_CERT" + case EventTypePostCode: + return "EV_POST_CODE" + case EventTypeNoAction: + return "EV_NO_ACTION" + case EventTypeSeparator: + return "EV_SEPARATOR" + case EventTypeAction: + return "EV_ACTION" + case EventTypeEventTag: + return "EV_EVENT_TAG" + case EventTypeSCRTMContents: + return "EV_S_CRTM_CONTENTS" + case EventTypeSCRTMVersion: + return "EV_S_CRTM_VERSION" + case EventTypeCPUMicrocode: + return "EV_CPU_MICROCODE" + case EventTypePlatformConfigFlags: + return "EV_PLATFORM_CONFIG_FLAGS" + case EventTypeTableOfDevices: + return "EV_TABLE_OF_DEVICES" + case EventTypeCompactHash: + return "EV_COMPACT_HASH" + case EventTypeIPL: + return "EV_IPL" + case EventTypeIPLPartitionData: + return "EV_IPL_PARTITION_DATA" + case EventTypeNonhostCode: + return "EV_NONHOST_CODE" + case EventTypeNonhostConfig: + return "EV_NONHOST_CONFIG" + case EventTypeNonhostInfo: + return "EV_NONHOST_INFO" + case EventTypeOmitBootDeviceEvents: + return "EV_OMIT_BOOT_DEVICE_EVENTS" + case EventTypeEFIVariableDriverConfig: + return "EV_EFI_VARIABLE_DRIVER_CONFIG" + case EventTypeEFIVariableBoot: + return "EV_EFI_VARIABLE_BOOT" + case EventTypeEFIBootServicesApplication: + return "EV_EFI_BOOT_SERVICES_APPLICATION" + case EventTypeEFIBootServicesDriver: + return "EV_EFI_BOOT_SERVICES_DRIVER" + case EventTypeEFIRuntimeServicesDriver: + return "EV_EFI_RUNTIME_SERVICES_DRIVER" + case EventTypeEFIGPTEvent: + return "EF_EFI_GPT_EVENT" + case EventTypeEFIAction: + return "EV_EFI_ACTION" + case EventTypeEFIPlatformFirmwareBlob: + return "EV_EFI_PLATFORM_FIRMWARE_BLOB" + case EventTypeEFIHandoffTables: + return "EV_EFI_HANDOFF_TABLES" + case EventTypeEFIHCRTMEvent: + return "EV_EFI_HCRTM_EVENT" + case EventTypeEFIVariableAuthority: + return "EV_EFI_VARIABLE_AUTHORITY" + default: + return fmt.Sprintf("%08x", uint32(e)) + } +} + +func (e EventType) Format(s fmt.State, f rune) { + switch f { + case 's', 'v': + fmt.Fprintf(s, "%s", e.String()) + default: + fmt.Fprintf(s, makeDefaultFormatter(s, f), uint32(e)) + } +} + +func (a AlgorithmId) String() string { + switch a { + case AlgorithmSha1: + return "SHA-1" + case AlgorithmSha256: + return "SHA-256" + case AlgorithmSha384: + return "SHA-384" + case AlgorithmSha512: + return "SHA-512" + default: + return fmt.Sprintf("%04x", uint16(a)) + } +} + +func (a AlgorithmId) Format(s fmt.State, f rune) { + switch f { + case 's', 'v': + fmt.Fprintf(s, "%s", a.String()) + default: + fmt.Fprintf(s, makeDefaultFormatter(s, f), uint16(a)) + } +} + +// AlgorithmListId is a slice of AlgorithmId values, +type AlgorithmIdList []AlgorithmId + +func (l AlgorithmIdList) Contains(a AlgorithmId) bool { + for _, alg := range l { + if alg == a { + return true + } + } + return false +} + +// Event corresponds to a single event in an event log. +type Event struct { + Index uint // Sequential index of event in the log + PCRIndex PCRIndex // PCR index to which this event was measured + EventType EventType // The type of this event + Digests DigestMap // The digests corresponding to this event for the supported algorithms + Data EventData // The data recorded with this event +} diff -Nru snapd-2.47.1+20.10.1build1/vendor/github.com/canonical/tcglog-parser/utils.go snapd-2.48+21.04/vendor/github.com/canonical/tcglog-parser/utils.go --- snapd-2.47.1+20.10.1build1/vendor/github.com/canonical/tcglog-parser/utils.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48+21.04/vendor/github.com/canonical/tcglog-parser/utils.go 2020-10-28 17:13:27.000000000 +0000 @@ -0,0 +1,50 @@ +// Copyright 2019 Canonical Ltd. +// Licensed under the LGPLv3 with static-linking exception. +// See LICENCE file for details. + +package tcglog + +import ( + "bytes" + "fmt" + "unicode/utf16" + "unicode/utf8" +) + +func makeDefaultFormatter(s fmt.State, f rune) string { + var builder bytes.Buffer + builder.WriteString("%%") + for _, flag := range [...]int{'+', '-', '#', ' ', '0'} { + if s.Flag(flag) { + fmt.Fprintf(&builder, "%c", flag) + } + } + if width, ok := s.Width(); ok { + fmt.Fprintf(&builder, "%d", width) + } + if prec, ok := s.Precision(); ok { + fmt.Fprintf(&builder, ".%d", prec) + } + fmt.Fprintf(&builder, "%c", f) + return builder.String() +} + +func convertStringToUtf16(str string) []uint16 { + var unicodePoints []rune + for len(str) > 0 { + r, s := utf8.DecodeRuneInString(str) + unicodePoints = append(unicodePoints, r) + str = str[s:] + } + return utf16.Encode(unicodePoints) +} + +func convertUtf16ToString(u []uint16) string { + var utf8Str []byte + for _, r := range utf16.Decode(u) { + utf8Char := make([]byte, utf8.RuneLen(r)) + utf8.EncodeRune(utf8Char, r) + utf8Str = append(utf8Str, utf8Char...) + } + return string(utf8Str) +} diff -Nru snapd-2.47.1+20.10.1build1/vendor/github.com/chrisccoulson/tcglog-parser/constants.go snapd-2.48+21.04/vendor/github.com/chrisccoulson/tcglog-parser/constants.go --- snapd-2.47.1+20.10.1build1/vendor/github.com/chrisccoulson/tcglog-parser/constants.go 2020-04-06 16:55:41.000000000 +0000 +++ snapd-2.48+21.04/vendor/github.com/chrisccoulson/tcglog-parser/constants.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,67 +0,0 @@ -package tcglog - -const ( - EventTypePrebootCert EventType = 0x00000000 // EV_PREBOOT_CERT - EventTypePostCode EventType = 0x00000001 // EV_POST_CODE - // EventTypeUnused = 0x00000002 - EventTypeNoAction EventType = 0x00000003 // EV_NO_ACTION - EventTypeSeparator EventType = 0x00000004 // EV_SEPARATOR - EventTypeAction EventType = 0x00000005 // EV_ACTION - EventTypeEventTag EventType = 0x00000006 // EV_EVENT_TAG - EventTypeSCRTMContents EventType = 0x00000007 // EV_S_CRTM_CONTENTS - EventTypeSCRTMVersion EventType = 0x00000008 // EV_S_CRTM_VERSION - EventTypeCPUMicrocode EventType = 0x00000009 // EV_CPU_MICROCODE - EventTypePlatformConfigFlags EventType = 0x0000000a // EV_PLATFORM_CONFIG_FLAGS - EventTypeTableOfDevices EventType = 0x0000000b // EV_TABLE_OF_DEVICES - EventTypeCompactHash EventType = 0x0000000c // EV_COMPACT_HASH - EventTypeIPL EventType = 0x0000000d // EV_IPL - EventTypeIPLPartitionData EventType = 0x0000000e // EV_IPL_PARTITION_DATA - EventTypeNonhostCode EventType = 0x0000000f // EV_NONHOST_CODE - EventTypeNonhostConfig EventType = 0x00000010 // EV_NONHOST_CONFIG - EventTypeNonhostInfo EventType = 0x00000011 // EV_NONHOST_INFO - EventTypeOmitBootDeviceEvents EventType = 0x00000012 // EV_OMIT_BOOT_DEVICE_EVENTS - - EventTypeEFIEventBase EventType = 0x80000000 // EV_EFI_EVENT_BASE - EventTypeEFIVariableDriverConfig EventType = 0x80000001 // EV_EFI_VARIABLE_DRIVER_CONFIG - EventTypeEFIVariableBoot EventType = 0x80000002 // EV_EFI_VARIABLE_BOOT - EventTypeEFIBootServicesApplication EventType = 0x80000003 // EV_EFI_BOOT_SERVICES_APPLICATION - EventTypeEFIBootServicesDriver EventType = 0x80000004 // EV_EFI_BOOT_SERVICES_DRIVER - EventTypeEFIRuntimeServicesDriver EventType = 0x80000005 // EV_EFI_RUNTIME_SERVICES_DRIVER - EventTypeEFIGPTEvent EventType = 0x80000006 // EV_EFI_GPT_EVENT - EventTypeEFIAction EventType = 0x80000007 // EV_EFI_ACTION - EventTypeEFIPlatformFirmwareBlob EventType = 0x80000008 // EV_EFI_PLATFORM_FIRMWARE_BLOB - EventTypeEFIHandoffTables EventType = 0x80000009 // EF_EFI_HANDOFF_TABLES - EventTypeEFIHCRTMEvent EventType = 0x80000010 // EF_EFI_HCRTM_EVENT - EventTypeEFIVariableAuthority EventType = 0x800000e0 // EV_EFI_VARIABLE_AUTHORITY -) - -const ( - AlgorithmSha1 AlgorithmId = 0x0004 // TPM_ALG_SHA1 - AlgorithmSha256 AlgorithmId = 0x000b // TPM_ALG_SHA256 - AlgorithmSha384 AlgorithmId = 0x000c // TPM_ALG_SHA384 - AlgorithmSha512 AlgorithmId = 0x000d // TPM_ALG_SHA512 -) - -const ( - // SpecUnknown indicates that the specification to which the log conforms is unknown because it doesn't - // start with a spec ID event. - SpecUnknown Spec = iota - - // SpecPCClient indicates that the log conforms to "TCG PC Client Specific Implementation Specification - // for Conventional BIOS". - // See https://www.trustedcomputinggroup.org/wp-content/uploads/TCG_PCClientImplementation_1-21_1_00.pdf - SpecPCClient - - // SpecEFI_1_2 indicates that the log conforms to "TCG EFI Platform Specification For TPM Family 1.1 or - // 1.2". - // See https://trustedcomputinggroup.org/wp-content/uploads/TCG_EFI_Platform_1_22_Final_-v15.pdf - SpecEFI_1_2 - - // SpecEFI_2 indicates that the log conforms to "TCG PC Client Platform Firmware Profile Specification" - // See https://trustedcomputinggroup.org/wp-content/uploads/TCG_PCClientSpecPlat_TPM_2p0_1p04_pub.pdf - SpecEFI_2 -) - -const ( - separatorEventErrorValue uint32 = 1 -) diff -Nru snapd-2.47.1+20.10.1build1/vendor/github.com/chrisccoulson/tcglog-parser/efi.go snapd-2.48+21.04/vendor/github.com/chrisccoulson/tcglog-parser/efi.go --- snapd-2.47.1+20.10.1build1/vendor/github.com/chrisccoulson/tcglog-parser/efi.go 2020-04-06 16:55:41.000000000 +0000 +++ snapd-2.48+21.04/vendor/github.com/chrisccoulson/tcglog-parser/efi.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,782 +0,0 @@ -package tcglog - -import ( - "bytes" - "encoding/binary" - "fmt" - "io" - "unicode/utf16" - "unicode/utf8" -) - -var ( - surr1 uint16 = 0xd800 - surr2 uint16 = 0xdc00 - surr3 uint16 = 0xe000 -) - -// UEFI_VARIABLE_DATA specifies the number of *characters* for a UTF-16 sequence rather than the size of -// the buffer. Extract a UTF-16 sequence of the correct length, given a buffer and the number of characters. -// The returned buffer can be passed to utf16.Decode. -func extractUTF16Buffer(stream io.ReadSeeker, nchars uint64) ([]uint16, error) { - var out []uint16 - - for i := nchars; i > 0; i-- { - var c uint16 - if err := binary.Read(stream, binary.LittleEndian, &c); err != nil { - return nil, err - } - out = append(out, c) - if c >= surr1 && c < surr2 { - if err := binary.Read(stream, binary.LittleEndian, &c); err != nil { - return nil, err - } - if c < surr2 || c >= surr3 { - // Invalid surrogate sequence. utf16.Decode doesn't consume this - // byte when inserting the replacement char - if _, err := stream.Seek(-1, io.SeekCurrent); err != nil { - return nil, err - } - continue - } - // Valid surrogate sequence - out = append(out, c) - } - } - - return out, nil -} - -// EFIGUID corresponds to the EFI_GUID type -type EFIGUID struct { - Data1 uint32 - Data2 uint16 - Data3 uint16 - Data4 [8]uint8 -} - -func (g *EFIGUID) String() string { - return fmt.Sprintf("{%08x-%04x-%04x-%04x-%012x}", g.Data1, g.Data2, g.Data3, binary.BigEndian.Uint16(g.Data4[0:2]), g.Data4[2:]) -} - -func NewEFIGUID(a uint32, b, c, d uint16, e [6]uint8) *EFIGUID { - guid := &EFIGUID{Data1: a, Data2: b, Data3: c} - binary.BigEndian.PutUint16(guid.Data4[0:2], d) - copy(guid.Data4[2:], e[:]) - return guid -} - -// https://trustedcomputinggroup.org/wp-content/uploads/TCG_EFI_Platform_1_22_Final_-v15.pdf -// (section 7.4 "EV_NO_ACTION Event Types") -func parseEFI_1_2_SpecIdEvent(stream io.Reader, eventData *SpecIdEventData) error { - eventData.Spec = SpecEFI_1_2 - - // TCG_EfiSpecIdEventStruct.vendorInfoSize - var vendorInfoSize uint8 - if err := binary.Read(stream, binary.LittleEndian, &vendorInfoSize); err != nil { - return wrapSpecIdEventReadError(err) - } - - // TCG_EfiSpecIdEventStruct.vendorInfo - eventData.VendorInfo = make([]byte, vendorInfoSize) - if _, err := io.ReadFull(stream, eventData.VendorInfo); err != nil { - return wrapSpecIdEventReadError(err) - } - - return nil -} - -// https://trustedcomputinggroup.org/wp-content/uploads/TCG_PCClientSpecPlat_TPM_2p0_1p04_pub.pdf -// (secion 9.4.5.1 "Specification ID Version Event") -func parseEFI_2_SpecIdEvent(stream io.Reader, eventData *SpecIdEventData) error { - eventData.Spec = SpecEFI_2 - - // TCG_EfiSpecIdEvent.numberOfAlgorithms - var numberOfAlgorithms uint32 - if err := binary.Read(stream, binary.LittleEndian, &numberOfAlgorithms); err != nil { - return wrapSpecIdEventReadError(err) - } - - if numberOfAlgorithms < 1 { - return invalidSpecIdEventError{"numberOfAlgorithms is zero"} - } - - // TCG_EfiSpecIdEvent.digestSizes - eventData.DigestSizes = make([]EFISpecIdEventAlgorithmSize, numberOfAlgorithms) - if err := binary.Read(stream, binary.LittleEndian, eventData.DigestSizes); err != nil { - return wrapSpecIdEventReadError(err) - } - for _, d := range eventData.DigestSizes { - if d.AlgorithmId.supported() && d.AlgorithmId.size() != int(d.DigestSize) { - return invalidSpecIdEventError{ - fmt.Sprintf("digestSize for algorithmId 0x%04x doesn't match expected size "+ - "(got: %d, expected: %d)", d.AlgorithmId, d.DigestSize, d.AlgorithmId.size())} - } - } - - // TCG_EfiSpecIdEvent.vendorInfoSize - var vendorInfoSize uint8 - if err := binary.Read(stream, binary.LittleEndian, &vendorInfoSize); err != nil { - return wrapSpecIdEventReadError(err) - } - - // TCG_EfiSpecIdEvent.vendorInfo - eventData.VendorInfo = make([]byte, vendorInfoSize) - if _, err := io.ReadFull(stream, eventData.VendorInfo); err != nil { - return wrapSpecIdEventReadError(err) - } - - return nil -} - -type startupLocalityEventData struct { - data []byte - Locality uint8 -} - -func (e *startupLocalityEventData) String() string { - return fmt.Sprintf("EfiStartupLocalityEvent{ StartupLocality: %d }", e.Locality) -} - -func (e *startupLocalityEventData) Bytes() []byte { - return e.data -} - -func (e *startupLocalityEventData) Type() NoActionEventType { - return StartupLocality -} - -// https://trustedcomputinggroup.org/wp-content/uploads/TCG_PCClientSpecPlat_TPM_2p0_1p04_pub.pdf -// (section 9.4.5.3 "Startup Locality Event") -func decodeStartupLocalityEvent(stream io.Reader, data []byte) (*startupLocalityEventData, error) { - var locality uint8 - if err := binary.Read(stream, binary.LittleEndian, &locality); err != nil { - return nil, err - } - - return &startupLocalityEventData{data: data, Locality: locality}, nil -} - -type bimReferenceManifestEventData struct { - data []byte - VendorId uint32 - Guid EFIGUID -} - -func (e *bimReferenceManifestEventData) String() string { - return fmt.Sprintf("Sp800_155_PlatformId_Event{ VendorId: %d, ReferenceManifestGuid: %s }", - e.VendorId, &e.Guid) -} - -func (e *bimReferenceManifestEventData) Bytes() []byte { - return e.data -} - -func (e *bimReferenceManifestEventData) Type() NoActionEventType { - return BiosIntegrityMeasurement -} - -// https://trustedcomputinggroup.org/wp-content/uploads/TCG_PCClientSpecPlat_TPM_2p0_1p04_pub.pdf -// (section 9.4.5.2 "BIOS Integrity Measurement Reference Manifest Event") -// https://trustedcomputinggroup.org/wp-content/uploads/TCG_EFI_Platform_1_22_Final_-v15.pdf -// (section 7.4 "EV_NO_ACTION Event Types") -func decodeBIMReferenceManifestEvent(stream io.Reader, data []byte) (*bimReferenceManifestEventData, error) { - var d struct{ - VendorId uint32 - Guid EFIGUID - } - if err := binary.Read(stream, binary.LittleEndian, &d); err != nil { - return nil, err - } - - return &bimReferenceManifestEventData{data: data, VendorId: d.VendorId, Guid: d.Guid}, nil -} - -// EFIVariableEventData corresponds to the EFI_VARIABLE_DATA type. -type EFIVariableEventData struct { - data []byte - VariableName EFIGUID - UnicodeName string - VariableData []byte -} - -func (e *EFIVariableEventData) String() string { - return fmt.Sprintf("UEFI_VARIABLE_DATA{ VariableName: %s, UnicodeName: \"%s\" }", - e.VariableName.String(), e.UnicodeName) -} - -func (e *EFIVariableEventData) Bytes() []byte { - return e.data -} - -func (e *EFIVariableEventData) EncodeMeasuredBytes(buf io.Writer) error { - if err := binary.Write(buf, binary.LittleEndian, e.VariableName); err != nil { - return err - } - if err := binary.Write(buf, binary.LittleEndian, uint64(utf8.RuneCount([]byte(e.UnicodeName)))); err != nil { - return err - } - if err := binary.Write(buf, binary.LittleEndian, uint64(len(e.VariableData))); err != nil { - return err - } - if err := binary.Write(buf, binary.LittleEndian, convertStringToUtf16(e.UnicodeName)); err != nil { - return err - } - if _, err := buf.Write(e.VariableData); err != nil { - return err - } - return nil -} - -// https://trustedcomputinggroup.org/wp-content/uploads/TCG_EFI_Platform_1_22_Final_-v15.pdf (section 7.8 "Measuring EFI Variables") -// https://trustedcomputinggroup.org/wp-content/uploads/TCG_PCClientSpecPlat_TPM_2p0_1p04_pub.pdf (section 9.2.6 "Measuring UEFI Variables") -func decodeEventDataEFIVariableImpl(data []byte, eventType EventType) (*EFIVariableEventData, int, error) { - stream := bytes.NewReader(data) - - var guid EFIGUID - if err := binary.Read(stream, binary.LittleEndian, &guid); err != nil { - return nil, 0, err - } - - var unicodeNameLength uint64 - if err := binary.Read(stream, binary.LittleEndian, &unicodeNameLength); err != nil { - return nil, 0, err - } - - var variableDataLength uint64 - if err := binary.Read(stream, binary.LittleEndian, &variableDataLength); err != nil { - return nil, 0, err - } - - utf16Name, err := extractUTF16Buffer(stream, unicodeNameLength) - if err != nil { - return nil, 0, err - } - - variableData := make([]byte, variableDataLength) - if _, err := io.ReadFull(stream, variableData); err != nil { - return nil, 0, err - } - - return &EFIVariableEventData{data: data, - VariableName: guid, - UnicodeName: convertUtf16ToString(utf16Name), - VariableData: variableData}, stream.Len(), nil -} - -func decodeEventDataEFIVariable(data []byte, eventType EventType) (out EventData, trailingBytes int, err error) { - d, trailingBytes, err := decodeEventDataEFIVariableImpl(data, eventType) - if d != nil { - out = d - } - return -} - -type efiDevicePathNodeType uint8 - -func (t efiDevicePathNodeType) String() string { - switch t { - case efiDevicePathNodeHardware: - return "HardwarePath" - case efiDevicePathNodeACPI: - return "AcpiPath" - case efiDevicePathNodeMsg: - return "Msg" - case efiDevicePathNodeMedia: - return "MediaPath" - case efiDevicePathNodeBBS: - return "BbsPath" - default: - return fmt.Sprintf("Path[%02x]", uint8(t)) - } -} - -const ( - efiDevicePathNodeHardware efiDevicePathNodeType = 0x01 - efiDevicePathNodeACPI = 0x02 - efiDevicePathNodeMsg = 0x03 - efiDevicePathNodeMedia = 0x04 - efiDevicePathNodeBBS = 0x05 - efiDevicePathNodeEoH = 0x7f -) - -const ( - efiHardwareDevicePathNodePCI = 0x01 - - efiACPIDevicePathNodeNormal = 0x01 - - efiMsgDevicePathNodeLU = 0x11 - efiMsgDevicePathNodeSATA = 0x12 - - efiMediaDevicePathNodeHardDrive = 0x01 - efiMediaDevicePathNodeFilePath = 0x04 - efiMediaDevicePathNodeFvFile = 0x06 - efiMediaDevicePathNodeFv = 0x07 - efiMediaDevicePathNodeRelOffsetRange = 0x08 -) - -func firmwareDevicePathNodeToString(subType uint8, data []byte) (string, error) { - stream := bytes.NewReader(data) - - var name EFIGUID - if err := binary.Read(stream, binary.LittleEndian, &name); err != nil { - return "", err - } - - var builder bytes.Buffer - switch subType { - case efiMediaDevicePathNodeFvFile: - builder.WriteString("\\FvFile") - case efiMediaDevicePathNodeFv: - builder.WriteString("\\Fv") - default: - return "", fmt.Errorf("invalid sub type for firmware device path node: %d", subType) - } - - fmt.Fprintf(&builder, "(%s)", &name) - return builder.String(), nil -} - -func acpiDevicePathNodeToString(data []byte) (string, error) { - stream := bytes.NewReader(data) - - var hid uint32 - if err := binary.Read(stream, binary.LittleEndian, &hid); err != nil { - return "", err - } - - var uid uint32 - if err := binary.Read(stream, binary.LittleEndian, &uid); err != nil { - return "", err - } - - if hid&0xffff == 0x41d0 { - switch hid >> 16 { - case 0x0a03: - return fmt.Sprintf("\\PciRoot(0x%x)", uid), nil - case 0x0a08: - return fmt.Sprintf("\\PcieRoot(0x%x)", uid), nil - case 0x0604: - return fmt.Sprintf("\\Floppy(0x%x)", uid), nil - default: - return fmt.Sprintf("\\Acpi(PNP%04x,0x%x)", hid>>16, uid), nil - } - } else { - return fmt.Sprintf("\\Acpi(0x%08x,0x%x)", hid, uid), nil - } -} - -func pciDevicePathNodeToString(data []byte) (string, error) { - stream := bytes.NewReader(data) - - var function uint8 - if err := binary.Read(stream, binary.LittleEndian, &function); err != nil { - return "", err - } - - var device uint8 - if err := binary.Read(stream, binary.LittleEndian, &device); err != nil { - return "", err - } - - return fmt.Sprintf("\\Pci(0x%x,0x%x)", device, function), nil -} - -func luDevicePathNodeToString(data []byte) (string, error) { - stream := bytes.NewReader(data) - - var lun uint8 - if err := binary.Read(stream, binary.LittleEndian, &lun); err != nil { - return "", err - } - - return fmt.Sprintf("\\Unit(0x%x)", lun), nil -} - -func hardDriveDevicePathNodeToString(data []byte) (string, error) { - stream := bytes.NewReader(data) - - var partNumber uint32 - if err := binary.Read(stream, binary.LittleEndian, &partNumber); err != nil { - return "", err - } - - var partStart uint64 - if err := binary.Read(stream, binary.LittleEndian, &partStart); err != nil { - return "", err - } - - var partSize uint64 - if err := binary.Read(stream, binary.LittleEndian, &partSize); err != nil { - return "", err - } - - var sig [16]byte - if _, err := io.ReadFull(stream, sig[:]); err != nil { - return "", err - } - - var partFormat uint8 - if err := binary.Read(stream, binary.LittleEndian, &partFormat); err != nil { - return "", err - } - - var sigType uint8 - if err := binary.Read(stream, binary.LittleEndian, &sigType); err != nil { - return "", err - } - - var builder bytes.Buffer - - switch sigType { - case 0x01: - fmt.Fprintf(&builder, "\\HD(%d,MBR,0x%08x,", partNumber, binary.LittleEndian.Uint32(sig[:])) - case 0x02: - r := bytes.NewReader(sig[:]) - var guid EFIGUID - if err := binary.Read(r, binary.LittleEndian, &guid); err != nil { - return "", err - } - fmt.Fprintf(&builder, "\\HD(%d,GPT,%s,", partNumber, &guid) - default: - fmt.Fprintf(&builder, "\\HD(%d,%d,0,", partNumber, sigType) - } - - fmt.Fprintf(&builder, "0x%016x, 0x%016x)", partStart, partSize) - return builder.String(), nil -} - -func sataDevicePathNodeToString(data []byte) (string, error) { - stream := bytes.NewReader(data) - - var hbaPortNumber uint16 - if err := binary.Read(stream, binary.LittleEndian, &hbaPortNumber); err != nil { - return "", err - } - - var portMultiplierPortNumber uint16 - if err := binary.Read(stream, binary.LittleEndian, &portMultiplierPortNumber); err != nil { - return "", err - } - - var lun uint16 - if err := binary.Read(stream, binary.LittleEndian, &lun); err != nil { - return "", err - } - - return fmt.Sprintf("\\Sata(0x%x,0x%x,0x%x)", hbaPortNumber, portMultiplierPortNumber, lun), nil -} - -func filePathDevicePathNodeToString(data []byte) string { - u16 := make([]uint16, len(data)/2) - stream := bytes.NewReader(data) - binary.Read(stream, binary.LittleEndian, &u16) - - var buf bytes.Buffer - for _, r := range utf16.Decode(u16) { - buf.WriteRune(r) - } - return buf.String() -} - -func relOffsetRangePathNodeToString(data []byte) (string, error) { - stream := bytes.NewReader(data) - - if _, err := stream.Seek(4, io.SeekCurrent); err != nil { - return "", err - } - - var start uint64 - if err := binary.Read(stream, binary.LittleEndian, &start); err != nil { - return "", err - } - - var end uint64 - if err := binary.Read(stream, binary.LittleEndian, &end); err != nil { - return "", err - } - - return fmt.Sprintf("\\Offset(0x%x,0x%x)", start, end), nil -} - -func decodeDevicePathNode(stream io.Reader) (string, error) { - var t efiDevicePathNodeType - if err := binary.Read(stream, binary.LittleEndian, &t); err != nil { - return "", err - } - - if t == efiDevicePathNodeEoH { - return "", nil - } - - var subType uint8 - if err := binary.Read(stream, binary.LittleEndian, &subType); err != nil { - return "", err - } - - var length uint16 - if err := binary.Read(stream, binary.LittleEndian, &length); err != nil { - return "", err - } - - if length < 4 { - return "", fmt.Errorf("unexpected device path node length (got %d, expected >= 4)", length) - } - - data := make([]byte, length-4) - if _, err := io.ReadFull(stream, data); err != nil { - return "", err - } - - switch t { - case efiDevicePathNodeMedia: - switch subType { - case efiMediaDevicePathNodeFvFile: - fallthrough - case efiMediaDevicePathNodeFv: - return firmwareDevicePathNodeToString(subType, data) - case efiMediaDevicePathNodeHardDrive: - return hardDriveDevicePathNodeToString(data) - case efiMediaDevicePathNodeFilePath: - return filePathDevicePathNodeToString(data), nil - case efiMediaDevicePathNodeRelOffsetRange: - return relOffsetRangePathNodeToString(data) - } - case efiDevicePathNodeACPI: - switch subType { - case efiACPIDevicePathNodeNormal: - return acpiDevicePathNodeToString(data) - } - case efiDevicePathNodeHardware: - switch subType { - case efiHardwareDevicePathNodePCI: - return pciDevicePathNodeToString(data) - } - case efiDevicePathNodeMsg: - switch subType { - case efiMsgDevicePathNodeLU: - return luDevicePathNodeToString(data) - case efiMsgDevicePathNodeSATA: - return sataDevicePathNodeToString(data) - } - - } - - var builder bytes.Buffer - fmt.Fprintf(&builder, "\\%s(%d", t, subType) - if len(data) > 0 { - fmt.Fprintf(&builder, ", 0x") - for _, b := range data { - fmt.Fprintf(&builder, "%02x", b) - } - } - fmt.Fprintf(&builder, ")") - return builder.String(), nil -} - -func decodeDevicePath(data []byte) (string, error) { - stream := bytes.NewReader(data) - var builder bytes.Buffer - - for { - node, err := decodeDevicePathNode(stream) - if err != nil { - return "", err - } - if node == "" { - return builder.String(), nil - } - fmt.Fprintf(&builder, "%s", node) - } -} - -type efiImageLoadEventData struct { - data []byte - locationInMemory uint64 - lengthInMemory uint64 - linkTimeAddress uint64 - path string -} - -func (e *efiImageLoadEventData) String() string { - return fmt.Sprintf("UEFI_IMAGE_LOAD_EVENT{ ImageLocationInMemory: 0x%016x, ImageLengthInMemory: %d, "+ - "ImageLinkTimeAddress: 0x%016x, DevicePath: %s }", e.locationInMemory, e.lengthInMemory, - e.linkTimeAddress, e.path) -} - -func (e *efiImageLoadEventData) Bytes() []byte { - return e.data -} - -// https://trustedcomputinggroup.org/wp-content/uploads/TCG_EFI_Platform_1_22_Final_-v15.pdf (section 4 "Measuring PE/COFF Image Files") -// https://trustedcomputinggroup.org/wp-content/uploads/TCG_PCClientSpecPlat_TPM_2p0_1p04_pub.pdf (section 9.2.3 "UEFI_IMAGE_LOAD_EVENT Structure") -func decodeEventDataEFIImageLoadImpl(data []byte) (*efiImageLoadEventData, error) { - stream := bytes.NewReader(data) - - var locationInMemory uint64 - if err := binary.Read(stream, binary.LittleEndian, &locationInMemory); err != nil { - return nil, err - } - - var lengthInMemory uint64 - if err := binary.Read(stream, binary.LittleEndian, &lengthInMemory); err != nil { - return nil, err - } - - var linkTimeAddress uint64 - if err := binary.Read(stream, binary.LittleEndian, &linkTimeAddress); err != nil { - return nil, err - } - - var devicePathLength uint64 - if err := binary.Read(stream, binary.LittleEndian, &devicePathLength); err != nil { - return nil, err - } - - devicePathBuf := make([]byte, devicePathLength) - - if _, err := io.ReadFull(stream, devicePathBuf); err != nil { - return nil, err - } - - path, err := decodeDevicePath(devicePathBuf) - if err != nil { - return nil, err - } - - return &efiImageLoadEventData{data: data, - locationInMemory: locationInMemory, - lengthInMemory: lengthInMemory, - linkTimeAddress: linkTimeAddress, - path: path}, nil -} - -func decodeEventDataEFIImageLoad(data []byte) (out EventData, trailingBytes int, err error) { - d, err := decodeEventDataEFIImageLoadImpl(data) - if d != nil { - out = d - } - return -} - -type efiGPTPartitionEntry struct { - typeGUID EFIGUID - uniqueGUID EFIGUID - name string -} - -func (p *efiGPTPartitionEntry) String() string { - return fmt.Sprintf("PartitionTypeGUID: %s, UniquePartitionGUID: %s, Name: \"%s\"", - &p.typeGUID, &p.uniqueGUID, p.name) -} - -type efiGPTEventData struct { - data []byte - diskGUID EFIGUID - partitions []efiGPTPartitionEntry -} - -func (e *efiGPTEventData) String() string { - var builder bytes.Buffer - fmt.Fprintf(&builder, "UEFI_GPT_DATA{ DiskGUID: %s, Partitions: [", &e.diskGUID) - for i, part := range e.partitions { - if i > 0 { - fmt.Fprintf(&builder, ", ") - } - fmt.Fprintf(&builder, "{ %s }", &part) - } - fmt.Fprintf(&builder, "] }") - return builder.String() -} - -func (e *efiGPTEventData) Bytes() []byte { - return e.data -} - -func decodeEventDataEFIGPTImpl(data []byte) (*efiGPTEventData, int, error) { - stream := bytes.NewReader(data) - - // Skip UEFI_GPT_DATA.UEFIPartitionHeader.{Header, MyLBA, AlternateLBA, FirstUsableLBA, LastUsableLBA} - if _, err := stream.Seek(56, io.SeekCurrent); err != nil { - return nil, 0, err - } - - // UEFI_GPT_DATA.UEFIPartitionHeader.DiskGUID - var diskGUID EFIGUID - if err := binary.Read(stream, binary.LittleEndian, &diskGUID); err != nil { - return nil, 0, err - } - - // Skip UEFI_GPT_DATA.UEFIPartitionHeader.{PartitionEntryLBA, NumberOfPartitionEntries} - if _, err := stream.Seek(12, io.SeekCurrent); err != nil { - return nil, 0, err - } - - // UEFI_GPT_DATA.UEFIPartitionHeader.SizeOfPartitionEntry - var partEntrySize uint32 - if err := binary.Read(stream, binary.LittleEndian, &partEntrySize); err != nil { - return nil, 0, err - } - - // Skip UEFI_GPT_DATA.UEFIPartitionHeader.PartitionEntryArrayCRC32 - if _, err := stream.Seek(4, io.SeekCurrent); err != nil { - return nil, 0, err - } - - // UEFI_GPT_DATA.NumberOfPartitions - var numberOfParts uint64 - if err := binary.Read(stream, binary.LittleEndian, &numberOfParts); err != nil { - return nil, 0, err - } - - eventData := &efiGPTEventData{diskGUID: diskGUID, partitions: make([]efiGPTPartitionEntry, numberOfParts)} - - for i := uint64(0); i < numberOfParts; i++ { - entryData := make([]byte, partEntrySize) - if _, err := io.ReadFull(stream, entryData); err != nil { - return nil, 0, err - } - - entryStream := bytes.NewReader(entryData) - - var typeGUID EFIGUID - if err := binary.Read(entryStream, binary.LittleEndian, &typeGUID); err != nil { - return nil, 0, err - } - - var uniqueGUID EFIGUID - if err := binary.Read(entryStream, binary.LittleEndian, &uniqueGUID); err != nil { - return nil, 0, err - } - - // Skip UEFI_GPT_DATA.Partitions[i].{StartingLBA, EndingLBA, Attributes} - if _, err := entryStream.Seek(24, io.SeekCurrent); err != nil { - return nil, 0, err - } - - nameUtf16 := make([]uint16, entryStream.Len()/2) - if err := binary.Read(entryStream, binary.LittleEndian, &nameUtf16); err != nil { - return nil, 0, err - } - - var name bytes.Buffer - for _, r := range utf16.Decode(nameUtf16) { - if r == rune(0) { - break - } - name.WriteRune(r) - } - - eventData.partitions[i] = efiGPTPartitionEntry{typeGUID: typeGUID, uniqueGUID: uniqueGUID, name: name.String()} - } - - return eventData, stream.Len(), nil -} - -func decodeEventDataEFIGPT(data []byte) (out EventData, trailingBytes int, err error) { - d, trailingBytes, err := decodeEventDataEFIGPTImpl(data) - if d != nil { - out = d - } - return -} diff -Nru snapd-2.47.1+20.10.1build1/vendor/github.com/chrisccoulson/tcglog-parser/eventdata.go snapd-2.48+21.04/vendor/github.com/chrisccoulson/tcglog-parser/eventdata.go --- snapd-2.47.1+20.10.1build1/vendor/github.com/chrisccoulson/tcglog-parser/eventdata.go 2020-04-06 16:55:41.000000000 +0000 +++ snapd-2.48+21.04/vendor/github.com/chrisccoulson/tcglog-parser/eventdata.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,82 +0,0 @@ -package tcglog - -import ( - "fmt" - "io" -) - -// EventData is an interface that represents all event data types that appear in a log. Most implementations of -// this are private to this module. -type EventData interface { - String() string // Textual representation of the event data - Bytes() []byte // The raw event data bytes -} - -// BrokenEventData corresponds to an event data buffer that could not be parsed correctly, for the reason -// described by Error. -type BrokenEventData struct { - data []byte - Error error -} - -func (e *BrokenEventData) String() string { - if e.Error == io.ErrUnexpectedEOF { - return "Invalid event data: event data smaller than expected" - } - return fmt.Sprintf("Invalid event data: %v", e.Error) -} - -func (e *BrokenEventData) Bytes() []byte { - return e.data -} - -type opaqueEventData struct { - data []byte -} - -func (e *opaqueEventData) String() string { - return "" -} - -func (e *opaqueEventData) Bytes() []byte { - return e.data -} - -func decodeEventDataImpl(pcrIndex PCRIndex, eventType EventType, data []byte, options *LogOptions, - hasDigestOfSeparatorError bool) (EventData, int, error) { - switch { - case options.EnableGrub && (pcrIndex == 8 || pcrIndex == 9): - if d, n := decodeEventDataGRUB(pcrIndex, eventType, data); d != nil { - return d, n, nil - } - fallthrough - case options.EnableSystemdEFIStub && pcrIndex == options.SystemdEFIStubPCR && eventType == EventTypeIPL: - if d, n, e := decodeEventDataSystemdEFIStub(data); d != nil { - return d, n, nil - } else if e != nil { - return nil, 0, e - } - fallthrough - default: - return decodeEventDataTCG(eventType, data, hasDigestOfSeparatorError) - } -} - -func decodeEventData(pcrIndex PCRIndex, eventType EventType, data []byte, options *LogOptions, - hasDigestOfSeparatorError bool) (EventData, int) { - event, trailingBytes, err := - decodeEventDataImpl(pcrIndex, eventType, data, options, hasDigestOfSeparatorError) - - if err != nil { - if err == io.EOF { - err = io.ErrUnexpectedEOF - } - return &BrokenEventData{data: data, Error: err}, 0 - } - - if event != nil { - return event, trailingBytes - } - - return &opaqueEventData{data: data}, 0 -} diff -Nru snapd-2.47.1+20.10.1build1/vendor/github.com/chrisccoulson/tcglog-parser/grubeventdata.go snapd-2.48+21.04/vendor/github.com/chrisccoulson/tcglog-parser/grubeventdata.go --- snapd-2.47.1+20.10.1build1/vendor/github.com/chrisccoulson/tcglog-parser/grubeventdata.go 2020-04-06 16:55:41.000000000 +0000 +++ snapd-2.48+21.04/vendor/github.com/chrisccoulson/tcglog-parser/grubeventdata.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,73 +0,0 @@ -package tcglog - -import ( - "fmt" - "io" - "strings" -) - -var ( - kernelCmdlinePrefix = "kernel_cmdline: " - grubCmdPrefix = "grub_cmd: " -) - -type GrubStringEventType int - -const ( - GrubCmd GrubStringEventType = iota - KernelCmdline -) - -func grubEventTypeString(t GrubStringEventType) string { - switch t { - case GrubCmd: - return "grub_cmd" - case KernelCmdline: - return "kernel_cmdline" - } - panic("invalid value") -} - -type GrubStringEventData struct { - data []byte - Type GrubStringEventType - Str string -} - -func (e *GrubStringEventData) String() string { - return fmt.Sprintf("%s{ %s }", grubEventTypeString(e.Type), e.Str) -} - -func (e *GrubStringEventData) Bytes() []byte { - return e.data -} - -func (e *GrubStringEventData) EncodeMeasuredBytes(buf io.Writer) error { - if _, err := io.WriteString(buf, e.Str); err != nil { - return err - } - return nil -} - -func decodeEventDataGRUB(pcrIndex PCRIndex, eventType EventType, data []byte) (EventData, int) { - if eventType != EventTypeIPL { - return nil, 0 - } - - switch pcrIndex { - case 8: - str := string(data) - switch { - case strings.HasPrefix(str, kernelCmdlinePrefix): - return &GrubStringEventData{data, KernelCmdline, strings.TrimSuffix(strings.TrimPrefix(str, kernelCmdlinePrefix), "\x00")}, 0 - case strings.HasPrefix(str, grubCmdPrefix): - return &GrubStringEventData{data, GrubCmd, strings.TrimSuffix(strings.TrimPrefix(str, grubCmdPrefix), "\x00")}, 0 - default: - return nil, 0 - } - case 9: - return &asciiStringEventData{data: data}, 0 - default: - panic("unhandled PCR index") - } -} diff -Nru snapd-2.47.1+20.10.1build1/vendor/github.com/chrisccoulson/tcglog-parser/log.go snapd-2.48+21.04/vendor/github.com/chrisccoulson/tcglog-parser/log.go --- snapd-2.47.1+20.10.1build1/vendor/github.com/chrisccoulson/tcglog-parser/log.go 2020-04-06 16:55:41.000000000 +0000 +++ snapd-2.48+21.04/vendor/github.com/chrisccoulson/tcglog-parser/log.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,322 +0,0 @@ -package tcglog - -import ( - "bytes" - "encoding/binary" - "errors" - "fmt" - "io" -) - -// LogOptions allows the behaviour of Log to be controlled. -type LogOptions struct { - EnableGrub bool // Enable support for interpreting events recorded by GRUB - EnableSystemdEFIStub bool // Enable support for interpreting events recorded by systemd's EFI linux loader stub - SystemdEFIStubPCR PCRIndex // Specify the PCR that systemd's EFI linux loader stub measures to -} - -var zeroDigests = map[AlgorithmId][]byte{ - AlgorithmSha1: make([]byte, AlgorithmSha1.size()), - AlgorithmSha256: make([]byte, AlgorithmSha256.size()), - AlgorithmSha384: make([]byte, AlgorithmSha384.size()), - AlgorithmSha512: make([]byte, AlgorithmSha512.size())} - -type stream interface { - readNextEvent() (*Event, int, error) -} - -func isPCRIndexInRange(index PCRIndex) bool { - const maxPCRIndex PCRIndex = 31 - return index <= maxPCRIndex -} - -// https://trustedcomputinggroup.org/wp-content/uploads/TCG_PCClientImplementation_1-21_1_00.pdf -// (section 3.3.2.2 2 Error Conditions" , section 8.2.3 "Measuring Boot Events") -// https://trustedcomputinggroup.org/wp-content/uploads/PC-ClientSpecific_Platform_Profile_for_TPM_2p0_Systems_v51.pdf: -// (section 2.3.2 "Error Conditions", section 2.3.4 "PCR Usage", section 7.2 -// "Procedure for Pre-OS to OS-Present Transition") -func isDigestOfSeparatorErrorValue(digest Digest, alg AlgorithmId) bool { - errorValue := make([]byte, 4) - binary.LittleEndian.PutUint32(errorValue, separatorEventErrorValue) - - return bytes.Compare(digest, alg.hash(errorValue)) == 0 -} - -func wrapLogReadError(origErr error, partial bool) error { - if origErr == io.EOF { - if !partial { - return origErr - } - origErr = io.ErrUnexpectedEOF - } - - return fmt.Errorf("error when reading from log stream (%v)", origErr) -} - -func wrapPCRIndexOutOfRangeError(pcrIndex PCRIndex) error { - return fmt.Errorf("log entry has an out-of-range PCR index (%d)", pcrIndex) -} - -type eventHeader_1_2 struct { - PCRIndex PCRIndex - EventType EventType -} - -type stream_1_2 struct { - r io.ReadSeeker - options LogOptions -} - -// https://trustedcomputinggroup.org/wp-content/uploads/TCG_PCClientImplementation_1-21_1_00.pdf -// (section 11.1.1 "TCG_PCClientPCREventStruct Structure") -func (s *stream_1_2) readNextEvent() (*Event, int, error) { - var header eventHeader_1_2 - if err := binary.Read(s.r, binary.LittleEndian, &header); err != nil { - return nil, 0, wrapLogReadError(err, false) - } - - if !isPCRIndexInRange(header.PCRIndex) { - return nil, 0, wrapPCRIndexOutOfRangeError(header.PCRIndex) - } - - digest := make(Digest, AlgorithmSha1.size()) - if _, err := s.r.Read(digest); err != nil { - return nil, 0, wrapLogReadError(err, true) - } - digests := make(DigestMap) - digests[AlgorithmSha1] = digest - - var eventSize uint32 - if err := binary.Read(s.r, binary.LittleEndian, &eventSize); err != nil { - return nil, 0, wrapLogReadError(err, true) - } - - event := make([]byte, eventSize) - if _, err := io.ReadFull(s.r, event); err != nil { - return nil, 0, wrapLogReadError(err, true) - } - - data, trailing := decodeEventData(header.PCRIndex, header.EventType, event, &s.options, - isDigestOfSeparatorErrorValue(digest, AlgorithmSha1)) - - return &Event{ - PCRIndex: header.PCRIndex, - EventType: header.EventType, - Digests: digests, - Data: data, - }, trailing, nil -} - -type eventHeader_2 struct { - PCRIndex PCRIndex - EventType EventType - Count uint32 -} - -type stream_2 struct { - r io.ReadSeeker - options LogOptions - algSizes []EFISpecIdEventAlgorithmSize - readFirstEvent bool -} - -// https://trustedcomputinggroup.org/wp-content/uploads/TCG_PCClientSpecPlat_TPM_2p0_1p04_pub.pdf -// (section 9.2.2 "TCG_PCR_EVENT2 Structure") -func (s *stream_2) readNextEvent() (*Event, int, error) { - if !s.readFirstEvent { - s.readFirstEvent = true - stream := stream_1_2{r: s.r} - return stream.readNextEvent() - } - - var header eventHeader_2 - if err := binary.Read(s.r, binary.LittleEndian, &header); err != nil { - return nil, 0, wrapLogReadError(err, false) - } - - if !isPCRIndexInRange(header.PCRIndex) { - return nil, 0, wrapPCRIndexOutOfRangeError(header.PCRIndex) - } - - digests := make(DigestMap) - - for i := uint32(0); i < header.Count; i++ { - var algorithmId AlgorithmId - if err := binary.Read(s.r, binary.LittleEndian, &algorithmId); err != nil { - return nil, 0, wrapLogReadError(err, true) - } - - var digestSize uint16 - var j int - for j = 0; j < len(s.algSizes); j++ { - if s.algSizes[j].AlgorithmId == algorithmId { - digestSize = s.algSizes[j].DigestSize - break - } - } - - if j == len(s.algSizes) { - return nil, 0, fmt.Errorf("crypto-agile log entry contains a digest for an unrecognized "+ - "algorithm (%s)", algorithmId) - } - - digest := make(Digest, digestSize) - if _, err := io.ReadFull(s.r, digest); err != nil { - return nil, 0, wrapLogReadError(err, true) - } - - if _, exists := digests[algorithmId]; exists { - return nil, 0, fmt.Errorf("crypto-agile log entry contains more than one digest value "+ - "for algorithm %s", algorithmId) - } - digests[algorithmId] = digest - } - - for _, algSize := range s.algSizes { - if _, exists := digests[algSize.AlgorithmId]; !exists { - return nil, 0, - fmt.Errorf("crypto-agile log entry is missing a digest value for algorithm %s "+ - "that was present in the Spec ID Event", algSize.AlgorithmId) - } - } - - for alg, _ := range digests { - if alg.supported() { - continue - } - delete(digests, alg) - } - - var eventSize uint32 - if err := binary.Read(s.r, binary.LittleEndian, &eventSize); err != nil { - return nil, 0, wrapLogReadError(err, true) - } - - event := make([]byte, eventSize) - if _, err := io.ReadFull(s.r, event); err != nil { - return nil, 0, wrapLogReadError(err, true) - } - - data, trailing := decodeEventData(header.PCRIndex, header.EventType, event, &s.options, - isDigestOfSeparatorErrorValue(digests[s.algSizes[0].AlgorithmId], s.algSizes[0].AlgorithmId)) - - return &Event{ - PCRIndex: header.PCRIndex, - EventType: header.EventType, - Digests: digests, - Data: data, - }, trailing, nil -} - -func fixupSpecIdEvent(event *Event, algorithms AlgorithmIdList) { - if event.Data.(*SpecIdEventData).Spec != SpecEFI_2 { - return - } - - for _, alg := range algorithms { - if alg == AlgorithmSha1 { - continue - } - - if _, ok := event.Digests[alg]; ok { - continue - } - - event.Digests[alg] = zeroDigests[alg] - } -} - -func isSpecIdEvent(event *Event) (out bool) { - _, out = event.Data.(*SpecIdEventData) - return -} - -// Log corresponds to an event log parser instance, and allows the consumer to iterate over log entries. -type Log struct { - Spec Spec // The specification to which this log conforms - Algorithms AlgorithmIdList // The digest algorithms that appear in the log - stream stream - failed bool - indexTracker map[PCRIndex]uint -} - -func (l *Log) nextEventInternal() (*Event, int, error) { - if l.failed { - return nil, 0, - errors.New("cannot read next event: log status inconsistent due to a previous error") - } - - event, trailing, err := l.stream.readNextEvent() - if err != nil { - if err != io.EOF { - l.failed = true - } - return nil, 0, err - } - - if i, exists := l.indexTracker[event.PCRIndex]; exists { - event.Index = i - l.indexTracker[event.PCRIndex] = i + 1 - } else { - event.Index = 0 - l.indexTracker[event.PCRIndex] = 1 - } - - if isSpecIdEvent(event) { - fixupSpecIdEvent(event, l.Algorithms) - } - - return event, trailing, nil -} - -// NextEvent returns an Event structure that corresponds to the next event in the log. Upon successful completion, -// the Log instance will advance to the next event. If there are no more events in the log, it will return io.EOF. -func (l *Log) NextEvent() (event *Event, err error) { - event, _, err = l.nextEventInternal() - return -} - -// NewLog creates a new Log instance that reads an event log from r -func NewLog(r io.ReaderAt, options LogOptions) (*Log, error) { - var stream stream = &stream_1_2{r: io.NewSectionReader(r, 0, (1<<63)-1), options: options} - event, _, err := stream.readNextEvent() - if err != nil { - return nil, wrapLogReadError(err, true) - } - - var spec Spec = SpecUnknown - var digestSizes []EFISpecIdEventAlgorithmSize - var algorithms AlgorithmIdList - - switch d := event.Data.(type) { - case *SpecIdEventData: - spec = d.Spec - digestSizes = d.DigestSizes - case *BrokenEventData: - if _, isSpecErr := d.Error.(invalidSpecIdEventError); isSpecErr { - return nil, d.Error - } - } - - if spec == SpecEFI_2 { - algorithms = make(AlgorithmIdList, 0, len(digestSizes)) - for _, specAlgSize := range digestSizes { - if specAlgSize.AlgorithmId.supported() { - algorithms = append(algorithms, specAlgSize.AlgorithmId) - } - } - stream = &stream_2{r: io.NewSectionReader(r, 0, (1<<63)-1), - options: options, - algSizes: digestSizes, - readFirstEvent: false} - } else { - algorithms = AlgorithmIdList{AlgorithmSha1} - stream.(*stream_1_2).r.Seek(0, io.SeekStart) - } - - return &Log{Spec: spec, - Algorithms: algorithms, - stream: stream, - failed: false, - indexTracker: map[PCRIndex]uint{}}, nil -} diff -Nru snapd-2.47.1+20.10.1build1/vendor/github.com/chrisccoulson/tcglog-parser/README.md snapd-2.48+21.04/vendor/github.com/chrisccoulson/tcglog-parser/README.md --- snapd-2.47.1+20.10.1build1/vendor/github.com/chrisccoulson/tcglog-parser/README.md 2020-04-06 16:55:41.000000000 +0000 +++ snapd-2.48+21.04/vendor/github.com/chrisccoulson/tcglog-parser/README.md 1970-01-01 00:00:00.000000000 +0000 @@ -1,12 +0,0 @@ -# TCG Log Parser - -This repository contains a go library for parsing TCG event logs. Also included is a simple command line tool that prints details of log entries to the console. - -## Relevant specifications - -The library parses data in a format that is defined by the following specifications: -* [TCG PC Client Platform Firmware Profile Specification](https://trustedcomputinggroup.org/wp-content/uploads/TCG_PCClientSpecPlat_TPM_2p0_1p04_pub.pdf) -* [TCG EFI Platform Specification For TPM Family 1.1 or 1.2](https://trustedcomputinggroup.org/wp-content/uploads/TCG_EFI_Platform_1_22_Final_-v15.pdf) -* [TCG PC Client Specific Implementation Specification for Conventional BIOS](https://trustedcomputinggroup.org/wp-content/uploads/TCG_PCClientImplementation_1-21_1_00.pdf) -* [Unified Extensible Firmware Interface (UEFI) Specification](https://uefi.org/sites/default/files/resources/UEFI_Spec_2_8_final.pdf) -* [Platform Initialization (PI) Specification](https://uefi.org/sites/default/files/resources/PI_Spec_1_6.pdf) diff -Nru snapd-2.47.1+20.10.1build1/vendor/github.com/chrisccoulson/tcglog-parser/sdefistub.go snapd-2.48+21.04/vendor/github.com/chrisccoulson/tcglog-parser/sdefistub.go --- snapd-2.47.1+20.10.1build1/vendor/github.com/chrisccoulson/tcglog-parser/sdefistub.go 2020-04-06 16:55:41.000000000 +0000 +++ snapd-2.48+21.04/vendor/github.com/chrisccoulson/tcglog-parser/sdefistub.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,36 +0,0 @@ -package tcglog - -import ( - "bytes" - "encoding/binary" - "fmt" - "io" -) - -type SystemdEFIStubEventData struct { - data []byte - Str string -} - -func (e *SystemdEFIStubEventData) String() string { - return fmt.Sprintf("%s", e.Str) -} - -func (e *SystemdEFIStubEventData) Bytes() []byte { - return e.data -} - -func (e *SystemdEFIStubEventData) EncodeMeasuredBytes(buf io.Writer) error { - return binary.Write(buf, binary.LittleEndian, append(convertStringToUtf16(e.Str), 0)) -} - -func decodeEventDataSystemdEFIStub(data []byte) (EventData, int, error) { - // data is a UTF-16 string in little-endian form terminated with a single zero byte. - // Omit the zero byte added by the EFI stub and then convert to native byte order. - reader := bytes.NewReader(data[:len(data)-1]) - - utf16Str := make([]uint16, len(data)/2) - binary.Read(reader, binary.LittleEndian, &utf16Str) - - return &SystemdEFIStubEventData{data: data, Str: convertUtf16ToString(utf16Str)}, 0, nil -} diff -Nru snapd-2.47.1+20.10.1build1/vendor/github.com/chrisccoulson/tcglog-parser/tcgeventdata.go snapd-2.48+21.04/vendor/github.com/chrisccoulson/tcglog-parser/tcgeventdata.go --- snapd-2.47.1+20.10.1build1/vendor/github.com/chrisccoulson/tcglog-parser/tcgeventdata.go 2020-04-06 16:55:41.000000000 +0000 +++ snapd-2.48+21.04/vendor/github.com/chrisccoulson/tcglog-parser/tcgeventdata.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,287 +0,0 @@ -package tcglog - -import ( - "bytes" - "encoding/binary" - "fmt" - "io" - "math" - "unsafe" -) - -type invalidSpecIdEventError struct { - s string -} - -func (e invalidSpecIdEventError) Error() string { - return fmt.Sprintf("invalid SpecIdEvent (%s)", e.s) -} - -// EFISpecIdEventAlgorithmSize represents a digest algorithm and its length. -type EFISpecIdEventAlgorithmSize struct { - AlgorithmId AlgorithmId - DigestSize uint16 -} - -type NoActionEventType int - -const ( - UnknownNoActionEvent NoActionEventType = iota - SpecId - StartupLocality - BiosIntegrityMeasurement -) - -type NoActionEventData interface { - Type() NoActionEventType -} - -// SpecIdEventData corresponds to the event data for a Specification ID Version event -// (TCG_PCClientSpecIdEventStruct, TCG_EfiSpecIdEventStruct, TCG_EfiSpecIdEvent) -type SpecIdEventData struct { - data []byte - Spec Spec - PlatformClass uint32 - SpecVersionMinor uint8 - SpecVersionMajor uint8 - SpecErrata uint8 - UintnSize uint8 - DigestSizes []EFISpecIdEventAlgorithmSize // The digest algorithms contained within this log - VendorInfo []byte -} - -func (e *SpecIdEventData) String() string { - var builder bytes.Buffer - switch e.Spec { - case SpecPCClient: - builder.WriteString("PCClientSpecIdEvent") - case SpecEFI_1_2, SpecEFI_2: - builder.WriteString("EfiSpecIDEvent") - } - - fmt.Fprintf(&builder, "{ spec=%d, platformClass=%d, specVersionMinor=%d, specVersionMajor=%d, "+ - "specErrata=%d", e.Spec, e.PlatformClass, e.SpecVersionMinor, e.SpecVersionMajor, e.SpecErrata) - if e.Spec == SpecEFI_2 { - builder.WriteString(", digestSizes=[") - for i, algSize := range e.DigestSizes { - if i > 0 { - builder.WriteString(", ") - } - fmt.Fprintf(&builder, "{ algorithmId=0x%04x, digestSize=%d }", - uint16(algSize.AlgorithmId), algSize.DigestSize) - } - builder.WriteString("]") - } - builder.WriteString(" }") - return builder.String() -} - -func (e *SpecIdEventData) Bytes() []byte { - return e.data -} - -func (e *SpecIdEventData) Type() NoActionEventType { - return SpecId -} - -func wrapSpecIdEventReadError(origErr error) error { - if origErr == io.EOF { - return invalidSpecIdEventError{"not enough data"} - } - - return invalidSpecIdEventError{origErr.Error()} -} - -// https://trustedcomputinggroup.org/wp-content/uploads/TCG_PCClientImplementation_1-21_1_00.pdf -// (section 11.3.4.1 "Specification Event") -func parsePCClientSpecIdEvent(stream io.Reader, eventData *SpecIdEventData) error { - eventData.Spec = SpecPCClient - - // TCG_PCClientSpecIdEventStruct.vendorInfoSize - var vendorInfoSize uint8 - if err := binary.Read(stream, binary.LittleEndian, &vendorInfoSize); err != nil { - return wrapSpecIdEventReadError(err) - } - - // TCG_PCClientSpecIdEventStruct.vendorInfo - eventData.VendorInfo = make([]byte, vendorInfoSize) - if _, err := io.ReadFull(stream, eventData.VendorInfo); err != nil { - return wrapSpecIdEventReadError(err) - } - - return nil -} - -type specIdEventCommon struct { - PlatformClass uint32 - SpecVersionMinor uint8 - SpecVersionMajor uint8 - SpecErrata uint8 - UintnSize uint8 -} - -// https://trustedcomputinggroup.org/wp-content/uploads/TCG_PCClientImplementation_1-21_1_00.pdf -// (section 11.3.4.1 "Specification Event") -// https://trustedcomputinggroup.org/wp-content/uploads/TCG_EFI_Platform_1_22_Final_-v15.pdf -// (section 7.4 "EV_NO_ACTION Event Types") -// https://trustedcomputinggroup.org/wp-content/uploads/TCG_PCClientSpecPlat_TPM_2p0_1p04_pub.pdf -// (secion 9.4.5.1 "Specification ID Version Event") -func decodeSpecIdEvent(stream io.Reader, data []byte, helper func(io.Reader, *SpecIdEventData) error) (*SpecIdEventData, error) { - var common struct{ - PlatformClass uint32 - SpecVersionMinor uint8 - SpecVersionMajor uint8 - SpecErrata uint8 - UintnSize uint8 - } - if err := binary.Read(stream, binary.LittleEndian, &common); err != nil { - return nil, wrapSpecIdEventReadError(err) - } - - eventData := &SpecIdEventData{ - data: data, - PlatformClass: common.PlatformClass, - SpecVersionMinor: common.SpecVersionMinor, - SpecVersionMajor: common.SpecVersionMajor, - SpecErrata: common.SpecErrata, - UintnSize: common.UintnSize} - - if err := helper(stream, eventData); err != nil { - return nil, err - } - - return eventData, nil -} - -var ( - validNormalSeparatorValues = [...]uint32{0, math.MaxUint32} -) - -type asciiStringEventData struct { - data []byte -} - -func (e *asciiStringEventData) String() string { - return *(*string)(unsafe.Pointer(&e.data)) -} - -func (e *asciiStringEventData) Bytes() []byte { - return e.data -} - -type unknownNoActionEventData struct { - data []byte -} - -func (e *unknownNoActionEventData) String() string { - return "" -} - -func (e *unknownNoActionEventData) Bytes() []byte { - return e.data -} - -func (e *unknownNoActionEventData) Type() NoActionEventType { - return UnknownNoActionEvent -} - -// https://trustedcomputinggroup.org/wp-content/uploads/TCG_PCClientImplementation_1-21_1_00.pdf -// (section 11.3.4 "EV_NO_ACTION Event Types") -// https://trustedcomputinggroup.org/wp-content/uploads/TCG_PCClientSpecPlat_TPM_2p0_1p04_pub.pdf -// (section 9.4.5 "EV_NO_ACTION Event Types") -func decodeEventDataNoAction(data []byte) (out EventData, trailingBytes int, err error) { - stream := bytes.NewReader(data) - - // Signature field - signature := make([]byte, 16) - if _, err := io.ReadFull(stream, signature); err != nil { - return nil, 0, err - } - - switch *(*string)(unsafe.Pointer(&signature)) { - case "Spec ID Event00\x00": - d, e := decodeSpecIdEvent(stream, data, parsePCClientSpecIdEvent) - if d != nil { - out = d - } - err = e - case "Spec ID Event02\x00": - d, e := decodeSpecIdEvent(stream, data, parseEFI_1_2_SpecIdEvent) - if d != nil { - out = d - } - err = e - case "Spec ID Event03\x00": - d, e := decodeSpecIdEvent(stream, data, parseEFI_2_SpecIdEvent) - if d != nil { - out = d - } - err = e - case "SP800-155 Event\x00": - d, e := decodeBIMReferenceManifestEvent(stream, data) - if d != nil { - out = d - } - err = e - case "StartupLocality\x00": - d, e := decodeStartupLocalityEvent(stream, data) - if d != nil { - out = d - } - err = e - default: - return &unknownNoActionEventData{data}, 0, nil - } - - return -} - -// https://trustedcomputinggroup.org/wp-content/uploads/TCG_PCClientImplementation_1-21_1_00.pdf (section 11.3.3 "EV_ACTION event types") -// https://trustedcomputinggroup.org/wp-content/uploads/PC-ClientSpecific_Platform_Profile_for_TPM_2p0_Systems_v51.pdf (section 9.4.3 "EV_ACTION Event Types") -func decodeEventDataAction(data []byte) (*asciiStringEventData, int, error) { - return &asciiStringEventData{data: data}, 0, nil -} - -type separatorEventData struct { - data []byte - isError bool -} - -func (e *separatorEventData) String() string { - if !e.isError { - return "" - } - return "*ERROR*" -} - -func (e *separatorEventData) Bytes() []byte { - return e.data -} - -func decodeEventDataSeparator(data []byte, isError bool) (*separatorEventData, int, error) { - return &separatorEventData{data: data, isError: isError}, 0, nil -} - -// https://trustedcomputinggroup.org/wp-content/uploads/TCG_PCClientImplementation_1-21_1_00.pdf (section 11.3.1 "Event Types") -// https://trustedcomputinggroup.org/wp-content/uploads/TCG_EFI_Platform_1_22_Final_-v15.pdf (section 7.2 "Event Types") -// https://trustedcomputinggroup.org/wp-content/uploads/TCG_PCClientSpecPlat_TPM_2p0_1p04_pub.pdf (section 9.4.1 "Event Types") -func decodeEventDataTCG(eventType EventType, data []byte, - hasDigestOfSeparatorError bool) (out EventData, trailingBytes int, err error) { - switch eventType { - case EventTypeNoAction: - return decodeEventDataNoAction(data) - case EventTypeSeparator: - return decodeEventDataSeparator(data, hasDigestOfSeparatorError) - case EventTypeAction, EventTypeEFIAction: - return decodeEventDataAction(data) - case EventTypeEFIVariableDriverConfig, EventTypeEFIVariableBoot, EventTypeEFIVariableAuthority: - return decodeEventDataEFIVariable(data, eventType) - case EventTypeEFIBootServicesApplication, EventTypeEFIBootServicesDriver, - EventTypeEFIRuntimeServicesDriver: - return decodeEventDataEFIImageLoad(data) - case EventTypeEFIGPTEvent: - return decodeEventDataEFIGPT(data) - default: - } - return nil, 0, nil -} diff -Nru snapd-2.47.1+20.10.1build1/vendor/github.com/chrisccoulson/tcglog-parser/types.go snapd-2.48+21.04/vendor/github.com/chrisccoulson/tcglog-parser/types.go --- snapd-2.47.1+20.10.1build1/vendor/github.com/chrisccoulson/tcglog-parser/types.go 2020-04-06 16:55:41.000000000 +0000 +++ snapd-2.48+21.04/vendor/github.com/chrisccoulson/tcglog-parser/types.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,182 +0,0 @@ -package tcglog - -import ( - "crypto" - _ "crypto/sha1" - _ "crypto/sha256" - _ "crypto/sha512" - "fmt" - "hash" -) - -// Spec corresponds to the TCG specification that an event log conforms to. -type Spec uint - -// PCRIndex corresponds to the index of a PCR on the TPM. -type PCRIndex uint32 - -// EventType corresponds to the type of an event in an event log. -type EventType uint32 - -// AlgorithmId corresponds to the algorithm of digests that appear in the log. The values are in sync with those -// in the TPM Library Specification for the TPM_ALG_ID type. -// See https://trustedcomputinggroup.org/wp-content/uploads/TPM-Rev-2.0-Part-2-Structures-01.38.pdf (Table 9) -type AlgorithmId uint16 - -func (a AlgorithmId) getHash() crypto.Hash { - switch a { - case AlgorithmSha1: - return crypto.SHA1 - case AlgorithmSha256: - return crypto.SHA256 - case AlgorithmSha384: - return crypto.SHA384 - case AlgorithmSha512: - return crypto.SHA512 - default: - return 0 - } -} - -func (a AlgorithmId) supported() bool { - return a.getHash() != crypto.Hash(0) -} - -func (a AlgorithmId) size() int { - return a.getHash().Size() -} - -func (a AlgorithmId) newHash() hash.Hash { - return a.getHash().New() -} - -func (a AlgorithmId) hash(data []byte) []byte { - h := a.newHash() - h.Write(data) - return h.Sum(nil) -} - -// Digest is the result of hashing some data. -type Digest []byte - -// DigestMap is a map of algorithms to digests. -type DigestMap map[AlgorithmId]Digest - -func (e EventType) String() string { - switch e { - case EventTypePrebootCert: - return "EV_PREBOOT_CERT" - case EventTypePostCode: - return "EV_POST_CODE" - case EventTypeNoAction: - return "EV_NO_ACTION" - case EventTypeSeparator: - return "EV_SEPARATOR" - case EventTypeAction: - return "EV_ACTION" - case EventTypeEventTag: - return "EV_EVENT_TAG" - case EventTypeSCRTMContents: - return "EV_S_CRTM_CONTENTS" - case EventTypeSCRTMVersion: - return "EV_S_CRTM_VERSION" - case EventTypeCPUMicrocode: - return "EV_CPU_MICROCODE" - case EventTypePlatformConfigFlags: - return "EV_PLATFORM_CONFIG_FLAGS" - case EventTypeTableOfDevices: - return "EV_TABLE_OF_DEVICES" - case EventTypeCompactHash: - return "EV_COMPACT_HASH" - case EventTypeIPL: - return "EV_IPL" - case EventTypeIPLPartitionData: - return "EV_IPL_PARTITION_DATA" - case EventTypeNonhostCode: - return "EV_NONHOST_CODE" - case EventTypeNonhostConfig: - return "EV_NONHOST_CONFIG" - case EventTypeNonhostInfo: - return "EV_NONHOST_INFO" - case EventTypeOmitBootDeviceEvents: - return "EV_OMIT_BOOT_DEVICE_EVENTS" - case EventTypeEFIVariableDriverConfig: - return "EV_EFI_VARIABLE_DRIVER_CONFIG" - case EventTypeEFIVariableBoot: - return "EV_EFI_VARIABLE_BOOT" - case EventTypeEFIBootServicesApplication: - return "EV_EFI_BOOT_SERVICES_APPLICATION" - case EventTypeEFIBootServicesDriver: - return "EV_EFI_BOOT_SERVICES_DRIVER" - case EventTypeEFIRuntimeServicesDriver: - return "EV_EFI_RUNTIME_SERVICES_DRIVER" - case EventTypeEFIGPTEvent: - return "EF_EFI_GPT_EVENT" - case EventTypeEFIAction: - return "EV_EFI_ACTION" - case EventTypeEFIPlatformFirmwareBlob: - return "EV_EFI_PLATFORM_FIRMWARE_BLOB" - case EventTypeEFIHandoffTables: - return "EV_EFI_HANDOFF_TABLES" - case EventTypeEFIHCRTMEvent: - return "EV_EFI_HCRTM_EVENT" - case EventTypeEFIVariableAuthority: - return "EV_EFI_VARIABLE_AUTHORITY" - default: - return fmt.Sprintf("%08x", uint32(e)) - } -} - -func (e EventType) Format(s fmt.State, f rune) { - switch f { - case 's': - fmt.Fprintf(s, "%s", e.String()) - default: - fmt.Fprintf(s, makeDefaultFormatter(s, f), uint32(e)) - } -} - -func (a AlgorithmId) String() string { - switch a { - case AlgorithmSha1: - return "SHA-1" - case AlgorithmSha256: - return "SHA-256" - case AlgorithmSha384: - return "SHA-384" - case AlgorithmSha512: - return "SHA-512" - default: - return fmt.Sprintf("%04x", uint16(a)) - } -} - -func (a AlgorithmId) Format(s fmt.State, f rune) { - switch f { - case 's': - fmt.Fprintf(s, "%s", a.String()) - default: - fmt.Fprintf(s, makeDefaultFormatter(s, f), uint16(a)) - } -} - -// AlgorithmListId is a slice of AlgorithmId values, -type AlgorithmIdList []AlgorithmId - -func (l AlgorithmIdList) Contains(a AlgorithmId) bool { - for _, alg := range l { - if alg == a { - return true - } - } - return false -} - -// Event corresponds to a single event in an event log. -type Event struct { - Index uint // Sequential index of event in the log - PCRIndex PCRIndex // PCR index to which this event was measured - EventType EventType // The type of this event - Digests DigestMap // The digests corresponding to this event for the supported algorithms - Data EventData // The data recorded with this event -} diff -Nru snapd-2.47.1+20.10.1build1/vendor/github.com/chrisccoulson/tcglog-parser/utils.go snapd-2.48+21.04/vendor/github.com/chrisccoulson/tcglog-parser/utils.go --- snapd-2.47.1+20.10.1build1/vendor/github.com/chrisccoulson/tcglog-parser/utils.go 2020-04-06 16:55:41.000000000 +0000 +++ snapd-2.48+21.04/vendor/github.com/chrisccoulson/tcglog-parser/utils.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,84 +0,0 @@ -package tcglog - -import ( - "bytes" - "fmt" - "strconv" - "unicode/utf16" - "unicode/utf8" -) - -func makeDefaultFormatter(s fmt.State, f rune) string { - var builder bytes.Buffer - builder.WriteString("%%") - for _, flag := range [...]int{'+', '-', '#', ' ', '0'} { - if s.Flag(flag) { - fmt.Fprintf(&builder, "%c", flag) - } - } - if width, ok := s.Width(); ok { - fmt.Fprintf(&builder, "%d", width) - } - if prec, ok := s.Precision(); ok { - fmt.Fprintf(&builder, ".%d", prec) - } - fmt.Fprintf(&builder, "%c", f) - return builder.String() -} - -type PCRArgList []PCRIndex - -func (l *PCRArgList) String() string { - var builder bytes.Buffer - for i, pcr := range *l { - if i > 0 { - builder.WriteString(", ") - } - fmt.Fprintf(&builder, "%d", pcr) - } - return builder.String() -} - -func (l *PCRArgList) Set(value string) error { - v, err := strconv.ParseUint(value, 10, 32) - if err != nil { - return err - } - *l = append(*l, PCRIndex(v)) - return nil -} - -func ParseAlgorithm(alg string) (AlgorithmId, error) { - switch alg { - case "sha1": - return AlgorithmSha1, nil - case "sha256": - return AlgorithmSha256, nil - case "sha384": - return AlgorithmSha384, nil - case "sha512": - return AlgorithmSha512, nil - default: - return 0, fmt.Errorf("Unrecognized algorithm \"%s\"", alg) - } -} - -func convertStringToUtf16(str string) []uint16 { - var unicodePoints []rune - for len(str) > 0 { - r, s := utf8.DecodeRuneInString(str) - unicodePoints = append(unicodePoints, r) - str = str[s:] - } - return utf16.Encode(unicodePoints) -} - -func convertUtf16ToString(u []uint16) string { - var utf8Str []byte - for _, r := range utf16.Decode(u) { - utf8Char := make([]byte, utf8.RuneLen(r)) - utf8.EncodeRune(utf8Char, r) - utf8Str = append(utf8Str, utf8Char...) - } - return string(utf8Str) -} diff -Nru snapd-2.47.1+20.10.1build1/vendor/github.com/chrisccoulson/tcglog-parser/validate.go snapd-2.48+21.04/vendor/github.com/chrisccoulson/tcglog-parser/validate.go --- snapd-2.47.1+20.10.1build1/vendor/github.com/chrisccoulson/tcglog-parser/validate.go 2020-04-06 16:55:41.000000000 +0000 +++ snapd-2.48+21.04/vendor/github.com/chrisccoulson/tcglog-parser/validate.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,228 +0,0 @@ -package tcglog - -import ( - "bytes" - "encoding/binary" - "io" - "os" -) - -type EFIBootVariableBehaviour int - -const ( - EFIBootVariableBehaviourUnknown EFIBootVariableBehaviour = iota - EFIBootVariableBehaviourFull - EFIBootVariableBehaviourVarDataOnly -) - -type IncorrectDigestValue struct { - Algorithm AlgorithmId - Expected Digest -} - -type ValidatedEvent struct { - Event *Event - MeasuredBytes []byte - MeasuredTrailingBytesCount int - IncorrectDigestValues []IncorrectDigestValue -} - -type LogValidateResult struct { - EfiBootVariableBehaviour EFIBootVariableBehaviour - ValidatedEvents []*ValidatedEvent - Spec Spec - Algorithms AlgorithmIdList - ExpectedPCRValues map[PCRIndex]DigestMap -} - -func doesEventTypeExtendPCR(t EventType) bool { - if t == EventTypeNoAction { - return false - } - return true -} - -func performHashExtendOperation(alg AlgorithmId, initial Digest, event Digest) Digest { - hash := alg.newHash() - hash.Write(initial) - hash.Write(event) - return hash.Sum(nil) -} - -func determineMeasuredBytes(event *Event, efiBootVariableQuirk bool) ([]byte, bool) { - switch d := event.Data.(type) { - case *opaqueEventData: - switch event.EventType { - case EventTypeEventTag, EventTypeSCRTMVersion, EventTypePlatformConfigFlags, - EventTypeTableOfDevices, EventTypeNonhostInfo, EventTypeOmitBootDeviceEvents: - return event.Data.Bytes(), false - } - case *separatorEventData: - if !d.isError { - return event.Data.Bytes(), false - } else { - out := make([]byte, 4) - binary.LittleEndian.PutUint32(out, separatorEventErrorValue) - return out, false - } - case *asciiStringEventData: - switch event.EventType { - case EventTypeAction, EventTypeEFIAction: - return event.Data.Bytes(), false - } - case *EFIVariableEventData: - if event.EventType == EventTypeEFIVariableBoot && efiBootVariableQuirk { - return d.VariableData, false - } else { - return event.Data.Bytes(), true - } - case *efiGPTEventData: - return event.Data.Bytes(), true - case *GrubStringEventData: - return []byte(d.Str), false - case *SystemdEFIStubEventData: - // The event data is a UTF-16 string terminated with a single zero byte, but the measured - // data is a UTF-16 string with a UTF-16 null terminator. Add an extra zero byte here - c := make([]byte, len(d.data)+1) - copy(c, d.data) - return c, false - } - - return nil, false -} - -func isExpectedDigestValue(digest Digest, alg AlgorithmId, measuredBytes []byte) (bool, []byte) { - expected := alg.hash(measuredBytes) - return bytes.Equal(digest, expected), expected -} - -type logValidator struct { - log *Log - expectedPCRValues map[PCRIndex]DigestMap - efiBootVariableBehaviour EFIBootVariableBehaviour - validatedEvents []*ValidatedEvent -} - -func (v *logValidator) checkEventDigests(e *ValidatedEvent, trailingBytes int) { - for alg, digest := range e.Event.Digests { - if len(e.MeasuredBytes) > 0 { - // We've already determined the bytes measured for this event for a previous digest - if ok, expected := isExpectedDigestValue(digest, alg, e.MeasuredBytes); !ok { - e.IncorrectDigestValues = append(e.IncorrectDigestValues, - IncorrectDigestValue{Algorithm: alg, Expected: expected}) - } - continue - } - - efiBootVariableBehaviourTry := v.efiBootVariableBehaviour - - Loop: - for { - // Determine what we expect to be measured - provisionalMeasuredBytes, checkTrailingBytes := determineMeasuredBytes(e.Event, efiBootVariableBehaviourTry == EFIBootVariableBehaviourVarDataOnly) - if provisionalMeasuredBytes == nil { - return - } - - var provisionalMeasuredTrailingBytes int - if checkTrailingBytes { - provisionalMeasuredTrailingBytes = trailingBytes - } - - for { - // Determine whether the digest is consistent with the current provisional measured bytes - ok, _ := isExpectedDigestValue(digest, alg, provisionalMeasuredBytes) - switch { - case ok: - // All good - e.MeasuredBytes = provisionalMeasuredBytes - e.MeasuredTrailingBytesCount = provisionalMeasuredTrailingBytes - if e.Event.EventType == EventTypeEFIVariableBoot && v.efiBootVariableBehaviour == EFIBootVariableBehaviourUnknown { - // This is the first EV_EFI_VARIABLE_BOOT event, so record the measurement behaviour. - v.efiBootVariableBehaviour = efiBootVariableBehaviourTry - if efiBootVariableBehaviourTry == EFIBootVariableBehaviourUnknown { - v.efiBootVariableBehaviour = EFIBootVariableBehaviourFull - } - } - break Loop - case provisionalMeasuredTrailingBytes > 0: - // Invalid digest, the event data decoder determined there were trailing bytes, and we were expecting the measured - // bytes to match the event data. Test if any of the trailing bytes only appear in the event data by truncating - // the provisional measured bytes one byte at a time and re-testing. - provisionalMeasuredBytes = provisionalMeasuredBytes[0 : len(provisionalMeasuredBytes)-1] - provisionalMeasuredTrailingBytes -= 1 - default: - // Invalid digest - if e.Event.EventType == EventTypeEFIVariableBoot && efiBootVariableBehaviourTry == EFIBootVariableBehaviourUnknown { - // This is the first EV_EFI_VARIABLE_BOOT event, and this test was done assuming that the measured bytes - // would include the entire EFI_VARIABLE_DATA structure. Repeat the test with only the variable data. - efiBootVariableBehaviourTry = EFIBootVariableBehaviourVarDataOnly - continue Loop - } - // Record the expected digest on the event - expectedMeasuredBytes, _ := determineMeasuredBytes(e.Event, false) - e.IncorrectDigestValues = append( - e.IncorrectDigestValues, - IncorrectDigestValue{Algorithm: alg, Expected: alg.hash(expectedMeasuredBytes)}) - break Loop - } - } - } - } -} - -func (v *logValidator) processEvent(event *Event, trailingBytes int) { - if _, exists := v.expectedPCRValues[event.PCRIndex]; !exists { - v.expectedPCRValues[event.PCRIndex] = DigestMap{} - for _, alg := range v.log.Algorithms { - v.expectedPCRValues[event.PCRIndex][alg] = make(Digest, alg.size()) - } - } - - ve := &ValidatedEvent{Event: event} - v.validatedEvents = append(v.validatedEvents, ve) - - if !doesEventTypeExtendPCR(event.EventType) { - return - } - - for alg, digest := range event.Digests { - v.expectedPCRValues[event.PCRIndex][alg] = - performHashExtendOperation(alg, v.expectedPCRValues[event.PCRIndex][alg], digest) - } - - v.checkEventDigests(ve, trailingBytes) -} - -func (v *logValidator) run() (*LogValidateResult, error) { - for { - event, trailingBytes, err := v.log.nextEventInternal() - if err != nil { - if err == io.EOF { - return &LogValidateResult{ - EfiBootVariableBehaviour: v.efiBootVariableBehaviour, - ValidatedEvents: v.validatedEvents, - Spec: v.log.Spec, - Algorithms: v.log.Algorithms, - ExpectedPCRValues: v.expectedPCRValues}, nil - } - return nil, err - } - v.processEvent(event, trailingBytes) - } -} - -func ReplayAndValidateLog(logPath string, options LogOptions) (*LogValidateResult, error) { - file, err := os.Open(logPath) - if err != nil { - return nil, err - } - - log, err := NewLog(file, options) - if err != nil { - return nil, err - } - - v := &logValidator{log: log, expectedPCRValues: make(map[PCRIndex]DigestMap)} - return v.run() -} diff -Nru snapd-2.47.1+20.10.1build1/vendor/github.com/snapcore/secboot/constants.go snapd-2.48+21.04/vendor/github.com/snapcore/secboot/constants.go --- snapd-2.47.1+20.10.1build1/vendor/github.com/snapcore/secboot/constants.go 2020-09-02 10:31:40.000000000 +0000 +++ snapd-2.48+21.04/vendor/github.com/snapcore/secboot/constants.go 2020-11-18 09:44:21.000000000 +0000 @@ -24,8 +24,7 @@ ) const ( - lockNVHandle tpm2.Handle = 0x01801100 // Global NV handle for locking access to sealed key objects - lockNVDataHandle tpm2.Handle = 0x01801101 // NV index containing policy data for lockNVHandle + lockNVHandle tpm2.Handle = 0x01801100 // Legacy global NV handle for locking access to sealed key objects // SHA-256 is mandatory to exist on every PC-Client TPM // XXX: Maybe dynamically select algorithms based on what's available on the device? diff -Nru snapd-2.47.1+20.10.1build1/vendor/github.com/snapcore/secboot/crypt.go snapd-2.48+21.04/vendor/github.com/snapcore/secboot/crypt.go --- snapd-2.47.1+20.10.1build1/vendor/github.com/snapcore/secboot/crypt.go 2020-09-17 07:14:06.000000000 +0000 +++ snapd-2.48+21.04/vendor/github.com/snapcore/secboot/crypt.go 2020-11-18 09:44:21.000000000 +0000 @@ -30,6 +30,7 @@ "os" "os/exec" "path/filepath" + "regexp" "strconv" "strings" @@ -44,8 +45,17 @@ var ( runDir = "/run" systemdCryptsetupPath = "/lib/systemd/systemd-cryptsetup" + + defaultKeyringPrefix = "secboot" ) +func keyringPrefixOrDefault(prefix string) string { + if prefix == "" { + return defaultKeyringPrefix + } + return prefix +} + // RecoveryKey corresponds to a 16-byte recovery key in its binary form. type RecoveryKey [16]byte @@ -239,38 +249,35 @@ return askPassword(sourceDevicePath, "Please enter the "+description+" for disk "+sourceDevicePath+":") } -// RecoveryKeyUsageReason indicates the reason that a volume had to be activated with the fallback recovery key instead of the TPM -// sealed key. -type RecoveryKeyUsageReason uint8 +// KeyErrorCode indicates the reason that a TPM protected key could not be used to activate a volume. +type KeyErrorCode uint8 const ( - // RecoveryKeyUsageReasonUnexpectedError indicates that a volume had to be activated with the fallback recovery key because an - // unexpected error was encountered during activation with the TPM sealed key. - RecoveryKeyUsageReasonUnexpectedError RecoveryKeyUsageReason = iota + 1 - - // RecoveryKeyUsageReasonRequested indicates that a volume was activated with the fallback recovery key via the - // ActivateVolumeWithRecoveryKey API. - RecoveryKeyUsageReasonRequested - - // RecoveryKeyUsageReasonTPMLockout indicates that a volume had to be activated with the fallback recovery key because the TPM is in - // dictionary attack lockout mode. - RecoveryKeyUsageReasonTPMLockout - - // RecoveryKeyUsageReasonTPMProvisioningError indicates that a volume had to be activated with the fallback recovery key because the - // TPM is not correctly provisioned. - RecoveryKeyUsageReasonTPMProvisioningError - - // RecoveryKeyUsageReasonInvalidKeyFile indicates that a volume had to be activated with the fallback recovery key because the TPM - // sealed key file is invalid. Note that attempts to resolve this by creating a new file with SealKeyToTPM may indicate that the TPM - // is also not correctly provisioned. - RecoveryKeyUsageReasonInvalidKeyFile + // KeyUnexpectedError indicates that a key could not be used because an unexpected error was encountered + // with it. + KeyUnexpectedError KeyErrorCode = iota + 1 + + // This is here for compatibility with old secboot versions running in early boot. + legacyRecoveryKeyRequested + + // KeyErrorTPMLockout indicates that a key could not be used because the TPM is in dictionary attack + // lockout mode. + KeyErrorTPMLockout + + // KeyErrorTPMProvisioning indicates that a key could not be used because the TPM is not correctly provisioned. + KeyErrorTPMProvisioning + + // KeyErrorInvalidFile indicates that a key could not be used because the TPM sealed key is invalid. Note + // that attempts to resolve this by creating a new file with SealKeyToTPM may indicate that the TPM is also + // not correctly provisioned. + KeyErrorInvalidFile - // RecoveryKeyUsageReasonPINFail indicates that a volume had to be activated with the fallback recovery key because the correct PIN + // KeyErrorPassphraseFail indicates that a key could not be used because the correct user passphrase/PIN // was not provided. - RecoveryKeyUsageReasonPINFail + KeyErrorPassphraseFail ) -func activateWithRecoveryKey(volumeName, sourceDevicePath string, keyReader io.Reader, tries int, reason RecoveryKeyUsageReason, activateOptions []string) error { +func activateWithRecoveryKey(volumeName, sourceDevicePath string, keyReader io.Reader, tries int, reasons []KeyErrorCode, activateOptions []string, keyringPrefix string) error { if tries == 0 { return errors.New("no recovery key tries permitted") } @@ -304,108 +311,178 @@ continue } - if _, err := unix.AddKey("user", fmt.Sprintf("%s:%s:reason=%d", filepath.Base(os.Args[0]), volumeName, reason), key[:], userKeyring); err != nil { - lastErr = xerrors.Errorf("cannot add recovery key to user keyring: %w", err) + // Add a key to the calling user's user keyring with default 0x3f010000 permissions (these defaults are hardcoded in the kernel). + // This permission flags define the following permissions: + // Possessor Set Attribute / Possessor Link / Possessor Search / Possessor Write / Possessor Read / Possessor View / User View. + // Possessor permissions only apply to a process with a searchable link to the key from one of its own keyrings - just having the + // same UID is not sufficient. Read permission is required to read the contents of the key (view permission only permits viewing + // of the description and other public metadata that isn't the key payload). + // + // Note that by default, systemd starts services with a private session keyring which does not contain a link to the user keyring. + // Therefore these services cannot access the contents of keys in the root user's user keyring if those keys only permit + // possessor-read. + // + // Ignore errors - we've activated the volume and so we shouldn't return an error at this point unless we close the volume again. + var desc bytes.Buffer + fmt.Fprintf(&desc, "%s:%s?type=recovery", keyringPrefixOrDefault(keyringPrefix), sourceDevicePath) + if len(reasons) == 0 { + fmt.Fprintf(&desc, "&requested") + } else { + fmt.Fprintf(&desc, "&errors=") + for i, e := range reasons { + if i > 0 { + fmt.Fprintf(&desc, ",") + } + fmt.Fprintf(&desc, "%d", e) + } } + unix.AddKey("user", desc.String(), key[:], userKeyring) break } return lastErr } -func unsealKeyFromTPM(tpm *TPMConnection, k *SealedKeyObject, pin string) ([]byte, error) { - key, err := k.UnsealFromTPM(tpm, pin) +func unsealKeyFromTPM(tpm *TPMConnection, k *SealedKeyObject, pin string) ([]byte, []byte, error) { + sealedKey, authPrivateKey, err := k.UnsealFromTPM(tpm, pin) if err == ErrTPMProvisioning { // ErrTPMProvisioning in this context might indicate that there isn't a valid persistent SRK. Have a go at creating one now and then // retrying the unseal operation - if the previous SRK was evicted, the TPM owner hasn't changed and the storage hierarchy still // has a null authorization value, then this will allow us to unseal the key without requiring any type of manual recovery. If the // storage hierarchy has a non-null authorization value, ProvionTPM will fail. If the TPM owner has changed, ProvisionTPM might // succeed, but UnsealFromTPM will fail with InvalidKeyFileError when retried. - if pErr := ProvisionTPM(tpm, ProvisionModeWithoutLockout, nil); pErr == nil { - key, err = k.UnsealFromTPM(tpm, pin) + if pErr := tpm.EnsureProvisioned(ProvisionModeWithoutLockout, nil); pErr == nil || pErr == ErrTPMProvisioningRequiresLockout { + sealedKey, authPrivateKey, err = k.UnsealFromTPM(tpm, pin) } } - return key, err + return sealedKey, authPrivateKey, err +} + +func unsealKeyFromTPMAndActivate(tpm *TPMConnection, volumeName, sourceDevicePath, keyringPrefix string, activateOptions []string, k *SealedKeyObject, pin string) error { + sealedKey, authPrivateKey, err := unsealKeyFromTPM(tpm, k, pin) + if err != nil { + return xerrors.Errorf("cannot unseal key: %w", err) + } + + if err := activate(volumeName, sourceDevicePath, sealedKey, activateOptions); err != nil { + return xerrors.Errorf("cannot activate volume: %w", err) + } + + // Add a key to the calling user's user keyring with default 0x3f010000 permissions (these defaults are hardcoded in the kernel). + // This permission flags define the following permissions: + // Possessor Set Attribute / Possessor Link / Possessor Search / Possessor Write / Possessor Read / Possessor View / User View. + // Possessor permissions only apply to a process with a searchable link to the key from one of its own keyrings - just having the + // same UID is not sufficient. Read permission is required to read the contents of the key (view permission only permits viewing + // of the description and other public metadata that isn't the key payload). + // + // Note that by default, systemd starts services with a private session keyring which does not contain a link to the user keyring. + // Therefore these services cannot access the contents of keys in the root user's user keyring if those keys only permit + // possessor-read. + // + // Ignore errors - we've activated the volume and so we shouldn't return an error at this point unless we close the volume again. + unix.AddKey("user", fmt.Sprintf("%s:%s?type=tpm", keyringPrefixOrDefault(keyringPrefix), sourceDevicePath), authPrivateKey, userKeyring) + return nil } var requiresPinErr = errors.New("no PIN tries permitted when a PIN is required") -type lockAccessError struct { - err error +type activateWithTPMKeyError struct { + path string + err error } -func (e lockAccessError) Error() string { - return e.err.Error() +func (e *activateWithTPMKeyError) Error() string { + return fmt.Sprintf("%s: %v", e.path, e.err) } -func (e lockAccessError) Unwrap() error { +func (e *activateWithTPMKeyError) Unwrap() error { return e.err } -func isLockAccessError(err error) bool { - var e lockAccessError - return xerrors.As(err, &e) -} - -func activateWithTPMKey(tpm *TPMConnection, volumeName, sourceDevicePath, keyPath string, pinReader io.Reader, pinTries int, lock bool, activateOptions []string) error { - var lockErr error - key, err := func() ([]byte, error) { - defer func() { - if !lock { - return - } - lockErr = LockAccessToSealedKeys(tpm) - }() +type activateTPMKeyContext struct { + path string + k *SealedKeyObject + err error +} + +func (c *activateTPMKeyContext) Err() *activateWithTPMKeyError { + if c.err == nil { + return nil + } + return &activateWithTPMKeyError{path: c.path, err: c.err} +} - k, err := ReadSealedKeyObject(keyPath) +func activateWithTPMKeys(tpm *TPMConnection, volumeName, sourceDevicePath string, keyPaths []string, passphraseReader io.Reader, passphraseTries int, activateOptions []string, keyringPrefix string) (succeeded bool, errs []*activateWithTPMKeyError) { + var contexts []*activateTPMKeyContext + // Read key files + for _, path := range keyPaths { + k, err := ReadSealedKeyObject(path) if err != nil { - return nil, xerrors.Errorf("cannot read sealed key object: %w", err) + err = xerrors.Errorf("cannot read sealed key object: %w", err) } + contexts = append(contexts, &activateTPMKeyContext{path: path, k: k, err: err}) + } - switch { - case pinTries == 0 && k.AuthMode2F() == AuthModePIN: - return nil, requiresPinErr - case pinTries == 0: - pinTries = 1 + // Try key files that don't require a passphrase first. + for _, c := range contexts { + if c.err != nil { + continue + } + if c.k.AuthMode2F() != AuthModeNone { + continue } - var key []byte - - for ; pinTries > 0; pinTries-- { - var pin string - if k.AuthMode2F() == AuthModePIN { - r := pinReader - pinReader = nil - pin, err = getPassword(sourceDevicePath, "PIN", r) - if err != nil { - return nil, xerrors.Errorf("cannot obtain PIN: %w", err) - } - } + if err := unsealKeyFromTPMAndActivate(tpm, volumeName, sourceDevicePath, keyringPrefix, activateOptions, c.k, ""); err != nil { + c.err = err + continue + } - key, err = unsealKeyFromTPM(tpm, k, pin) - if err != nil && (err != ErrPINFail || k.AuthMode2F() != AuthModePIN) { + return true, nil + } + + // Try key files that do require a passhprase last. + for _, c := range contexts { + if c.err != nil { + continue + } + if c.k.AuthMode2F() != AuthModePIN { + continue + } + if passphraseTries == 0 { + c.err = requiresPinErr + continue + } + + var pin string + for i := 0; i < passphraseTries; i++ { + r := passphraseReader + passphraseReader = nil + var err error + pin, err = getPassword(sourceDevicePath, "PIN", r) + if err != nil { + c.err = xerrors.Errorf("cannot obtain PIN: %w", err) break } } - if err != nil { - return nil, xerrors.Errorf("cannot unseal key: %w", err) + if c.err != nil { + continue } - return key, nil - }() - switch { - case lockErr != nil: - return lockAccessError{err} - case err != nil: - return err + if err := unsealKeyFromTPMAndActivate(tpm, volumeName, sourceDevicePath, keyringPrefix, activateOptions, c.k, pin); err != nil { + c.err = err + continue + } + + return true, nil } - if err := activate(volumeName, sourceDevicePath, key, activateOptions); err != nil { - return xerrors.Errorf("cannot activate volume: %w", err) + // Activation has failed if we reach this point. + for _, c := range contexts { + errs = append(errs, c.Err()) } + return false, errs - return nil } func makeActivateOptions(in []string) ([]string, error) { @@ -419,63 +496,83 @@ return append(out, "tries=1"), nil } -// ActivateWithTPMSealedKeyOptions provides options to ActivateVolumeWtthTPMSealedKey. -type ActivateWithTPMSealedKeyOptions struct { - // PINTries specifies the maximum number of times that unsealing with a PIN should be attempted before failing with an error and - // falling back to activating with the recovery key if RecoveryKeyTries is greater than zero. Setting this to zero disables unsealing - // with a PIN - in this case, an error will be returned if the sealed key object indicates that a PIN has been set. Attempts to - // unseal with a PIN will stop if the TPM enters dictionary attack lockout mode before this limit is reached. - PINTries int - - // RecoveryKeyTries specifies the maximum number of times that activation with the fallback recovery key should be attempted - // if activation with the TPM sealed key fails, before failing with an error. Setting this to zero will disable attempts to activate - // with the fallback recovery key. +// ActivateVolumeOptions provides options to the ActivateVolumeWith* +// family of functions. +type ActivateVolumeOptions struct { + // PassphraseTries specifies the maximum number of times + // that unsealing with a user passphrase should be attempted + // before failing with an error and falling back to activating + // with the recovery key (see RecoveryKeyTries). + // Setting this to zero disables unsealing with a user + // passphrase - in this case, an error will be returned if the + // sealed key object indicates that a user passphrase has been + // set. + // With a TPM, attempts to unseal will stop if the TPM enters + // dictionary attack lockout mode before this limit is + // reached. + // It is ignored by ActivateWithRecoveryKey. + PassphraseTries int + + // RecoveryKeyTries specifies the maximum number of times that + // activation with the fallback recovery key should be + // attempted. + // It is used directly by ActivateWithRecoveryKey and + // indirectly with other methods upon failure, for example + // failed TPM unsealing. Setting this to zero will disable + // attempts to activate with the fallback recovery key. RecoveryKeyTries int - // ActivateOptions provides a mechanism to pass additional options to systemd-cryptsetup. + // ActivateOptions provides a mechanism to pass additional + // options to systemd-cryptsetup. ActivateOptions []string - // LockSealedKeyAccess controls whether LockAccessToSealedKeys should be called after unsealing the TPM sealed key. It is called if - // this is set to true, and not called if this is set to false. - LockSealedKeyAccess bool + // KeyringPrefix is the prefix used for the description of any + // kernel keys created during activation. + KeyringPrefix string } -// ActivateVolumeWithTPMSealedKey attempts to activate the LUKS encrypted volume at sourceDevicePath and create a mapping with the -// name volumeName, using the TPM sealed key object at the specified keyPath. This makes use of systemd-cryptsetup. -// -// If the TPM sealed key object has a PIN defined, then this function will use systemd-ask-password to request it. If pinReader is not -// nil, then an attempt to read the PIN from this will be made instead by reading all characters until the first newline. The PINTries -// field of options defines how many attempts should be made to obtain the correct PIN before failing. +// ActivateVolumeWithMultipleTPMSealedKeys attempts to activate the LUKS encrypted volume at sourceDevicePath and create a +// mapping with the name volumeName, using the TPM sealed key objects at the specified keyPaths. This makes use of +// systemd-cryptsetup. This function will try the sealed key objects that don't require a passphrase first, and then +// try sealed key objects that do require a passphrase. Sealed key objects are otherwise tried in the order in which +// they are provided. +// +// If this function tries a TPM sealed key object that has a user passphrase/PIN defined, then this function will use +// systemd-ask-password to request it. If passphraseReader is not nil, then an attempt to read the user passphrase/PIN from this +// will be made instead by reading all characters until the first newline. The PassphraseTries field of options defines how many +// attempts should be made to obtain the correct passphrase for each TPM sealed key before failing. // // The ActivateOptions field of options can be used to specify additional options to pass to systemd-cryptsetup. // -// If the LockSealedKeyAccess field of options is true, then this function will call LockAccessToSealedKeys after unsealing the key -// and before activating the LUKS volume. -// -// If activation with the TPM sealed key object fails, this function will attempt to activate it with the fallback recovery key +// If activation with the TPM sealed key objects fails, this function will attempt to activate it with the fallback recovery key // instead. The fallback recovery key will be requested using systemd-ask-password. The RecoveryKeyTries field of options specifies // how many attempts should be made to activate the volume with the recovery key before failing. If this is set to 0, then no attempts // will be made to activate the encrypted volume with the fallback recovery key. If activation with the recovery key is successful, -// the recovery key will be added to the root user keyring in the kernel with a description of the format -// "::reason=" where reason is an integer that describes the recovery reason - see the -// RecoveryKeyUsageReason type. +// calling GetActivationDataFromKernel will return a *RecoveryActivationData containing the recovery key and the error codes +// associated with the supplied TPM sealed keys. // -// If either the PINTries or RecoveryKeyTries fields of options are less than zero, an error will be returned. If the ActivateOptions +// If either the PassphraseTries or RecoveryKeyTries fields of options are less than zero, an error will be returned. If the ActivateOptions // field of options contains the "tries=" option, then an error will be returned. This option cannot be used with this function. // -// If the LockSealedKeyAccess field of options is true and the call to LockAccessToSealedKeys fails, a LockAccessToSealedKeysError -// error will be returned. In this case, activation with either the TPM sealed key or the fallback recovery key will not be attempted. +// If activation with the TPM sealed keys fails, a *ActivateWithMultipleTPMSealedKeysError error will be returned, even if the +// subsequent fallback recovery activation is successful. In this case, the RecoveryKeyUsageErr field of the returned error will +// be nil, and the TPMErrs field will contain the original errors for each of the TPM sealed keys. If activation with the fallback +// recovery key also fails, the RecoveryKeyUsageErr field of the returned error will also contain details of the error encountered +// during recovery key activation. +// +// If the volume is successfully activated with a TPM sealed key and the TPM sealed key has a version of greater than 1, calling +// GetActivationDataFromKernel will return a TPMPolicyAuthKey containing the private part of the key used for authorizing PCR policy +// updates with UpdateKeyPCRProtectionPolicy. // -// If activation with the TPM sealed key fails, a *ActivateWithTPMSealedKeyError error will be returned, even if the subsequent -// fallback recovery activation is successful. In this case, the RecoveryKeyUsageErr field of the returned error will be nil, and the -// TPMErr field will contain the original error. If activation with the fallback recovery key also fails, the RecoveryKeyUsageErr -// field of the returned error will also contain details of the error encountered during recovery key activation. -// -// If the volume is successfully activated, either with the TPM sealed key or the fallback recovery key, this function returns true. +// If the volume is successfully activated, either with a TPM sealed key or the fallback recovery key, this function returns true. // If it is not successfully activated, then this function returns false. -func ActivateVolumeWithTPMSealedKey(tpm *TPMConnection, volumeName, sourceDevicePath, keyPath string, pinReader io.Reader, options *ActivateWithTPMSealedKeyOptions) (bool, error) { - if options.PINTries < 0 { - return false, errors.New("invalid PINTries") +func ActivateVolumeWithMultipleTPMSealedKeys(tpm *TPMConnection, volumeName, sourceDevicePath string, keyPaths []string, passphraseReader io.Reader, options *ActivateVolumeOptions) (bool, error) { + if len(keyPaths) == 0 { + return false, errors.New("no key files provided") + } + + if options.PassphraseTries < 0 { + return false, errors.New("invalid PassphraseTries") } if options.RecoveryKeyTries < 0 { return false, errors.New("invalid RecoveryKeyTries") @@ -486,68 +583,274 @@ return false, err } - if err := activateWithTPMKey(tpm, volumeName, sourceDevicePath, keyPath, pinReader, options.PINTries, options.LockSealedKeyAccess, activateOptions); err != nil { - reason := RecoveryKeyUsageReasonUnexpectedError - switch { - case isLockAccessError(err): - return false, LockAccessToSealedKeysError(err.Error()) - case xerrors.Is(err, ErrTPMLockout): - reason = RecoveryKeyUsageReasonTPMLockout - case xerrors.Is(err, ErrTPMProvisioning): - reason = RecoveryKeyUsageReasonTPMProvisioningError - case isInvalidKeyFileError(err): - reason = RecoveryKeyUsageReasonInvalidKeyFile - case xerrors.Is(err, requiresPinErr): - reason = RecoveryKeyUsageReasonPINFail - case xerrors.Is(err, ErrPINFail): - reason = RecoveryKeyUsageReasonPINFail - case isExecError(err, systemdCryptsetupPath): - // systemd-cryptsetup only provides 2 exit codes - success or fail - so we don't know the reason it failed yet. If activation - // with the recovery key is successful, then it's safe to assume that it failed because the key unsealed from the TPM is incorrect. - reason = RecoveryKeyUsageReasonInvalidKeyFile + if success, errs := activateWithTPMKeys(tpm, volumeName, sourceDevicePath, keyPaths, passphraseReader, options.PassphraseTries, activateOptions, options.KeyringPrefix); !success { + var tpmErrCodes []KeyErrorCode + var tpmErrs []error + + for _, e := range errs { + code := KeyUnexpectedError + switch { + case xerrors.Is(e, ErrTPMLockout): + code = KeyErrorTPMLockout + case xerrors.Is(e, ErrTPMProvisioning): + code = KeyErrorTPMProvisioning + case isInvalidKeyFileError(e): + code = KeyErrorInvalidFile + case xerrors.Is(e, requiresPinErr): + code = KeyErrorPassphraseFail + case xerrors.Is(e, ErrPINFail): + code = KeyErrorPassphraseFail + case isExecError(e, systemdCryptsetupPath): + // systemd-cryptsetup only provides 2 exit codes - success or fail - so we don't know the reason it failed yet. + // If activation with the recovery key is successful, then it's safe to assume that it failed because the key + // unsealed from the TPM is incorrect. + code = KeyErrorInvalidFile + } + tpmErrCodes = append(tpmErrCodes, code) + tpmErrs = append(tpmErrs, e) + } - rErr := activateWithRecoveryKey(volumeName, sourceDevicePath, nil, options.RecoveryKeyTries, reason, activateOptions) - return rErr == nil, &ActivateWithTPMSealedKeyError{err, rErr} + rErr := activateWithRecoveryKey(volumeName, sourceDevicePath, nil, options.RecoveryKeyTries, tpmErrCodes, activateOptions, options.KeyringPrefix) + return rErr == nil, &ActivateWithMultipleTPMSealedKeysError{tpmErrs, rErr} } return true, nil } -// ActivateWithRecoveryKeyOptions provides options to ActivateVolumeWithRecoveryKey. -type ActivateWithRecoveryKeyOptions struct { - // Tries specifies the maximum number of times that activation with the fallback recovery key should be attempted before failing - // with an error. - Tries int - - // ActivateOptions provides a mechanism to pass additional options to systemd-cryptsetup. - ActivateOptions []string +// ActivateVolumeWithTPMSealedKey attempts to activate the LUKS encrypted volume at sourceDevicePath and create a mapping with the +// name volumeName, using the TPM sealed key object at the specified keyPath. This makes use of systemd-cryptsetup. +// +// If the TPM sealed key object has a user passphrase/PIN defined, then this function will use systemd-ask-password to request +// it. If passphraseReader is not nil, then an attempt to read the user passphrase/PIN from this will be made instead by reading +// all characters until the first newline. The PassphraseTries field of options defines how many attempts should be made to +// obtain the correct passphrase before failing. +// +// The ActivateOptions field of options can be used to specify additional options to pass to systemd-cryptsetup. +// +// If activation with the TPM sealed key object fails, this function will attempt to activate it with the fallback recovery key +// instead. The fallback recovery key will be requested using systemd-ask-password. The RecoveryKeyTries field of options specifies +// how many attempts should be made to activate the volume with the recovery key before failing. If this is set to 0, then no attempts +// will be made to activate the encrypted volume with the fallback recovery key. If activation with the recovery key is successful, +// calling GetActivationDataFromKernel will return a *RecoveryActivationData containing the recovery key and the error code +// associated with the TPM sealed key. +// +// If either the PassphraseTries or RecoveryKeyTries fields of options are less than zero, an error will be returned. If the +// ActivateOptions field of options contains the "tries=" option, then an error will be returned. This option cannot be used with +// this function. +// +// If activation with the TPM sealed key fails, a *ActivateWithTPMSealedKeyError error will be returned, even if the subsequent +// fallback recovery activation is successful. In this case, the RecoveryKeyUsageErr field of the returned error will be nil, and the +// TPMErr field will contain the original error. If activation with the fallback recovery key also fails, the RecoveryKeyUsageErr +// field of the returned error will also contain details of the error encountered during recovery key activation. +// +// If the volume is successfully activated with the TPM sealed key and the TPM sealed key has a version of greater than 1, calling +// GetActivationDataFromKernel will return a TPMPolicyAuthKey containing the private part of the key used for authorizing PCR policy +// updates with UpdateKeyPCRProtectionPolicy. +// +// If the volume is successfully activated, either with the TPM sealed key or the fallback recovery key, this function returns true. +// If it is not successfully activated, then this function returns false. +func ActivateVolumeWithTPMSealedKey(tpm *TPMConnection, volumeName, sourceDevicePath, keyPath string, passphraseReader io.Reader, options *ActivateVolumeOptions) (bool, error) { + succeeded, err := ActivateVolumeWithMultipleTPMSealedKeys(tpm, volumeName, sourceDevicePath, []string{keyPath}, passphraseReader, options) + if e1, ok := err.(*ActivateWithMultipleTPMSealedKeysError); ok { + if e2, ok := e1.TPMErrs[0].(*activateWithTPMKeyError); ok { + err = &ActivateWithTPMSealedKeyError{e2.err, e1.RecoveryKeyUsageErr} + } else { + err = &ActivateWithTPMSealedKeyError{e1.TPMErrs[0], e1.RecoveryKeyUsageErr} + } + } + return succeeded, err } // ActivateVolumeWithRecoveryKey attempts to activate the LUKS encrypted volume at sourceDevicePath and create a mapping with the // name volumeName, using the fallback recovery key. This makes use of systemd-cryptsetup. // // This function will use systemd-ask-password to request the recovery key. If keyReader is not nil, then an attempt to read the key -// from this will be made instead by reading all characters until the first newline. The Tries field of options defines how many +// from this will be made instead by reading all characters until the first newline. The RecoveryKeyTries field of options defines how many // attempts should be made to activate the volume with the recovery key before failing. // // The ActivateOptions field of options can be used to specify additional options to pass to systemd-cryptsetup. // -// If activation with the recovery key is successful, the recovery key will be added to the root user keyring in the kernel with a -// description of the format "::reason=2". +// If activation with the recovery key is successful, calling GetActivationDataFromKernel will return a *RecoveryActivationData +// containing the recovery key and the Requested flag set to true. // -// If the Tries field of options is less than zero, an error will be returned. If the ActivateOptions field of options contains the +// If the RecoveryKeyTries field of options is less than zero, an error will be returned. If the ActivateOptions field of options contains the // "tries=" option, then an error will be returned. This option cannot be used with this function. -func ActivateVolumeWithRecoveryKey(volumeName, sourceDevicePath string, keyReader io.Reader, options *ActivateWithRecoveryKeyOptions) error { - if options.Tries < 0 { - return errors.New("invalid Tries") +func ActivateVolumeWithRecoveryKey(volumeName, sourceDevicePath string, keyReader io.Reader, options *ActivateVolumeOptions) error { + if options.RecoveryKeyTries < 0 { + return errors.New("invalid RecoveryKeyTries") + } + + activateOptions, err := makeActivateOptions(options.ActivateOptions) + if err != nil { + return err } + return activateWithRecoveryKey(volumeName, sourceDevicePath, keyReader, options.RecoveryKeyTries, nil, activateOptions, options.KeyringPrefix) +} + +// ActivateVolumeWithKey attempts to activate the LUKS encrypted volume at +// sourceDevicePath and create a mapping with the name volumeName, using the +// provided key. This makes use of systemd-cryptsetup. +// +// The ActivateOptions field of options can be used to specify additional +// options to pass to systemd-cryptsetup. All other fields are ignored. +// +// If the ActivateOptions field of options contains the "tries=" option, then an +// error will be returned. This option cannot be used with this function. +func ActivateVolumeWithKey(volumeName, sourceDevicePath string, key []byte, options *ActivateVolumeOptions) error { + // do not be more strict about checking options to allow reusing it + // across different calls activateOptions, err := makeActivateOptions(options.ActivateOptions) if err != nil { return err } - return activateWithRecoveryKey(volumeName, sourceDevicePath, keyReader, options.Tries, RecoveryKeyUsageReasonRequested, activateOptions) + return activate(volumeName, sourceDevicePath, key, activateOptions) +} + +// ActivationData corresponds to some data added to the user keyring by one of the ActivateVolume functions. +type ActivationData interface{} + +// RecoveryActivationData is added to the user keyring when a recovery key is used to activate a volume. +type RecoveryActivationData struct { + Key RecoveryKey + Requested bool // The recovery key was used via the ActivateVolumeWithRecoveryKey API + + // ErrorCodes indicates the errors encountered with each key file passed to ActivateVolumeWithTPMSealedKey + // or ActivateVolumeWithMultipleTPMSealedKeys in the case that Requested is false. + ErrorCodes []KeyErrorCode +} + +// GetActivationDataFromKernel retrieves data that was added to the current user's user keyring by ActivateVolumeWithTPMSealedKey or +// ActivateVolumeWithRecoveryKey for the specified source block device, using the prefix that was passed to either of those functions. +// The block device path must match the path passed to one of the ActivateVolume functions. The type of data returned is dependent on +// how the volume was activated - see the documentation for each function, If no data is found for the specified device, a +// ErrNoActivationData error is returned. +// +// If remove is true, this function will unlink the key from the user's user keyring. +func GetActivationDataFromKernel(prefix, sourceDevicePath string, remove bool) (ActivationData, error) { + var userKeys []int + + sz, err := unix.KeyctlBuffer(unix.KEYCTL_READ, userKeyring, nil, 0) + if err != nil { + return nil, xerrors.Errorf("cannot determine size of user keyring payload: %w", err) + } + + for { + payload := make([]byte, sz) + n, err := unix.KeyctlBuffer(unix.KEYCTL_READ, userKeyring, payload, 0) + if err != nil { + return nil, xerrors.Errorf("cannot read user keyring payload: %w", err) + } + + if n <= sz { + payload = payload[:n] + + for len(payload) > 0 { + userKeys = append(userKeys, int(binary.LittleEndian.Uint32(payload))) + payload = payload[4:] + } + break + } + + sz = n + } + + re := regexp.MustCompile(fmt.Sprintf(`^user;[[:digit:]]+;[[:digit:]]+;[[:xdigit:]]+;%s:([^\?]+)\??(.*)`, keyringPrefixOrDefault(prefix))) + for _, id := range userKeys { + desc, err := unix.KeyctlString(unix.KEYCTL_DESCRIBE, id) + if err != nil { + continue + } + m := re.FindStringSubmatch(desc) + if len(m) == 0 { + continue + } + if m[1] != sourceDevicePath { + continue + } + + sz, err := unix.KeyctlBuffer(unix.KEYCTL_READ, id, nil, 0) + if err != nil { + return nil, xerrors.Errorf("cannot determine size of key payload: %w", err) + } + payload := make([]byte, sz) + _, err = unix.KeyctlBuffer(unix.KEYCTL_READ, id, payload, 0) + if err != nil { + return nil, xerrors.Errorf("cannot read key payload: %w", err) + } + + if remove { + // XXX: What should we do if unlinking fails? + unix.KeyctlInt(unix.KEYCTL_UNLINK, id, userKeyring, 0, 0) + } + + params := make(map[string]string) + if len(m) > 2 { + for _, p := range strings.Split(m[2], "&") { + s := strings.SplitN(p, "=", 2) + k := s[0] + var v string + if len(s) > 1 { + v = s[1] + } + params[k] = v + } + } + + t, ok := params["type"] + if !ok { + return nil, errors.New("invalid description (no type)") + } + switch t { + case "tpm": + return TPMPolicyAuthKey(payload), nil + case "recovery": + if len(payload) != binary.Size(RecoveryKey{}) { + return nil, errors.New("invalid payload size") + } + + var key RecoveryKey + copy(key[:], payload) + + _, ok := params["requested"] + if ok { + return &RecoveryActivationData{Key: key, Requested: true}, nil + } + + e, ok := params["errors"] + if ok { + var errCodes []KeyErrorCode + for _, s := range strings.Split(e, ",") { + c, err := strconv.Atoi(s) + if err != nil { + return nil, xerrors.Errorf("invalid recovery error code: %w", err) + } + errCodes = append(errCodes, KeyErrorCode(c)) + } + return &RecoveryActivationData{Key: key, ErrorCodes: errCodes}, nil + } + + // This is here for compatibility with old secboot versions running in early boot. + reason, ok := params["reason"] + if ok { + n, err := strconv.Atoi(reason) + if err != nil { + return nil, xerrors.Errorf("invalid recovery reason: %w", err) + } + if n == int(legacyRecoveryKeyRequested) { + return &RecoveryActivationData{Key: key, Requested: true}, nil + } + return &RecoveryActivationData{Key: key, ErrorCodes: []KeyErrorCode{KeyErrorCode(n)}}, nil + } + + return nil, errors.New("invalid recovery key parameters") + default: + return nil, errors.New("invalid description (unhandled type)") + } + } + + return nil, ErrNoActivationData } func setLUKS2KeyslotPreferred(devicePath string, slot int) error { @@ -559,11 +862,59 @@ return nil } +// InitializeLUKS2ContainerOptions carries options for initializing LUKS2 +// containers. +type InitializeLUKS2ContainerOptions struct { + // MetadataKiBSize sets the size of the LUKS2 metadata (JSON) area, + // expressed in multiples of 1024 bytes. The value includes 4096 bytes + // for the binary metadata. According to LUKS2 specification and + // cryptsetup(8), only these values are valid: 16, 32, 64, 128, 256, + // 512, 1024, 2048 and 4096 KiB. + MetadataKiBSize int + // KeyslotsAreaSize sets the size of the LUKS2 binary keyslot area, + // expressed in multiples of 1024 bytes. The value must be aligned to + // 4096 bytes, with the maximum size of 128MB. + KeyslotsAreaKiBSize int +} + +func validateInitializeLUKS2Options(options *InitializeLUKS2ContainerOptions) error { + if options == nil { + return nil + } + + if options.MetadataKiBSize != 0 { + // metadata size is one of the allowed values (in kB) + allowedSizesKB := []int{16, 32, 64, 128, 256, 512, 1024, 2048, 4096} + found := false + for _, sz := range allowedSizesKB { + if options.MetadataKiBSize == sz { + found = true + break + } + } + if !found { + return fmt.Errorf("cannot set metadata size to %v KiB", + options.MetadataKiBSize) + } + } + if options.KeyslotsAreaKiBSize != 0 { + // minimum size 4096 (4KiB), a multiple of 4096, max size 128MiB + sizeValid := options.KeyslotsAreaKiBSize >= 4 && + options.KeyslotsAreaKiBSize <= 128*1024 && + options.KeyslotsAreaKiBSize%4 == 0 + if !sizeValid { + return fmt.Errorf("cannot set keyslots area size to %v KiB", + options.KeyslotsAreaKiBSize) + } + } + return nil +} + // InitializeLUKS2Container will initialize the partition at the specified devicePath as a new LUKS2 container. This can only // be called on a partition that isn't mapped. The label for the new LUKS2 container is provided via the label argument. // // The initial key used for unlocking the container is provided via the key argument, and must be a cryptographically secure -// 64-byte random number. The key should be stored encrypted by using SealKeyToTPM. +// random number of at least 32-bytes. The key should be encrypted by using SealKeyToTPM. // // The container will be configured to encrypt data with AES-256 and XTS block cipher mode. // @@ -571,12 +922,15 @@ // // WARNING: This function is destructive. Calling this on an existing LUKS container will make the data contained inside of it // irretrievable. -func InitializeLUKS2Container(devicePath, label string, key []byte) error { - if len(key) != 64 { - return fmt.Errorf("expected a key length of 512-bits (got %d)", len(key)*8) +func InitializeLUKS2Container(devicePath, label string, key []byte, options *InitializeLUKS2ContainerOptions) error { + if len(key) < 32 { + return fmt.Errorf("expected a key length of at least 256-bits (got %d)", len(key)*8) + } + if err := validateInitializeLUKS2Options(options); err != nil { + return err } - cmd := exec.Command("cryptsetup", + args := []string{ // batch processing, no password verification for formatting an existing LUKS container "-q", // formatting a new volume @@ -587,14 +941,28 @@ "--key-file", "-", // use AES-256 with XTS block cipher mode (XTS requires 2 keys) "--cipher", "aes-xts-plain64", "--key-size", "512", - // use argon2i as the KDF with minimum cost (lowest possible time and memory costs). This is done - // because the supplied input key has the same entropy (512-bits) as the derived key and therefore - // increased time or memory cost don't provide a security benefit (but does slow down unlocking). - "--pbkdf", "argon2i", "--pbkdf-force-iterations", "4", "--pbkdf-memory", "32", + // use argon2i as the KDF with reduced cost. This is done because the supplied input key has an + // entropy of at least 32 bytes, and increased cost doesn't provide a security benefit because + // this key and these settings are already more secure than the recovery key. Increased cost + // here only slows down unlocking. + "--pbkdf", "argon2i", "--iter-time", "100", // set LUKS2 label "--label", label, + } + if options != nil { + if options.MetadataKiBSize != 0 { + args = append(args, + "--luks2-metadata-size", fmt.Sprintf("%dk", options.MetadataKiBSize)) + } + if options.KeyslotsAreaKiBSize != 0 { + args = append(args, + "--luks2-keyslots-size", fmt.Sprintf("%dk", options.KeyslotsAreaKiBSize)) + } + } + args = append(args, // device to format devicePath) + cmd := exec.Command("cryptsetup", args...) cmd.Stdin = bytes.NewReader(key) if output, err := cmd.CombinedOutput(); err != nil { return osutil.OutputErr(output, err) @@ -678,8 +1046,8 @@ // the new key. This is not a problem, because this function is intended to be called in the scenario that the default key cannot // be used to activate the LUKS2 container. func ChangeLUKS2KeyUsingRecoveryKey(devicePath string, recoveryKey RecoveryKey, key []byte) error { - if len(key) != 64 { - return fmt.Errorf("expected a key length of 512-bits (got %d)", len(key)*8) + if len(key) < 32 { + return fmt.Errorf("expected a key length of at least 256-bits (got %d)", len(key)*8) } cmd := exec.Command("cryptsetup", "luksKillSlot", "--key-file", "-", devicePath, "0") @@ -689,10 +1057,11 @@ } if err := addKeyToLUKS2Container(devicePath, recoveryKey[:], key, []string{ - // use argon2i as the KDF with minimum cost (lowest possible time and memory costs). This is done - // because the supplied input key has the same entropy (512-bits) as the derived key and therefore - // increased time or memory cost don't provide a security benefit (but does slow down unlocking). - "--pbkdf", "argon2i", "--pbkdf-force-iterations", "4", "--pbkdf-memory", "32", + // use argon2i as the KDF with reduced cost. This is done because the supplied input key has an + // entropy of at least 32 bytes, and increased cost doesn't provide a security benefit because + // this key and these settings are already more secure than the recovery key. Increased cost + // here only slows down unlocking. + "--pbkdf", "argon2i", "--iter-time", "100", // always have the main key in slot 0 for now "--key-slot", "0"}); err != nil { return err diff -Nru snapd-2.47.1+20.10.1build1/vendor/github.com/snapcore/secboot/efibootmanager_policy.go snapd-2.48+21.04/vendor/github.com/snapcore/secboot/efibootmanager_policy.go --- snapd-2.47.1+20.10.1build1/vendor/github.com/snapcore/secboot/efibootmanager_policy.go 2020-09-29 09:59:28.000000000 +0000 +++ snapd-2.48+21.04/vendor/github.com/snapcore/secboot/efibootmanager_policy.go 2020-11-18 09:44:21.000000000 +0000 @@ -29,7 +29,7 @@ "sort" "github.com/canonical/go-tpm2" - "github.com/chrisccoulson/tcglog-parser" + "github.com/canonical/tcglog-parser" "github.com/snapcore/secboot/internal/efi" "github.com/snapcore/secboot/internal/pe1.14" @@ -258,9 +258,9 @@ if err != nil { return xerrors.Errorf("cannot open TCG event log: %w", err) } - log, err := tcglog.NewLog(eventLog, tcglog.LogOptions{}) + log, err := tcglog.ParseLog(eventLog, &tcglog.LogOptions{}) if err != nil { - return xerrors.Errorf("cannot parse TCG event log header: %w", err) + return xerrors.Errorf("cannot parse TCG event log: %w", err) } if !log.Algorithms.Contains(tcglog.AlgorithmId(params.PCRAlgorithm)) { @@ -272,15 +272,7 @@ // Replay the event log until we see the transition from "pre-OS" to "OS-present". The event log may contain measurements // for system preparation applications, and spec-compliant firmware should measure a EV_EFI_ACTION “Calling EFI Application // from Boot Option” event before the EV_SEPARATOR event, but not all firmware does this. - for { - event, err := log.NextEvent() - if err == io.EOF { - break - } - if err != nil { - return xerrors.Errorf("cannot parse TCG event log: %w", err) - } - + for _, event := range log.Events { if event.PCRIndex != bootManagerCodePCR { continue } diff -Nru snapd-2.47.1+20.10.1build1/vendor/github.com/snapcore/secboot/errors.go snapd-2.48+21.04/vendor/github.com/snapcore/secboot/errors.go --- snapd-2.47.1+20.10.1build1/vendor/github.com/snapcore/secboot/errors.go 2020-07-16 11:24:42.000000000 +0000 +++ snapd-2.48+21.04/vendor/github.com/snapcore/secboot/errors.go 2020-11-18 09:44:21.000000000 +0000 @@ -20,6 +20,7 @@ package secboot import ( + "bytes" "errors" "fmt" @@ -29,10 +30,15 @@ ) var ( - // ErrTPMClearRequiresPPI is returned from ProvisionTPM and indicates that clearing the TPM must be performed via + // ErrTPMClearRequiresPPI is returned from TPMConnection.EnsureProvisioned and indicates that clearing the TPM must be performed via // the Physical Presence Interface. ErrTPMClearRequiresPPI = errors.New("clearing the TPM requires the use of the Physical Presence Interface") + // ErrTPMProvisioningRequiresLockout is returned from TPMConnection.EnsureProvisioned when fully provisioning the TPM requires + // the use of the lockout hierarchy. In this case, the provisioning steps that can be performed without the use of the lockout + // hierarchy are completed. + ErrTPMProvisioningRequiresLockout = errors.New("provisioning the TPM requires the use of the lockout hierarchy") + // ErrTPMProvisioning indicates that the TPM is not provisioned correctly for the requested operation. Please note that other errors // that can be returned may also be caused by incomplete provisioning, as it is not always possible to detect incomplete or // incorrect provisioning in all contexts. @@ -46,12 +52,12 @@ // ErrPINFail is returned from SealedKeyObject.UnsealFromTPM if the provided PIN is incorrect. ErrPINFail = errors.New("the provided PIN is incorrect") - // ErrSealedKeyAccessLocked is returned from SealedKeyObject.UnsealFromTPM if the sealed key object cannot be unsealed until the - // next TPM reset or restart. - ErrSealedKeyAccessLocked = errors.New("cannot access the sealed key object until the next TPM reset or restart") - // ErrNoTPM2Device is returned from ConnectToDefaultTPM or SecureConnectToDefaultTPM if no TPM2 device is avaiable. ErrNoTPM2Device = errors.New("no TPM2 device is available") + + // ErrNoActivationData is returned from GetActivationDataFromKernel if no activation data was found in the user keyring for + // the specified block device. + ErrNoActivationData = errors.New("no activation data found for the specified device") ) // TPMResourceExistsError is returned from any function that creates a persistent TPM resource if a resource already exists @@ -125,14 +131,6 @@ return xerrors.As(err, &e) } -// LockAccessToSealedKeysError is returned from ActivateVolumeWithTPMSealedKey if an error occurred whilst trying to lock access -// to sealed keys created by this package. -type LockAccessToSealedKeysError string - -func (e LockAccessToSealedKeysError) Error() string { - return "cannot lock access to sealed keys: " + string(e) -} - // ActivateWithTPMSealedKeyError is returned from ActivateVolumeWithTPMSealedKey if activation with the TPM protected key failed. type ActivateWithTPMSealedKeyError struct { // TPMErr details the error that occurred during activation with the TPM sealed key. @@ -149,3 +147,28 @@ } return fmt.Sprintf("cannot activate with TPM sealed key (%v) but activation with recovery key was successful", e.TPMErr) } + +// ActivateWithMultipleTPMSealedKeysError is returned from ActivateVolumeWithMultipleTPMSealedKeys if activation with the +// TPM protected keys failed. +type ActivateWithMultipleTPMSealedKeysError struct { + // TPMErrs details the errors that occurred during activation with the TPM sealed keys. + TPMErrs []error + + // RecoveryKeyUsageErr details the error that occurred during activation with the fallback recovery key, if activation + // with the recovery key was also unsuccessful. + RecoveryKeyUsageErr error +} + +func (e *ActivateWithMultipleTPMSealedKeysError) Error() string { + var s bytes.Buffer + fmt.Fprintf(&s, "cannot activate with TPM sealed keys:") + for _, err := range e.TPMErrs { + fmt.Fprintf(&s, "\n- %v", err) + } + if e.RecoveryKeyUsageErr != nil { + fmt.Fprintf(&s, "\nand activation with recovery key failed: %v", e.RecoveryKeyUsageErr) + } else { + fmt.Fprintf(&s, "\nbut activation with recovery key was successful") + } + return s.String() +} diff -Nru snapd-2.47.1+20.10.1build1/vendor/github.com/snapcore/secboot/keydata.go snapd-2.48+21.04/vendor/github.com/snapcore/secboot/keydata.go --- snapd-2.47.1+20.10.1build1/vendor/github.com/snapcore/secboot/keydata.go 2020-09-29 09:59:28.000000000 +0000 +++ snapd-2.48+21.04/vendor/github.com/snapcore/secboot/keydata.go 2020-11-18 09:44:21.000000000 +0000 @@ -22,10 +22,12 @@ import ( "bytes" "crypto" + "crypto/ecdsa" "crypto/rsa" "crypto/x509" "errors" "fmt" + "hash" "io" "math/big" "os" @@ -37,10 +39,12 @@ "github.com/snapcore/snapd/osutil/sys" "golang.org/x/xerrors" + + "maze.io/x/crypto/afis" ) const ( - currentMetadataVersion uint32 = 0 + currentMetadataVersion uint32 = 1 keyDataHeader uint32 = 0x55534b24 keyPolicyUpdateDataHeader uint32 = 0x55534b50 ) @@ -53,6 +57,105 @@ AuthModePIN ) +// TPMPolicyAuthKey corresponds to the private part of the key used for signing updates to the authorization policy for a sealed key. +type TPMPolicyAuthKey []byte + +type sealedData struct { + Key []byte + AuthPrivateKey TPMPolicyAuthKey +} + +type afSplitDataRawHdr struct { + Stripes uint32 + HashAlg tpm2.HashAlgorithmId + Size uint32 +} + +// afSlitDataRaw is the on-disk version of afSplitData. +type afSplitDataRaw struct { + Hdr afSplitDataRawHdr + Data mu.RawBytes +} + +func (d *afSplitDataRaw) Marshal(w io.Writer) error { + _, err := mu.MarshalToWriter(w, d.Hdr, d.Data) + return err +} + +func (d *afSplitDataRaw) Unmarshal(r mu.Reader) error { + var h afSplitDataRawHdr + if _, err := mu.UnmarshalFromReader(r, &h); err != nil { + return xerrors.Errorf("cannot unmarshal header: %w", err) + } + + data := make([]byte, h.Size) + if _, err := io.ReadFull(r, data); err != nil { + return xerrors.Errorf("cannot read data: %w", err) + } + + d.Hdr = h + d.Data = data + return nil +} + +func (d *afSplitDataRaw) data() *afSplitData { + return &afSplitData{ + stripes: d.Hdr.Stripes, + hashAlg: d.Hdr.HashAlg, + data: d.Data} +} + +// makeAfSplitDataRaw converts afSplitData to its on disk form. +func makeAfSplitDataRaw(d *afSplitData) *afSplitDataRaw { + return &afSplitDataRaw{ + Hdr: afSplitDataRawHdr{ + Stripes: d.stripes, + HashAlg: d.hashAlg, + Size: uint32(len(d.data))}, + Data: d.data} +} + +// afSplitData is a container for data that has been passed through an Anti-Forensic Information Splitter, to support +// secure destruction of on-disk key material by increasing the size of the data stored and requiring every bit to survive +// in order to recover the original data. +type afSplitData struct { + stripes uint32 + hashAlg tpm2.HashAlgorithmId + data []byte +} + +// merge recovers the original data from this container. +func (d *afSplitData) merge() ([]byte, error) { + if d.stripes < 1 { + return nil, errors.New("invalid number of stripes") + } + if !d.hashAlg.Supported() { + return nil, errors.New("unsupported digest algorithm") + } + return afis.MergeHash(d.data, int(d.stripes), func() hash.Hash { return d.hashAlg.NewHash() }) +} + +// makeAfSplitData passes the supplied data through an Anti-Forensic Information Splitter to increase the size of the data to at +// least the size specified by the minSz argument. +func makeAfSplitData(data []byte, minSz int, hashAlg tpm2.HashAlgorithmId) (*afSplitData, error) { + stripes := uint32((minSz / len(data)) + 1) + + split, err := afis.SplitHash(data, int(stripes), func() hash.Hash { return hashAlg.NewHash() }) + if err != nil { + return nil, err + } + + return &afSplitData{stripes: stripes, hashAlg: hashAlg, data: split}, nil +} + +func (d *afSplitData) Marshal(w io.Writer) (nbytes int, err error) { + panic("cannot be marshalled") +} + +func (d *afSplitData) Unmarshal(r io.Reader) (nbytes int, err error) { + panic("cannot be unmarshalled") +} + // keyPolicyUpdateDataRaw_v0 is version 0 of the on-disk format of keyPolicyUpdateData. type keyPolicyUpdateDataRaw_v0 struct { AuthKey []byte @@ -64,19 +167,14 @@ // authorization policies. type keyPolicyUpdateData struct { version uint32 - authKey *rsa.PrivateKey + authKey crypto.PrivateKey creationInfo tpm2.Data creationData *tpm2.CreationData creationTicket *tpm2.TkCreation } func (d *keyPolicyUpdateData) Marshal(w io.Writer) error { - raw := &keyPolicyUpdateDataRaw_v0{ - AuthKey: x509.MarshalPKCS1PrivateKey(d.authKey), - CreationData: d.creationData, - CreationTicket: d.creationTicket} - _, err := mu.MarshalToWriter(w, d.version, raw) - return err + panic("not implemented") } func (d *keyPolicyUpdateData) Unmarshal(r mu.Reader) error { @@ -103,7 +201,7 @@ } *d = keyPolicyUpdateData{ - version: 0, + version: version, authKey: authKey, creationInfo: h.Sum(nil), creationData: raw.CreationData, @@ -114,19 +212,6 @@ return nil } -// write serializes keyPolicyUpdateData to the provided io.Writer. -func (d *keyPolicyUpdateData) write(buf io.Writer) error { - if d.version != currentMetadataVersion { - return errors.New("writing old metadata versions is not supported") - } - - if _, err := mu.MarshalToWriter(buf, keyPolicyUpdateDataHeader, d); err != nil { - return err - } - - return nil -} - // decodeKeyPolicyUpdateData deserializes keyPolicyUpdateData from the provided io.Reader. func decodeKeyPolicyUpdateData(r io.Reader) (*keyPolicyUpdateData, error) { var header uint32 @@ -154,6 +239,15 @@ DynamicPolicyData *dynamicPolicyDataRaw_v0 } +// keyDataRaw_v1 is version 1 of the on-disk format of keyDataRaw. +type keyDataRaw_v1 struct { + KeyPrivate tpm2.Private + KeyPublic *tpm2.Public + AuthModeHint AuthMode + StaticPolicyData *staticPolicyDataRaw_v1 + DynamicPolicyData *dynamicPolicyDataRaw_v0 +} + // keyData corresponds to the part of a sealed key object that contains the TPM sealed object and associated metadata required // for executing authorization policy assertions. type keyData struct { @@ -181,6 +275,24 @@ if _, err := mu.MarshalToWriter(w, raw); err != nil { return xerrors.Errorf("cannot marshal raw data: %w", err) } + case 1: + var tmpW bytes.Buffer + raw := keyDataRaw_v1{ + KeyPrivate: d.keyPrivate, + KeyPublic: d.keyPublic, + AuthModeHint: d.authModeHint, + StaticPolicyData: makeStaticPolicyDataRaw_v1(d.staticPolicyData), + DynamicPolicyData: makeDynamicPolicyDataRaw_v0(d.dynamicPolicyData)} + if _, err := mu.MarshalToWriter(&tmpW, raw); err != nil { + return xerrors.Errorf("cannot marshal raw data: %w", err) + } + splitData, err := makeAfSplitData(tmpW.Bytes(), 128*1024, tpm2.HashAlgorithmSHA256) + if err != nil { + return xerrors.Errorf("cannot split data: %w", err) + } + if _, err := mu.MarshalToWriter(w, makeAfSplitDataRaw(splitData)); err != nil { + return xerrors.Errorf("cannot marshal split data: %w", err) + } default: return fmt.Errorf("unexpected version number (%d)", d.version) } @@ -200,7 +312,29 @@ return xerrors.Errorf("cannot unmarshal data: %w", err) } *d = keyData{ - version: 0, + version: version, + keyPrivate: raw.KeyPrivate, + keyPublic: raw.KeyPublic, + authModeHint: raw.AuthModeHint, + staticPolicyData: raw.StaticPolicyData.data(), + dynamicPolicyData: raw.DynamicPolicyData.data()} + case 1: + var splitData afSplitDataRaw + if _, err := mu.UnmarshalFromReader(r, &splitData); err != nil { + return xerrors.Errorf("cannot unmarshal split data: %w", err) + } + + merged, err := splitData.data().merge() + if err != nil { + return xerrors.Errorf("cannot merge data: %w", err) + } + + var raw keyDataRaw_v1 + if _, err := mu.UnmarshalFromBytes(merged, &raw); err != nil { + return xerrors.Errorf("cannot unmarshal data: %w", err) + } + *d = keyData{ + version: version, keyPrivate: raw.KeyPrivate, keyPublic: raw.KeyPublic, authModeHint: raw.AuthModeHint, @@ -238,12 +372,11 @@ return keyContext, nil } -// validate performs some correctness checking on the provided keyData and keyPolicyUpdateData. On success, it returns the validated -// public area for the PIN NV index. -func (d *keyData) validate(tpm *tpm2.TPMContext, policyUpdateData *keyPolicyUpdateData, session tpm2.SessionContext) (*tpm2.NVPublic, error) { - srkContext, err := tpm.CreateResourceContextFromTPM(tcg.SRKHandle) - if err != nil { - return nil, xerrors.Errorf("cannot create context for SRK: %w", err) +// validate performs some correctness checking on the provided keyData and authKey. On success, it returns the validated public area +// for the PCR policy counter. +func (d *keyData) validate(tpm *tpm2.TPMContext, authKey crypto.PrivateKey, session tpm2.SessionContext) (*tpm2.NVPublic, error) { + if d.version > currentMetadataVersion { + return nil, keyFileError{errors.New("invalid metadata version")} } sealedKeyTemplate := makeSealedKeyTemplate() @@ -259,136 +392,163 @@ } // Load the sealed data object in to the TPM for integrity checking - keyContext, err := tpm.Load(srkContext, d.keyPrivate, keyPublic, session) + keyContext, err := d.load(tpm, session) if err != nil { - invalidObject := false - switch { - case tpm2.IsTPMParameterError(err, tpm2.AnyErrorCode, tpm2.CommandLoad, tpm2.AnyParameterIndex): - invalidObject = true - case tpm2.IsTPMError(err, tpm2.ErrorSensitive, tpm2.CommandLoad): - invalidObject = true - } - if invalidObject { - return nil, keyFileError{errors.New("cannot load sealed key object in to TPM: bad sealed key object or TPM owner changed")} - } - return nil, xerrors.Errorf("cannot load sealed key object in to TPM: %w", err) + return nil, err } // It's loaded ok, so we know that the private and public parts are consistent. - defer tpm.FlushContext(keyContext) + tpm.FlushContext(keyContext) - lockIndex, err := tpm.CreateResourceContextFromTPM(lockNVHandle) - if err != nil { - return nil, xerrors.Errorf("cannot create context for lock NV index: %v", err) - } - lockIndexPub, err := readAndValidateLockNVIndexPublic(tpm, lockIndex, session) - if err != nil { - return nil, xerrors.Errorf("cannot determine if NV index at %v is global lock index: %w", lockNVHandle, err) - } - if lockIndexPub == nil { - return nil, xerrors.Errorf("NV index at %v is not a valid global lock index", lockNVHandle) - } - lockIndexName, err := lockIndexPub.Name() - if err != nil { - return nil, xerrors.Errorf("cannot compute lock NV index name: %w", err) + var legacyLockIndexName tpm2.Name + if d.version == 0 { + index, err := tpm.CreateResourceContextFromTPM(lockNVHandle, session.IncludeAttrs(tpm2.AttrAudit)) + if err != nil { + if tpm2.IsResourceUnavailableError(err, lockNVHandle) { + return nil, keyFileError{errors.New("lock NV index is unavailable")} + } + return nil, xerrors.Errorf("cannot create context for lock NV index: %w", err) + } + indexPub, _, err := tpm.NVReadPublic(index, session.IncludeAttrs(tpm2.AttrAudit)) + if err != nil { + return nil, xerrors.Errorf("cannot read public area of lock NV index: %w", err) + } + indexPub.Attrs &^= tpm2.AttrNVReadLocked + legacyLockIndexName, err = indexPub.Name() + if err != nil { + return nil, xerrors.Errorf("cannot compute name of lock NV index: %w", err) + } } - // Obtain a ResourceContext for the PIN NV index. Go-tpm2 calls TPM2_NV_ReadPublic twice here. The second time is with a session, and - // there is also verification that the returned public area is for the specified handle so that we know that the returned - // ResourceContext corresponds to an actual entity on the TPM at PinIndexHandle. - pinIndexHandle := d.staticPolicyData.PinIndexHandle - if pinIndexHandle.Type() != tpm2.HandleTypeNVIndex { - return nil, keyFileError{errors.New("PIN NV index handle is invalid")} + // Obtain a ResourceContext for the PCR policy counter. Go-tpm2 calls TPM2_NV_ReadPublic twice here. The second time is with a + // session, and there is also verification that the returned public area is for the specified handle so that we know that the + // returned ResourceContext corresponds to an actual entity on the TPM at the specified handle. This index is used for PCR policy + // revocation, and also for PIN integration with v0 metadata only. + pcrPolicyCounterHandle := d.staticPolicyData.pcrPolicyCounterHandle + if (pcrPolicyCounterHandle != tpm2.HandleNull || d.version == 0) && pcrPolicyCounterHandle.Type() != tpm2.HandleTypeNVIndex { + return nil, keyFileError{errors.New("PCR policy counter handle is invalid")} } - pinIndex, err := tpm.CreateResourceContextFromTPM(pinIndexHandle, session.IncludeAttrs(tpm2.AttrAudit)) - if err != nil { - if tpm2.IsResourceUnavailableError(err, pinIndexHandle) { - return nil, keyFileError{errors.New("PIN NV index is unavailable")} + + var pcrPolicyCounter tpm2.ResourceContext + if pcrPolicyCounterHandle != tpm2.HandleNull { + pcrPolicyCounter, err = tpm.CreateResourceContextFromTPM(pcrPolicyCounterHandle, session.IncludeAttrs(tpm2.AttrAudit)) + if err != nil { + if tpm2.IsResourceUnavailableError(err, pcrPolicyCounterHandle) { + return nil, keyFileError{errors.New("PCR policy counter is unavailable")} + } + return nil, xerrors.Errorf("cannot create context for PCR policy counter: %w", err) } - return nil, xerrors.Errorf("cannot create context for PIN NV index: %w", err) } - authPublicKey := d.staticPolicyData.AuthPublicKey + var pcrPolicyRef tpm2.Nonce + if d.version > 0 { + pcrPolicyRef = computePcrPolicyRefFromCounterContext(pcrPolicyCounter) + } + + // Validate the type and scheme of the dynamic authorization policy signing key. + authPublicKey := d.staticPolicyData.authPublicKey authKeyName, err := authPublicKey.Name() if err != nil { return nil, keyFileError{xerrors.Errorf("cannot compute name of dynamic authorization policy key: %w", err)} } - if authPublicKey.Type != tpm2.ObjectTypeRSA { + var expectedAuthKeyType tpm2.ObjectTypeId + var expectedAuthKeyScheme tpm2.AsymSchemeId + switch d.version { + case 0: + expectedAuthKeyType = tpm2.ObjectTypeRSA + expectedAuthKeyScheme = tpm2.AsymSchemeRSAPSS + default: + expectedAuthKeyType = tpm2.ObjectTypeECC + expectedAuthKeyScheme = tpm2.AsymSchemeECDSA + } + if authPublicKey.Type != expectedAuthKeyType { return nil, keyFileError{errors.New("public area of dynamic authorization policy signing key has the wrong type")} } + authKeyScheme := authPublicKey.Params.AsymDetail().Scheme + if authKeyScheme.Scheme != tpm2.AsymSchemeNull { + if authKeyScheme.Scheme != expectedAuthKeyScheme { + return nil, keyFileError{errors.New("dynamic authorization policy signing key has unexpected scheme")} + } + if authKeyScheme.Details.Any().HashAlg != authPublicKey.NameAlg { + return nil, keyFileError{errors.New("dynamic authorization policy signing key algorithm must match name algorithm")} + } + } // Make sure that the static authorization policy data is consistent with the sealed key object's policy. trial, err := tpm2.ComputeAuthPolicy(keyPublic.NameAlg) if err != nil { return nil, keyFileError{xerrors.Errorf("cannot determine if static authorization policy matches sealed key object: %w", err)} } - trial.PolicyAuthorize(nil, authKeyName) - trial.PolicySecret(pinIndex.Name(), nil) - trial.PolicyNV(lockIndexName, nil, 0, tpm2.OpEq) - if !bytes.Equal(trial.GetDigest(), keyPublic.AuthPolicy) { - return nil, keyFileError{errors.New("the sealed key object's authorization policy is inconsistent with the associatedc metadata or persistent TPM resources")} + trial.PolicyAuthorize(pcrPolicyRef, authKeyName) + if d.version == 0 { + trial.PolicySecret(pcrPolicyCounter.Name(), nil) + trial.PolicyNV(legacyLockIndexName, nil, 0, tpm2.OpEq) + } else { + // v1 metadata and later + trial.PolicyAuthValue() } - pinIndexPublic, _, err := tpm.NVReadPublic(pinIndex, session.IncludeAttrs(tpm2.AttrAudit)) - if err != nil { - return nil, xerrors.Errorf("cannot read public area of PIN NV index: %w", err) + if !bytes.Equal(trial.GetDigest(), keyPublic.AuthPolicy) { + return nil, keyFileError{errors.New("the sealed key object's authorization policy is inconsistent with the associated metadata or persistent TPM resources")} } - pinIndexAuthPolicies := d.staticPolicyData.PinIndexAuthPolicies - expectedPinIndexAuthPolicies, err := computePinNVIndexPostInitAuthPolicies(pinIndexPublic.NameAlg, authKeyName) - if err != nil { - return nil, keyFileError{xerrors.Errorf("cannot determine if PIN NV index has a valid authorization policy: %w", err)} - } - if len(pinIndexAuthPolicies)-1 != len(expectedPinIndexAuthPolicies) { - return nil, keyFileError{errors.New("unexpected number of OR policy digests for PIN NV index")} - } - for i, expected := range expectedPinIndexAuthPolicies { - if !bytes.Equal(expected, pinIndexAuthPolicies[i+1]) { - return nil, keyFileError{errors.New("unexpected OR policy digest for PIN NV index")} + // Read the public area of the PCR policy counter + var pcrPolicyCounterPub *tpm2.NVPublic + if pcrPolicyCounter != nil { + pcrPolicyCounterPub, _, err = tpm.NVReadPublic(pcrPolicyCounter, session.IncludeAttrs(tpm2.AttrAudit)) + if err != nil { + return nil, xerrors.Errorf("cannot read public area of PCR policy counter: %w", err) } } - trial, _ = tpm2.ComputeAuthPolicy(pinIndexPublic.NameAlg) - trial.PolicyOR(pinIndexAuthPolicies) - if !bytes.Equal(pinIndexPublic.AuthPolicy, trial.GetDigest()) { - return nil, keyFileError{errors.New("PIN NV index has unexpected authorization policy")} + // For v0 metadata, validate that the OR policy digests for the PCR policy counter match the public area of the index. + if d.version == 0 { + pcrPolicyCounterAuthPolicies := d.staticPolicyData.v0PinIndexAuthPolicies + expectedPcrPolicyCounterAuthPolicies, err := computeV0PinNVIndexPostInitAuthPolicies(pcrPolicyCounterPub.NameAlg, authKeyName) + if err != nil { + return nil, keyFileError{xerrors.Errorf("cannot determine if PCR policy counter has a valid authorization policy: %w", err)} + } + if len(pcrPolicyCounterAuthPolicies)-1 != len(expectedPcrPolicyCounterAuthPolicies) { + return nil, keyFileError{errors.New("unexpected number of OR policy digests for PCR policy counter")} + } + for i, expected := range expectedPcrPolicyCounterAuthPolicies { + if !bytes.Equal(expected, pcrPolicyCounterAuthPolicies[i+1]) { + return nil, keyFileError{errors.New("unexpected OR policy digest for PCR policy counter")} + } + } + + trial, _ = tpm2.ComputeAuthPolicy(pcrPolicyCounterPub.NameAlg) + trial.PolicyOR(pcrPolicyCounterAuthPolicies) + if !bytes.Equal(pcrPolicyCounterPub.AuthPolicy, trial.GetDigest()) { + return nil, keyFileError{errors.New("PCR policy counter has unexpected authorization policy")} + } } // At this point, we know that the sealed object is an object with an authorization policy created by this package and with // matching static metadata and persistent TPM resources. - if policyUpdateData == nil { - // If we weren't passed a private data structure, we're done. - return pinIndexPublic, nil - } - - // Verify that the private data structure is bound to the key data structure. - h := keyPublic.NameAlg.NewHash() - if _, err := mu.MarshalToWriter(h, policyUpdateData.creationData); err != nil { - panic(fmt.Sprintf("cannot marshal creation data: %v", err)) - } - - if _, _, err := tpm.CertifyCreation(nil, keyContext, nil, h.Sum(nil), nil, policyUpdateData.creationTicket, nil, - session.IncludeAttrs(tpm2.AttrAudit)); err != nil { - if tpm2.IsTPMParameterError(err, tpm2.ErrorTicket, tpm2.CommandCertifyCreation, 4) { - return nil, keyFileError{errors.New("key data file and dynamic authorization policy update data file mismatch: invalid creation ticket")} + switch k := authKey.(type) { + case *rsa.PrivateKey: + goAuthPublicKey := rsa.PublicKey{ + N: new(big.Int).SetBytes(authPublicKey.Unique.RSA()), + E: int(authPublicKey.Params.RSADetail().Exponent)} + if k.E != goAuthPublicKey.E || k.N.Cmp(goAuthPublicKey.N) != 0 { + return nil, keyFileError{errors.New("dynamic authorization policy signing private key doesn't match public key")} + } + case *ecdsa.PrivateKey: + if d.version == 0 { + return nil, keyFileError{errors.New("unexpected dynamic authorization policy signing private key type")} + } + expectedX, expectedY := k.Curve.ScalarBaseMult(k.D.Bytes()) + if expectedX.Cmp(k.X) != 0 || expectedY.Cmp(k.Y) != 0 { + return nil, keyFileError{errors.New("dynamic authorization policy signing private key doesn't match public key")} } - return nil, xerrors.Errorf("cannot validate creation data for sealed data object: %w", err) - } - - if !bytes.Equal(policyUpdateData.creationInfo, policyUpdateData.creationData.OutsideInfo) { - return nil, keyFileError{errors.New("key data file and dynamic authorization policy update data file mismatch: digest doesn't match creation data")} - } - - authKey := policyUpdateData.authKey - goAuthPublicKey := rsa.PublicKey{ - N: new(big.Int).SetBytes(authPublicKey.Unique.RSA()), - E: int(authPublicKey.Params.RSADetail().Exponent)} - if authKey.E != goAuthPublicKey.E || authKey.N.Cmp(goAuthPublicKey.N) != 0 { - return nil, keyFileError{errors.New("dynamic authorization policy signing private key doesn't match public key")} + case nil: + default: + return nil, keyFileError{errors.New("unexpected dynamic authorization policy signing private key type")} } - return pinIndexPublic, nil + return pcrPolicyCounterPub, nil } // write serializes keyData in to the provided io.Writer. @@ -453,30 +613,50 @@ return xerrors.As(err, &e) } -// decodeAndValidateKeyData will deserialize keyData and keyPolicyUpdateData from the provided io.Readers and then perform some correctness -// checking. On success, it returns the keyData, keyPolicyUpdateData and the validated public area of the PIN NV index. -func decodeAndValidateKeyData(tpm *tpm2.TPMContext, keyFile, keyPolicyUpdateFile io.Reader, session tpm2.SessionContext) (*keyData, *keyPolicyUpdateData, *tpm2.NVPublic, error) { +// decodeAndValidateKeyData will deserialize keyData from the provided io.Reader and then perform some correctness checking. On +// success, it returns the keyData, dynamic authorization policy signing key (if authData is provided) and the validated public area +// of the PCR policy counter index. +func decodeAndValidateKeyData(tpm *tpm2.TPMContext, keyFile io.Reader, authData interface{}, session tpm2.SessionContext) (*keyData, crypto.PrivateKey, *tpm2.NVPublic, error) { // Read the key data data, err := decodeKeyData(keyFile) if err != nil { return nil, nil, nil, keyFileError{xerrors.Errorf("cannot read key data: %w", err)} } - var policyUpdateData *keyPolicyUpdateData - if keyPolicyUpdateFile != nil { - var err error - policyUpdateData, err = decodeKeyPolicyUpdateData(keyPolicyUpdateFile) + var authKey crypto.PrivateKey + + switch a := authData.(type) { + case io.Reader: + // If we were called with an io.Reader, then we're expecting to load a legacy version-0 keydata and associated + // private key file. + policyUpdateData, err := decodeKeyPolicyUpdateData(a) if err != nil { return nil, nil, nil, keyFileError{xerrors.Errorf("cannot read dynamic policy update data: %w", err)} } + if policyUpdateData.version != data.version { + return nil, nil, nil, keyFileError{errors.New("mismatched metadata versions")} + } + authKey = policyUpdateData.authKey + case TPMPolicyAuthKey: + if len(a) > 0 { + // If we were called with a byte slice, then we're expecting to load the current keydata version and the byte + // slice is the private part of the elliptic auth key. + authKey, err = createECDSAPrivateKeyFromTPM(data.staticPolicyData.authPublicKey, tpm2.ECCParameter(a)) + if err != nil { + return nil, nil, nil, keyFileError{xerrors.Errorf("cannot create auth key: %w", err)} + } + } + case nil: + default: + panic("invalid type") } - pinNVPublic, err := data.validate(tpm, policyUpdateData, session) + pcrPolicyCounterPub, err := data.validate(tpm, authKey, session) if err != nil { return nil, nil, nil, xerrors.Errorf("cannot validate key data: %w", err) } - return data, policyUpdateData, pinNVPublic, nil + return data, authKey, pcrPolicyCounterPub, nil } // SealedKeyObject corresponds to a sealed key data file and exists to provide access to some read only operations on the underlying @@ -485,14 +665,20 @@ data *keyData } +// Version returns the version number that this sealed key object was created with. +func (k *SealedKeyObject) Version() uint32 { + return k.data.version +} + // AuthMode2F indicates the 2nd-factor authentication type for this sealed key object. func (k *SealedKeyObject) AuthMode2F() AuthMode { return k.data.authModeHint } -// PINIndexHandle indicates the handle of the NV index used for PIN support for this sealed key object. -func (k *SealedKeyObject) PINIndexHandle() tpm2.Handle { - return k.data.staticPolicyData.PinIndexHandle +// PCRPolicyCounterHandle indicates the handle of the NV counter used for PCR policy revocation for this sealed key object (and for +// PIN integration for version 0 key files). +func (k *SealedKeyObject) PCRPolicyCounterHandle() tpm2.Handle { + return k.data.staticPolicyData.pcrPolicyCounterHandle } // ReadSealedKeyObject loads a sealed key data file created by SealKeyToTPM from the specified path. If the file cannot be opened, diff -Nru snapd-2.47.1+20.10.1build1/vendor/github.com/snapcore/secboot/pin.go snapd-2.48+21.04/vendor/github.com/snapcore/secboot/pin.go --- snapd-2.47.1+20.10.1build1/vendor/github.com/snapcore/secboot/pin.go 2020-07-16 11:24:42.000000000 +0000 +++ snapd-2.48+21.04/vendor/github.com/snapcore/secboot/pin.go 2020-11-18 09:44:21.000000000 +0000 @@ -20,24 +20,22 @@ package secboot import ( - "crypto/rand" - "crypto/rsa" - "encoding/binary" "os" "github.com/canonical/go-tpm2" + "github.com/snapcore/secboot/internal/tcg" "golang.org/x/xerrors" ) -// computePinNVIndexPostInitAuthPolicies computes the authorization policy digests associated with the post-initialization -// actions on a NV index created with createPinNVIndex. These are: +// computeV0PinNVIndexPostInitAuthPolicies computes the authorization policy digests associated with the post-initialization +// actions on a NV index created with the removed createPinNVIndex for version 0 key files. These are: // - A policy for updating the index to revoke old dynamic authorization policies, requiring an assertion signed by the key // associated with updateKeyName. // - A policy for updating the authorization value (PIN / passphrase), requiring knowledge of the current authorization value. // - A policy for reading the counter value without knowing the authorization value, as the value isn't secret. // - A policy for using the counter value in a TPM2_PolicyNV assertion without knowing the authorization value. -func computePinNVIndexPostInitAuthPolicies(alg tpm2.HashAlgorithmId, updateKeyName tpm2.Name) (tpm2.DigestList, error) { +func computeV0PinNVIndexPostInitAuthPolicies(alg tpm2.HashAlgorithmId, updateKeyName tpm2.Name) (tpm2.DigestList, error) { var out tpm2.DigestList // Compute a policy for incrementing the index to revoke dynamic authorization policies, requiring an assertion signed by the // key associated with updateKeyName. @@ -78,152 +76,13 @@ return out, nil } -// createPinNVIndex creates a NV index that is associated with a sealed key object and is used for implementing PIN support. It is -// also used as a counter to support revoking of dynamic authorization policies. +// performPinChangeV0 changes the authorization value of the dynamic authorization policy counter associated with the public +// argument, for PIN integration in version 0 key files. This requires the authorization policy digests initially returned from +// (the now removed) createPinNVIndex function in order to execute the policy session required to change the authorization value. +// The current authorization value must be provided via the oldAuth argument. // -// To prevent someone with knowledge of the owner authorization (which is empty unless someone has taken ownership of the TPM) from -// resetting the PIN by just undefining and redifining a new NV index with the same properties, we need a way to prevent someone -// from being able to create an index with the same name. To do this, we require the NV index to have been written to and only allow -// the initial write with a signed authorization policy. Once initialized, the signing key that authorized the initial write is -// discarded. This works because the name of the signing key is included in the authorization policy digest for the NV index, and the -// authorization policy digest and attributes are included in the name of the NV index. Without the private part of the signing key, -// it is impossible to create a new NV index with the same name, and so, if this NV index is undefined then it becomes impossible to -// satisfy the authorization policy for the sealed key object to which it is associated. -// -// The NV index will be created with an authorization policy that permits TPM2_NV_Read and TPM2_PolicyNV without knowing the PIN, -// and an authorization policy that permits TPM2_NV_Increment with a signed authorization policy, signed by the key associated with -// updateKeyName. -func createPinNVIndex(tpm *tpm2.TPMContext, handle tpm2.Handle, updateKeyName tpm2.Name, hmacSession tpm2.SessionContext) (*tpm2.NVPublic, tpm2.DigestList, error) { - initKey, err := rsa.GenerateKey(rand.Reader, 2048) - if err != nil { - return nil, nil, xerrors.Errorf("cannot create signing key for initializing NV index: %w", err) - } - - initKeyPublic := createPublicAreaForRSASigningKey(&initKey.PublicKey) - initKeyName, err := initKeyPublic.Name() - if err != nil { - return nil, nil, xerrors.Errorf("cannot compute name of signing key for initializing NV index: %w", err) - } - - nameAlg := tpm2.HashAlgorithmSHA256 - - // The NV index requires 5 policies: - // - A policy for initializing the index, requiring an assertion signed with an ephemeral key so that the index cannot be recreated. - // - A policy for updating the index to revoke old dynamic authorization policies, requiring a signed assertion. - // - A policy for updating the authorization value (PIN / passphrase), requiring knowledge of the current authorization value. - // - A policy for reading the counter value without knowing the authorization value, as the value isn't secret. - // - A policy for using the counter value in a TPM2_PolicyNV assertion without knowing the authorization value. - var authPolicies tpm2.DigestList - - // Compute a policy for initialization which requires an assertion signed with an ephemeral key (initKey) - trial, _ := tpm2.ComputeAuthPolicy(nameAlg) - trial.PolicyCommandCode(tpm2.CommandNVIncrement) - trial.PolicyNvWritten(false) - trial.PolicySigned(initKeyName, nil) - authPolicies = append(authPolicies, trial.GetDigest()) - - // Compute the remaining 4 post-initalization policies. - postInitAuthPolicies, err := computePinNVIndexPostInitAuthPolicies(nameAlg, updateKeyName) - if err != nil { - return nil, nil, xerrors.Errorf("cannot compute authorization policies: %w", err) - } - authPolicies = append(authPolicies, postInitAuthPolicies...) - - trial, _ = tpm2.ComputeAuthPolicy(nameAlg) - trial.PolicyOR(authPolicies) - - // Define the NV index - public := &tpm2.NVPublic{ - Index: handle, - NameAlg: nameAlg, - Attrs: tpm2.NVTypeCounter.WithAttrs(tpm2.AttrNVPolicyWrite | tpm2.AttrNVAuthRead | tpm2.AttrNVPolicyRead), - AuthPolicy: trial.GetDigest(), - Size: 8} - - index, err := tpm.NVDefineSpace(tpm.OwnerHandleContext(), nil, public, hmacSession) - if err != nil { - return nil, nil, xerrors.Errorf("cannot define NV space: %w", err) - } - - // NVDefineSpace was integrity protected, so we know that we have an index with the expected public area at the handle we specified - // at this point. - - succeeded := false - defer func() { - if succeeded { - return - } - tpm.NVUndefineSpace(tpm.OwnerHandleContext(), index, hmacSession) - }() - - // Begin a session to initialize the index. - policySession, err := tpm.StartAuthSession(nil, nil, tpm2.SessionTypePolicy, nil, nameAlg) - if err != nil { - return nil, nil, xerrors.Errorf("cannot begin policy session to initialize NV index: %w", err) - } - defer tpm.FlushContext(policySession) - - // Compute a digest for signing with our key - signDigest := tpm2.HashAlgorithmSHA256 - h := signDigest.NewHash() - h.Write(policySession.NonceTPM()) - binary.Write(h, binary.BigEndian, int32(0)) - - // Sign the digest - sig, err := rsa.SignPSS(rand.Reader, initKey, signDigest.GetHash(), h.Sum(nil), &rsa.PSSOptions{SaltLength: rsa.PSSSaltLengthEqualsHash}) - if err != nil { - return nil, nil, xerrors.Errorf("cannot provide signature for initializing NV index: %w", err) - } - - // Load the public part of the key in to the TPM. There's no integrity protection for this command as if it's altered in - // transit then either the signature verification fails or the policy digest will not match the one associated with the NV - // index. - initKeyContext, err := tpm.LoadExternal(nil, initKeyPublic, tpm2.HandleEndorsement) - if err != nil { - return nil, nil, xerrors.Errorf("cannot load public part of key used to initialize NV index to the TPM: %w", err) - } - defer tpm.FlushContext(initKeyContext) - - signature := tpm2.Signature{ - SigAlg: tpm2.SigSchemeAlgRSAPSS, - Signature: tpm2.SignatureU{ - Data: &tpm2.SignatureRSAPSS{ - Hash: signDigest, - Sig: tpm2.PublicKeyRSA(sig)}}} - - // Execute the policy assertions - if err := tpm.PolicyCommandCode(policySession, tpm2.CommandNVIncrement); err != nil { - return nil, nil, xerrors.Errorf("cannot execute assertion to initialize NV index: %w", err) - } - if err := tpm.PolicyNvWritten(policySession, false); err != nil { - return nil, nil, xerrors.Errorf("cannot execute assertion to initialize NV index: %w", err) - } - if _, _, err := tpm.PolicySigned(initKeyContext, policySession, true, nil, nil, 0, &signature); err != nil { - return nil, nil, xerrors.Errorf("cannot execute assertion to initialize NV index: %w", err) - } - if err := tpm.PolicyOR(policySession, authPolicies); err != nil { - return nil, nil, xerrors.Errorf("cannot execute assertion to initialize NV index: %w", err) - } - - // Initialize the index - if err := tpm.NVIncrement(index, index, policySession, hmacSession.IncludeAttrs(tpm2.AttrAudit)); err != nil { - return nil, nil, xerrors.Errorf("cannot initialize NV index: %w", err) - } - - // The index has a different name now that it has been written, so update the public area we return so that it can be used - // to construct an authorization policy. - public.Attrs |= tpm2.AttrNVWritten - - succeeded = true - return public, authPolicies, nil -} - -// performPinChange changes the authorization value of the PIN NV index associated with the public argument. This requires the -// authorization policy digests initially returned from createPinNVIndex in order to execute the policy session required to change -// the authorization value. The current authorization value must be provided via the oldAuth argument. -// -// On success, the authorization value of the PIN NV index will be changed to newAuth. -func performPinChange(tpm *tpm2.TPMContext, public *tpm2.NVPublic, authPolicies tpm2.DigestList, oldAuth, newAuth string, hmacSession tpm2.SessionContext) error { +// On success, the authorization value of the counter will be changed to newAuth. +func performPinChangeV0(tpm *tpm2.TPMContext, public *tpm2.NVPublic, authPolicies tpm2.DigestList, oldAuth, newAuth string, hmacSession tpm2.SessionContext) error { index, err := tpm2.CreateNVIndexResourceContextFromPublic(public) if err != nil { return xerrors.Errorf("cannot create resource context for NV index: %w", err) @@ -253,6 +112,33 @@ return nil } +// performPinChange changes the authorization value of the sealed key object associated with keyPrivate and keyPublic, for PIN +// integration in current key files. The sealed key file must be created without the AttrAdminWithPolicy attribute. The current +// authorization value must be provided via the oldAuth argument. +// +// On success, a new private area will be returned for the sealed key object, containing the new PIN. +func performPinChange(tpm *tpm2.TPMContext, keyPrivate tpm2.Private, keyPublic *tpm2.Public, oldPIN, newPIN string, session tpm2.SessionContext) (tpm2.Private, error) { + srk, err := tpm.CreateResourceContextFromTPM(tcg.SRKHandle) + if err != nil { + return nil, xerrors.Errorf("cannot create context for SRK: %w", err) + } + + key, err := tpm.Load(srk, keyPrivate, keyPublic, session) + if err != nil { + return nil, xerrors.Errorf("cannot load sealed key object in to TPM: %w", err) + } + defer tpm.FlushContext(key) + + key.SetAuthValue([]byte(oldPIN)) + + newKeyPrivate, err := tpm.ObjectChangeAuth(key, srk, []byte(newPIN), session.IncludeAttrs(tpm2.AttrCommandEncrypt)) + if err != nil { + return nil, xerrors.Errorf("cannot change sealed key object authorization value: %w", err) + } + + return newKeyPrivate, nil +} + // ChangePIN changes the PIN for the key data file at the specified path. The existing PIN must be supplied via the oldPIN argument. // Setting newPIN to an empty string will clear the PIN and set a hint on the key data file that no PIN is set. // @@ -282,21 +168,31 @@ defer keyFile.Close() // Read and validate the key data file - data, _, pinIndexPublic, err := decodeAndValidateKeyData(tpm.TPMContext, keyFile, nil, tpm.HmacSession()) + data, _, pcrPolicyCounterPub, err := decodeAndValidateKeyData(tpm.TPMContext, keyFile, nil, tpm.HmacSession()) if err != nil { - var kfErr keyFileError - if xerrors.As(err, &kfErr) { + if isKeyFileError(err) { return InvalidKeyFileError{err.Error()} } return xerrors.Errorf("cannot read and validate key data file: %w", err) } // Change the PIN - if err := performPinChange(tpm.TPMContext, pinIndexPublic, data.staticPolicyData.PinIndexAuthPolicies, oldPIN, newPIN, tpm.HmacSession()); err != nil { - if isAuthFailError(err, tpm2.CommandNVChangeAuth, 1) { - return ErrPINFail + if data.version == 0 { + if err := performPinChangeV0(tpm.TPMContext, pcrPolicyCounterPub, data.staticPolicyData.v0PinIndexAuthPolicies, oldPIN, newPIN, tpm.HmacSession()); err != nil { + if isAuthFailError(err, tpm2.CommandNVChangeAuth, 1) { + return ErrPINFail + } + return err + } + } else { + newKeyPrivate, err := performPinChange(tpm.TPMContext, data.keyPrivate, data.keyPublic, oldPIN, newPIN, tpm.HmacSession()) + if err != nil { + if isAuthFailError(err, tpm2.CommandObjectChangeAuth, 1) { + return ErrPINFail + } + return err } - return err + data.keyPrivate = newKeyPrivate } // Update the metadata and write a new key data file @@ -307,7 +203,7 @@ data.authModeHint = AuthModePIN } - if origAuthModeHint == data.authModeHint { + if origAuthModeHint == data.authModeHint && data.version == 0 { return nil } diff -Nru snapd-2.47.1+20.10.1build1/vendor/github.com/snapcore/secboot/policy.go snapd-2.48+21.04/vendor/github.com/snapcore/secboot/policy.go --- snapd-2.47.1+20.10.1build1/vendor/github.com/snapcore/secboot/policy.go 2020-09-29 09:59:28.000000000 +0000 +++ snapd-2.48+21.04/vendor/github.com/snapcore/secboot/policy.go 2020-11-18 09:44:21.000000000 +0000 @@ -20,43 +20,34 @@ package secboot import ( - "bytes" + "crypto" + "crypto/ecdsa" "crypto/rand" "crypto/rsa" "encoding/binary" "errors" - "fmt" "github.com/canonical/go-tpm2" - "github.com/canonical/go-tpm2/mu" "golang.org/x/xerrors" ) -const ( - lockNVIndexVersion uint8 = 0 // Policy data version for lockNVHandle, to support backwards compatible changes - lockNVIndexGraceTime = 5000 // Time window in milliseconds in which lockNVHandle can be initialized after creation -) - var ( - // lockNVIndexAttrs are the attributes for the global lock NV index. - lockNVIndexAttrs = tpm2.NVTypeOrdinary.WithAttrs(tpm2.AttrNVPolicyWrite | tpm2.AttrNVAuthRead | tpm2.AttrNVNoDA | tpm2.AttrNVReadStClear) + // lockNVIndex1Attrs are the attributes for the first global lock NV index. + lockNVIndex1Attrs = tpm2.NVTypeOrdinary.WithAttrs(tpm2.AttrNVPolicyWrite | tpm2.AttrNVAuthRead | tpm2.AttrNVNoDA | tpm2.AttrNVReadStClear) ) // dynamicPolicyComputeParams provides the parameters to computeDynamicPolicy. type dynamicPolicyComputeParams struct { - key *rsa.PrivateKey // Key used to authorize the generated dynamic authorization policy + key crypto.PrivateKey // Key used to authorize the generated dynamic authorization policy // signAlg is the digest algorithm for the signature used to authorize the generated dynamic authorization policy. It must // match the name algorithm of the public part of key that will be loaded in to the TPM for verification. - signAlg tpm2.HashAlgorithmId - pcrs tpm2.PCRSelectionList // PCR selection - pcrDigests tpm2.DigestList // Approved PCR digests - policyCountIndexName tpm2.Name // Name of the NV index used for revoking authorization policies - - // policyCount is the maximum permitted value of the NV index associated with policyCountIndexName, beyond which, this authorization - // policy will not be satisfied. - policyCount uint64 + signAlg tpm2.HashAlgorithmId + pcrs tpm2.PCRSelectionList // PCR selection + pcrDigests tpm2.DigestList // Approved PCR digests + policyCounterName tpm2.Name // Name of the NV index used for revoking authorization policies + policyCount uint64 // Count for this policy, used for revocation } // policyOrDataNode represents a collection of up to 8 digests used in a single TPM2_PolicyOR invocation, and forms part of a tree @@ -70,6 +61,15 @@ // dynamicPolicyData is an output of computeDynamicPolicy and provides metadata for executing a policy session. type dynamicPolicyData struct { + pcrSelection tpm2.PCRSelectionList + pcrOrData policyOrDataTree + policyCount uint64 + authorizedPolicy tpm2.Digest + authorizedPolicySignature *tpm2.Signature +} + +// dynamicPolicyDataRaw_v0 is version 0 of the on-disk format of dynamicPolicyData. +type dynamicPolicyDataRaw_v0 struct { PCRSelection tpm2.PCRSelectionList PCROrData policyOrDataTree PolicyCount uint64 @@ -77,54 +77,116 @@ AuthorizedPolicySignature *tpm2.Signature } -// dynamicPolicyDataRaw_v0 is version 0 of the on-disk format of dynamicPolicyData. They are currently the same structures. -type dynamicPolicyDataRaw_v0 dynamicPolicyData - func (d *dynamicPolicyDataRaw_v0) data() *dynamicPolicyData { - return (*dynamicPolicyData)(d) + return &dynamicPolicyData{ + pcrSelection: d.PCRSelection, + pcrOrData: d.PCROrData, + policyCount: d.PolicyCount, + authorizedPolicy: d.AuthorizedPolicy, + authorizedPolicySignature: d.AuthorizedPolicySignature} } -// makeDynamicPolicyDataRaw_v0 converts dynamicPolicyData to version 0 of the on-disk format. They are currently the same structures -// so this is just a cast, but this may not be the case if the metadata version changes in the future. +// makeDynamicPolicyDataRaw_v0 converts dynamicPolicyData to version 0 of the on-disk format. func makeDynamicPolicyDataRaw_v0(data *dynamicPolicyData) *dynamicPolicyDataRaw_v0 { - return (*dynamicPolicyDataRaw_v0)(data) + return &dynamicPolicyDataRaw_v0{ + PCRSelection: data.pcrSelection, + PCROrData: data.pcrOrData, + PolicyCount: data.policyCount, + AuthorizedPolicy: data.authorizedPolicy, + AuthorizedPolicySignature: data.authorizedPolicySignature} } // staticPolicyComputeParams provides the parameters to computeStaticPolicy. type staticPolicyComputeParams struct { - key *tpm2.Public // Public part of key used to authorize a dynamic authorization policy - pinIndexPub *tpm2.NVPublic // Public area of the NV index used for the PIN - pinIndexAuthPolicies tpm2.DigestList // Metadata for executing policy sessions to interact with the PIN NV index - lockIndexName tpm2.Name // Name of the global NV index for locking access to sealed key objects + key *tpm2.Public // Public part of key used to authorize a dynamic authorization policy + pcrPolicyCounterPub *tpm2.NVPublic // Public area of the NV counter used for revoking PCR policies } // staticPolicyData is an output of computeStaticPolicy and provides metadata for executing a policy session. type staticPolicyData struct { + authPublicKey *tpm2.Public + pcrPolicyCounterHandle tpm2.Handle + v0PinIndexAuthPolicies tpm2.DigestList +} + +// staticPolicyDataRaw_v0 is version 0 of the on-disk format of staticPolicyData. +type staticPolicyDataRaw_v0 struct { AuthPublicKey *tpm2.Public PinIndexHandle tpm2.Handle PinIndexAuthPolicies tpm2.DigestList } -// staticPolicyDataRaw_v0 is the v0 version of the on-disk format of staticPolicyData. They are currently the same structures. -type staticPolicyDataRaw_v0 staticPolicyData - func (d *staticPolicyDataRaw_v0) data() *staticPolicyData { - return (*staticPolicyData)(d) + return &staticPolicyData{ + authPublicKey: d.AuthPublicKey, + pcrPolicyCounterHandle: d.PinIndexHandle, + v0PinIndexAuthPolicies: d.PinIndexAuthPolicies} } -// makeStaticPolicyDataRaw_v0 converts staticPolicyData to version 0 of the on-disk format. They are currently the same structures -// so this is just a cast, but this may not be the case if the metadata version changes in the future. +// makeStaticPolicyDataRaw_v0 converts staticPolicyData to version 0 of the on-disk format. func makeStaticPolicyDataRaw_v0(data *staticPolicyData) *staticPolicyDataRaw_v0 { - return (*staticPolicyDataRaw_v0)(data) + return &staticPolicyDataRaw_v0{ + AuthPublicKey: data.authPublicKey, + PinIndexHandle: data.pcrPolicyCounterHandle, + PinIndexAuthPolicies: data.v0PinIndexAuthPolicies} +} + +// staticPolicyDataRaw_v1 is version 1 of the on-disk format of staticPolicyData. +type staticPolicyDataRaw_v1 struct { + AuthPublicKey *tpm2.Public + PCRPolicyCounterHandle tpm2.Handle + PCRPolicyRef tpm2.Nonce +} + +func (d *staticPolicyDataRaw_v1) data() *staticPolicyData { + return &staticPolicyData{ + authPublicKey: d.AuthPublicKey, + pcrPolicyCounterHandle: d.PCRPolicyCounterHandle} +} + +// makeStaticPolicyDataRaw_v1 converts staticPolicyData to version 1 of the on-disk format. +func makeStaticPolicyDataRaw_v1(data *staticPolicyData) *staticPolicyDataRaw_v1 { + return &staticPolicyDataRaw_v1{ + AuthPublicKey: data.authPublicKey, + PCRPolicyCounterHandle: data.pcrPolicyCounterHandle} +} + +// computePcrPolicyCounterAuthPolicies computes the authorization policy digests passed to TPM2_PolicyOR for a PCR +// policy counter that can be updated with the key associated with updateKeyName. +func computePcrPolicyCounterAuthPolicies(alg tpm2.HashAlgorithmId, updateKeyName tpm2.Name) (tpm2.DigestList, error) { + // The NV index requires 2 policies: + // - A policy to initialize the index with no authorization + // - A policy for updating the index to revoke old PCR policies using a signed assertion. This isn't done for security + // reasons, but just to make it harder to accidentally increment the counter for anyone interacting with the TPM. + // This is simpler than the policy required for the v0 PIN NV index because it doesn't require additional authorization + // policy branches to allow its authorization value to be changed, or to be able to read the counter value or use it in + // a policy assertion without knowing the authorization value (reading the value of this counter does require the + // authorization value, but it is always empty and this policy doesn't allow it to be changed). + var authPolicies tpm2.DigestList + + trial, err := tpm2.ComputeAuthPolicy(alg) + if err != nil { + return nil, err + } + trial.PolicyNvWritten(false) + authPolicies = append(authPolicies, trial.GetDigest()) + + trial, _ = tpm2.ComputeAuthPolicy(alg) + trial.PolicySigned(updateKeyName, nil) + authPolicies = append(authPolicies, trial.GetDigest()) + + return authPolicies, nil } -// incrementDynamicPolicyCounter will increment the NV counter index associated with nvPublic. This is designed to operate on a -// NV index created by createPinNVIndex. The authorization policy digests returned from createPinNVIndex must be supplied via the -// nvAuthPolicies argument. +// incrementPcrPolicyCounter will increment the NV counter index associated with nvPublic. This is designed to operate on a +// NV index created by createPcrPolicyCounter (for current key files) or on a NV index created by (the now deleted) +// createPinNVINdex for version 0 key files. // -// This requires a signed authorization. The keyPublic argument must correspond to the updateKeyName argument originally passed to -// createPinNVIndex. The private part of that key must be supplied via the key argument. -func incrementDynamicPolicyCounter(tpm *tpm2.TPMContext, nvPublic *tpm2.NVPublic, nvAuthPolicies tpm2.DigestList, key *rsa.PrivateKey, keyPublic *tpm2.Public, hmacSession tpm2.SessionContext) error { +// This requires a signed authorization. For current key files, the keyPublic argument must correspond to the updateKeyName argument +// originally passed to createPcrPolicyCounter. For version 0 key files, this must correspond to the key originally passed to +// createPinNVIndex. The private part of that key must be supplied via the key argument. For version 0 key files, the authorization +// policy digests returned from createPinNVIndex must be supplied via the nvAuthPolicies argument. +func incrementPcrPolicyCounter(tpm *tpm2.TPMContext, version uint32, nvPublic *tpm2.NVPublic, nvAuthPolicies tpm2.DigestList, key crypto.PrivateKey, keyPublic *tpm2.Public, hmacSession tpm2.SessionContext) error { index, err := tpm2.CreateNVIndexResourceContextFromPublic(nvPublic) if err != nil { return xerrors.Errorf("cannot create context for NV index: %w", err) @@ -138,15 +200,46 @@ defer tpm.FlushContext(policySession) // Compute a digest for signing with the update key - signDigest := tpm2.HashAlgorithmSHA256 + signDigest := tpm2.HashAlgorithmNull + keyScheme := keyPublic.Params.AsymDetail().Scheme + if keyScheme.Scheme != tpm2.AsymSchemeNull { + signDigest = keyScheme.Details.Any().HashAlg + } + if signDigest == tpm2.HashAlgorithmNull { + signDigest = tpm2.HashAlgorithmSHA256 + } h := signDigest.NewHash() h.Write(policySession.NonceTPM()) binary.Write(h, binary.BigEndian, int32(0)) // expiration // Sign the digest - sig, err := rsa.SignPSS(rand.Reader, key, signDigest.GetHash(), h.Sum(nil), &rsa.PSSOptions{SaltLength: rsa.PSSSaltLengthEqualsHash}) - if err != nil { - return xerrors.Errorf("cannot sign authorization: %w", err) + var signature tpm2.Signature + switch k := key.(type) { + case *rsa.PrivateKey: + sig, err := rsa.SignPSS(rand.Reader, k, signDigest.GetHash(), h.Sum(nil), &rsa.PSSOptions{SaltLength: rsa.PSSSaltLengthEqualsHash}) + if err != nil { + return xerrors.Errorf("cannot sign authorization: %w", err) + } + signature = tpm2.Signature{ + SigAlg: tpm2.SigSchemeAlgRSAPSS, + Signature: tpm2.SignatureU{ + Data: &tpm2.SignatureRSAPSS{ + Hash: signDigest, + Sig: tpm2.PublicKeyRSA(sig)}}} + case *ecdsa.PrivateKey: + sigR, sigS, err := ecdsa.Sign(rand.Reader, k, h.Sum(nil)) + if err != nil { + return xerrors.Errorf("cannot sign authorization: %w", err) + } + signature = tpm2.Signature{ + SigAlg: tpm2.SigSchemeAlgECDSA, + Signature: tpm2.SignatureU{ + Data: &tpm2.SignatureECDSA{ + Hash: signDigest, + SignatureR: sigR.Bytes(), + SignatureS: sigS.Bytes()}}} + default: + panic("invalid private key type") } // Load the public part of the key in to the TPM. There's no integrity protection for this command as if it's altered in @@ -158,20 +251,23 @@ } defer tpm.FlushContext(keyLoaded) - signature := tpm2.Signature{ - SigAlg: tpm2.SigSchemeAlgRSAPSS, - Signature: tpm2.SignatureU{ - Data: &tpm2.SignatureRSAPSS{ - Hash: signDigest, - Sig: tpm2.PublicKeyRSA(sig)}}} - // Execute the policy assertions - if err := tpm.PolicyCommandCode(policySession, tpm2.CommandNVIncrement); err != nil { - return xerrors.Errorf("cannot execute assertion to increment counter: %w", err) - } - if err := tpm.PolicyNvWritten(policySession, true); err != nil { - return xerrors.Errorf("cannot execute assertion to increment counter: %w", err) + if version == 0 { + // See the comment for computeV0PinNVIndexPostInitAuthPolicies for a description of the authorization policy + // for the v0 NV index. + if err := tpm.PolicyCommandCode(policySession, tpm2.CommandNVIncrement); err != nil { + return xerrors.Errorf("cannot execute assertion to increment counter: %w", err) + } + if err := tpm.PolicyNvWritten(policySession, true); err != nil { + return xerrors.Errorf("cannot execute assertion to increment counter: %w", err) + } + } else { + nvAuthPolicies, err = computePcrPolicyCounterAuthPolicies(nvPublic.NameAlg, keyLoaded.Name()) + if err != nil { + return xerrors.Errorf("cannot compute auth policies for counter: %w", err) + } } + if _, _, err := tpm.PolicySigned(keyLoaded, policySession, true, nil, nil, 0, &signature); err != nil { return xerrors.Errorf("cannot execute assertion to increment counter: %w", err) } @@ -187,29 +283,41 @@ return nil } -// readDynamicPolicyCounter will read the value of the counter NV index associated with nvPublic. This is designed to operate on a -// NV index created by createPinNVIndex. The authorization policy digests returned from createPinNVIndex must be supplied via the -// nvAuthPolicies argument. -func readDynamicPolicyCounter(tpm *tpm2.TPMContext, nvPublic *tpm2.NVPublic, nvAuthPolicies tpm2.DigestList, hmacSession tpm2.SessionContext) (uint64, error) { +// readPcrPolicyCounter will read the value of the counter NV index associated with nvPublic. This is designed to operate on a +// NV index created by createPcrPolicyCounter (for current key files) or on a NV index created by (the now deleted) +// createPinNVINdex for version 0 key files. For version 0 key files, the authorization policy digests returned from createPinNVIndex +// must be supplied via the nvAuthPolicies argument. +func readPcrPolicyCounter(tpm *tpm2.TPMContext, version uint32, nvPublic *tpm2.NVPublic, nvAuthPolicies tpm2.DigestList, hmacSession tpm2.SessionContext) (uint64, error) { index, err := tpm2.CreateNVIndexResourceContextFromPublic(nvPublic) if err != nil { return 0, xerrors.Errorf("cannot create context for NV index: %w", err) } - policySession, err := tpm.StartAuthSession(nil, nil, tpm2.SessionTypePolicy, nil, nvPublic.NameAlg) - if err != nil { - return 0, xerrors.Errorf("cannot begin policy session: %w", err) - } - defer tpm.FlushContext(policySession) + var authSession tpm2.SessionContext + var extraSession tpm2.SessionContext + if version == 0 { + authSession, err = tpm.StartAuthSession(nil, nil, tpm2.SessionTypePolicy, nil, nvPublic.NameAlg) + if err != nil { + return 0, xerrors.Errorf("cannot begin policy session: %w", err) + } + defer tpm.FlushContext(authSession) - if err := tpm.PolicyCommandCode(policySession, tpm2.CommandNVRead); err != nil { - return 0, xerrors.Errorf("cannot execute assertion to read counter: %w", err) - } - if err := tpm.PolicyOR(policySession, nvAuthPolicies); err != nil { - return 0, xerrors.Errorf("cannot execute assertion to increment counter: %w", err) + // See the comment for computeV0PinNVIndexPostInitAuthPolicies for a description of the authorization policy + // for the v0 NV index. Because the v0 NV index was also used for the PIN, it needed an authorization policy + // to permit reading the counter value without knowing the authorization value of the index. + if err := tpm.PolicyCommandCode(authSession, tpm2.CommandNVRead); err != nil { + return 0, xerrors.Errorf("cannot execute assertion to read counter: %w", err) + } + if err := tpm.PolicyOR(authSession, nvAuthPolicies); err != nil { + return 0, xerrors.Errorf("cannot execute assertion to increment counter: %w", err) + } + + extraSession = hmacSession.IncludeAttrs(tpm2.AttrAudit) + } else { + authSession = hmacSession } - c, err := tpm.NVReadCounter(index, index, policySession, hmacSession.IncludeAttrs(tpm2.AttrAudit)) + c, err := tpm.NVReadCounter(index, index, authSession, extraSession) if err != nil { return 0, xerrors.Errorf("cannot read counter: %w", err) } @@ -217,272 +325,69 @@ return c, nil } -// ensureLockNVIndex creates a NV index at lockNVHandle if one doesn't exist already. This is used for locking access to any sealed -// key objects we create until the next TPM restart or reset. The same handle is used for all keys, regardless of individual key -// policy. -// -// Locking works by enabling the read lock bit for the NV index. As this changes the name of the index until the next TPM reset or -// restart, it makes any authorization policy that depends on it un-satisfiable. We do this rather than extending an extra value to a -// PCR, as it decouples the PCR policy from the locking feature and allows for the option of having more flexible, owner-customizable -// and maybe device-specific PCR policies in the future. +// createPcrPolicyCounter creates and initializes a NV counter that is associated with a sealed key object and is used for +// implementing dynamic authorization policy revocation. // -// To prevent someone with knowledge of the owner authorization (which is empty unless someone has taken ownership of the TPM) from -// clearing the read lock bit by just undefining and redifining a new NV index with the same properties, we need a way to prevent -// someone from being able to create an identical index. One option for this would be to use a NV counter, which has the property -// that they can only increment and are initialized with a value larger than the largest count value seen on the TPM. Whilst it -// would be possible to recreate a counter with the same name, it wouldn't be possible to recreate one with the same value. Keys -// sealed by this package can then execute an assertion that the counter is equal to a certain value. One problem with this is that -// the current value needs to be known at key sealing time (as it forms part of the authorization policy), and the read lock bit will -// prevent the count value from being read from the TPM. Also, the number of counters available is extremely limited, and we already -// use one per sealed key. -// -// Another approach is to use an ordinary NV index that can't be recreated with the same name. To do this, we require the NV index -// to have been written to and only allow writes with a signed authorization policy. Once initialized, the signing key is discarded. -// This works because the name of the signing key is included in the authorization policy digest for the NV index, and the -// authorization policy digest and attributes are included in the name of the NV index. Without the private part of the signing key, -// it is impossible to create a new NV index with the same name, and so, if this NV index is undefined then it becomes impossible to -// satisfy the authorization policy for any sealed key objects we've created already. -// -// The issue here though is that the globally defined NV index is created at provisioning time, and it may be possible to seal a new -// key to the TPM at any point in the future without provisioning a new global NV index here. In the time between provisioning and -// sealing a key to the TPM, an adversary may have created a new NV index with a policy that only allows writes with a signed -// authorization, initialized it, but then retained the private part of the key. This allows them to undefine and redefine a new NV -// index with the same name in the future in order to remove the read lock bit. To mitigate this, we include another assertion in -// the authorization policy that only allows writes during a small time window (sufficient to initialize the index after it is -// created), and disallows writes once the TPM's clock has advanced past that window. As the parameters of this assertion are -// included in the authorization policy digest, it becomes impossible even for someone with the private part of the key to create -// and initialize a NV index with the same name once the TPM's clock has advanced past that point, without performing a clear of the -// TPM. Clearing the TPM changes the SPS anyway, and makes it impossible to recover any keys previously sealed. -// -// The signing key name and the time window during which the index can be initialized are recorded in another NV index so that it is -// possible to use those to determine whether the lock NV index has an authorization policy that can never be satisfied, in order to -// verify that the index can not be recreated and is therefore safe to use. -func ensureLockNVIndex(tpm *tpm2.TPMContext, session tpm2.SessionContext) error { - if existing, err := tpm.CreateResourceContextFromTPM(lockNVHandle); err == nil { - if _, err := readAndValidateLockNVIndexPublic(tpm, existing, session); err == nil { - return nil - } - } - - // Create signing key. - key, err := rsa.GenerateKey(rand.Reader, 2048) - if err != nil { - return xerrors.Errorf("cannot create signing key for initializing NV index: %w", err) - } - - keyPublic := createPublicAreaForRSASigningKey(&key.PublicKey) - keyName, err := keyPublic.Name() - if err != nil { - return xerrors.Errorf("cannot compute name of signing key for initializing NV index: %w", err) - } - - // Read the TPM clock (no session here because some Infineon devices don't allow them, despite being permitted in the spec - // and reference implementation) - time, err := tpm.ReadClock() - if err != nil { - return xerrors.Errorf("cannot read current time: %w", err) - } - // Give us a small window in which to initialize the index, beyond which the index cannot be written to without a change in TPM owner. - time.ClockInfo.Clock += lockNVIndexGraceTime - clockBytes := make(tpm2.Operand, binary.Size(time.ClockInfo.Clock)) - binary.BigEndian.PutUint64(clockBytes, time.ClockInfo.Clock) - +// The NV index will be created with attributes that allow anyone to read the index, and an authorization policy that permits +// TPM2_NV_Increment with a signed authorization policy. +func createPcrPolicyCounter(tpm *tpm2.TPMContext, handle tpm2.Handle, updateKeyName tpm2.Name, hmacSession tpm2.SessionContext) (*tpm2.NVPublic, error) { nameAlg := tpm2.HashAlgorithmSHA256 - // Compute the authorization policy. + authPolicies, _ := computePcrPolicyCounterAuthPolicies(nameAlg, updateKeyName) + trial, _ := tpm2.ComputeAuthPolicy(nameAlg) - trial.PolicyCommandCode(tpm2.CommandNVWrite) - trial.PolicyCounterTimer(clockBytes, 8, tpm2.OpUnsignedLT) - trial.PolicySigned(keyName, nil) + trial.PolicyOR(authPolicies) - // Create the index. + // Define the NV index public := &tpm2.NVPublic{ - Index: lockNVHandle, + Index: handle, NameAlg: nameAlg, - Attrs: lockNVIndexAttrs, + Attrs: tpm2.NVTypeCounter.WithAttrs(tpm2.AttrNVPolicyWrite | tpm2.AttrNVAuthRead | tpm2.AttrNVNoDA), AuthPolicy: trial.GetDigest(), - Size: 0} - index, err := tpm.NVDefineSpace(tpm.OwnerHandleContext(), nil, public, session) + Size: 8} + + index, err := tpm.NVDefineSpace(tpm.OwnerHandleContext(), nil, public, hmacSession) if err != nil { - var e *tpm2.TPMError - if tpm2.AsTPMError(err, tpm2.ErrorNVDefined, tpm2.CommandNVDefineSpace, &e) { - return &tpmErrorWithHandle{err: e, handle: public.Index} - } - return xerrors.Errorf("cannot create NV index: %w", err) + return nil, xerrors.Errorf("cannot define NV space: %w", err) } + // NVDefineSpace was integrity protected, so we know that we have an index with the expected public area at the handle we specified + // at this point. + succeeded := false defer func() { if succeeded { return } - tpm.NVUndefineSpace(tpm.OwnerHandleContext(), index, session) + tpm.NVUndefineSpace(tpm.OwnerHandleContext(), index, hmacSession) }() // Begin a session to initialize the index. policySession, err := tpm.StartAuthSession(nil, nil, tpm2.SessionTypePolicy, nil, nameAlg) if err != nil { - return xerrors.Errorf("cannot begin policy session to initialize NV index: %w", err) + return nil, xerrors.Errorf("cannot begin policy session to initialize NV index: %w", err) } defer tpm.FlushContext(policySession) - // Compute a digest for signing with our key - signDigest := tpm2.HashAlgorithmSHA256 - h := signDigest.NewHash() - h.Write(policySession.NonceTPM()) - binary.Write(h, binary.BigEndian, int32(0)) - - // Sign the digest - sig, err := rsa.SignPSS(rand.Reader, key, signDigest.GetHash(), h.Sum(nil), &rsa.PSSOptions{SaltLength: rsa.PSSSaltLengthEqualsHash}) - if err != nil { - return xerrors.Errorf("cannot provide signature for initializing NV index: %w", err) - } - - // Load the public part of the key in to the TPM. There's no integrity protection for this command as if it's altered in - // transit then either the signature verification fails or the policy digest will not match the one associated with the NV - // index. - keyLoaded, err := tpm.LoadExternal(nil, keyPublic, tpm2.HandleEndorsement) - if err != nil { - return xerrors.Errorf("cannot load public part of key used to initialize NV index to the TPM: %w", err) - } - defer tpm.FlushContext(keyLoaded) - - signature := tpm2.Signature{ - SigAlg: tpm2.SigSchemeAlgRSAPSS, - Signature: tpm2.SignatureU{ - Data: &tpm2.SignatureRSAPSS{ - Hash: signDigest, - Sig: tpm2.PublicKeyRSA(sig)}}} - // Execute the policy assertions - if err := tpm.PolicyCommandCode(policySession, tpm2.CommandNVWrite); err != nil { - return xerrors.Errorf("cannot execute assertion to initialize NV index: %w", err) - } - if err := tpm.PolicyCounterTimer(policySession, clockBytes, 8, tpm2.OpUnsignedLT); err != nil { - return xerrors.Errorf("cannot execute assertion to initialize NV index: %w", err) + if err := tpm.PolicyNvWritten(policySession, false); err != nil { + return nil, xerrors.Errorf("cannot execute assertion to initialize NV index: %w", err) } - if _, _, err := tpm.PolicySigned(keyLoaded, policySession, true, nil, nil, 0, &signature); err != nil { - return xerrors.Errorf("cannot execute assertion to initialize NV index: %w", err) + if err := tpm.PolicyOR(policySession, authPolicies); err != nil { + return nil, xerrors.Errorf("cannot execute assertion to initialize NV index: %w", err) } // Initialize the index - if err := tpm.NVWrite(index, index, nil, 0, policySession, session.IncludeAttrs(tpm2.AttrAudit)); err != nil { - return xerrors.Errorf("cannot initialize NV index: %w", err) - } - - // Marshal key name and cut-off time for writing to the NV index so that they can be used for verification in the future. - data, err := mu.MarshalToBytes(lockNVIndexVersion, keyName, time.ClockInfo.Clock) - if err != nil { - panic(fmt.Sprintf("cannot marshal contents for policy data NV index: %v", err)) - } - - // Create the data index. - dataPublic := tpm2.NVPublic{ - Index: lockNVDataHandle, - NameAlg: tpm2.HashAlgorithmSHA256, - Attrs: tpm2.NVTypeOrdinary.WithAttrs(tpm2.AttrNVAuthWrite | tpm2.AttrNVWriteDefine | tpm2.AttrNVAuthRead | tpm2.AttrNVNoDA), - Size: uint16(len(data))} - dataIndex, err := tpm.NVDefineSpace(tpm.OwnerHandleContext(), nil, &dataPublic, session) - if err != nil { - var e *tpm2.TPMError - if tpm2.AsTPMError(err, tpm2.ErrorNVDefined, tpm2.CommandNVDefineSpace, &e) { - return &tpmErrorWithHandle{err: e, handle: dataPublic.Index} - } - return xerrors.Errorf("cannot create policy data NV index: %w", err) + if err := tpm.NVIncrement(index, index, policySession, hmacSession.IncludeAttrs(tpm2.AttrAudit)); err != nil { + return nil, xerrors.Errorf("cannot initialize NV index: %w", err) } - defer func() { - if succeeded { - return - } - tpm.NVUndefineSpace(tpm.OwnerHandleContext(), dataIndex, session) - }() - - // Initialize the index - if err := tpm.NVWrite(dataIndex, dataIndex, data, 0, session); err != nil { - return xerrors.Errorf("cannot initialize policy data NV index: %w", err) - } - if err := tpm.NVWriteLock(dataIndex, dataIndex, session); err != nil { - return xerrors.Errorf("cannot write lock policy data NV index: %w", err) - } + // The index has a different name now that it has been written, so update the public area we return so that it can be used + // to construct an authorization policy. + public.Attrs |= tpm2.AttrNVWritten succeeded = true - return nil -} - -// readAndValidateLockNVIndexPublic validates that the NV index at the global lock handle is safe to protect a new key against, and -// then returns the public area if it is. The name of the public area can then be used in an authorization policy. -func readAndValidateLockNVIndexPublic(tpm *tpm2.TPMContext, index tpm2.ResourceContext, session tpm2.SessionContext) (*tpm2.NVPublic, error) { - // Obtain the data recorded alongside the lock NV index for validating that it has a valid authorization policy. - dataIndex, err := tpm.CreateResourceContextFromTPM(lockNVDataHandle) - if err != nil { - return nil, xerrors.Errorf("cannot obtain context for policy data NV index: %w", err) - } - dataPub, _, err := tpm.NVReadPublic(dataIndex) - if err != nil { - return nil, xerrors.Errorf("cannot read public area of policy data NV index: %w", err) - } - data, err := tpm.NVRead(dataIndex, dataIndex, dataPub.Size, 0, nil) - if err != nil { - return nil, xerrors.Errorf("cannot read policy data: %w", err) - } - - // Unmarshal the data - var version uint8 - var keyName tpm2.Name - var clock uint64 - if _, err := mu.UnmarshalFromBytes(data, &version, &keyName, &clock); err != nil { - return nil, xerrors.Errorf("cannot unmarshal policy data: %w", err) - } - - // Allow for future changes to the public attributes or auth policy configuration. - if version != lockNVIndexVersion { - return nil, errors.New("unrecognized version for policy data") - } - - // Read the TPM clock (no session here because some Infineon devices don't allow them, despite being permitted in the spec - // and reference implementation) - time, err := tpm.ReadClock() - if err != nil { - return nil, xerrors.Errorf("cannot read current time: %w", err) - } - - // Make sure the window beyond which this index can be written has passed or about to pass. - if time.ClockInfo.Clock+lockNVIndexGraceTime < clock { - return nil, errors.New("unexpected clock value in policy data") - } - - // Read the public area of the lock NV index. - pub, _, err := tpm.NVReadPublic(index, session.IncludeAttrs(tpm2.AttrAudit)) - if err != nil { - return nil, xerrors.Errorf("cannot read public area of NV index: %w", err) - } - - pub.Attrs &^= tpm2.AttrNVReadLocked - // Validate its attributes - if pub.Attrs != lockNVIndexAttrs|tpm2.AttrNVWritten { - return nil, errors.New("unexpected NV index attributes") - } - - clockBytes := make([]byte, binary.Size(clock)) - binary.BigEndian.PutUint64(clockBytes, clock) - - // Compute the expected authorization policy from the contents of the data index, and make sure this matches the public area. - // This verifies that the lock NV index has a valid authorization policy. - trial, err := tpm2.ComputeAuthPolicy(pub.NameAlg) - if err != nil { - return nil, xerrors.Errorf("cannot compute expected policy for NV index: %w", err) - } - trial.PolicyCommandCode(tpm2.CommandNVWrite) - trial.PolicyCounterTimer(clockBytes, 8, tpm2.OpUnsignedLT) - trial.PolicySigned(keyName, nil) - - if !bytes.Equal(trial.GetDigest(), pub.AuthPolicy) { - return nil, errors.New("incorrect policy for NV index") - } - - // This is a valid global lock NV index that cannot be recreated! - return pub, nil + return public, nil } // ensureSufficientORDigests turns a single digest in to a pair of identical digests. This is because TPM2_PolicyOR assertions @@ -495,28 +400,65 @@ return digests } -// computeStaticPolicy computes the part of an authorization policy that is bound to a sealed key object and never changes. +// computePcrPolicyRefFromCounterName computes the reference used for authorization of signed PCR policies from the supplied +// PCR policy counter name. If name is empty, then the name of the null handle is assumed. The policy ref serves 2 purposes: +// 1) It limits the scope of the signed policy to just PCR policies (the dynamic authorization policy key may be able to sign +// different types of policy in the future, for example, to permit recovery with a signed assertion. +// 2) It binds the name of the PCR policy counter to the static authorization policy. +func computePcrPolicyRefFromCounterName(name tpm2.Name) tpm2.Nonce { + if len(name) == 0 { + name = make(tpm2.Name, binary.Size(tpm2.Handle(0))) + binary.BigEndian.PutUint32(name, uint32(tpm2.HandleNull)) + } + + h := tpm2.HashAlgorithmSHA256.NewHash() + h.Write([]byte("AUTH-PCR-POLICY")) + h.Write(name) + + return h.Sum(nil) +} + +// computePcrPolicyRefFromCounterContext computes the reference used for authorization of signed PCR policies from the supplied +// ResourceContext. +func computePcrPolicyRefFromCounterContext(context tpm2.ResourceContext) tpm2.Nonce { + var name tpm2.Name + if context != nil { + name = context.Name() + } + + return computePcrPolicyRefFromCounterName(name) +} + +// computeStaticPolicy computes the part of an authorization policy that is bound to a sealed key object and never changes. The +// static policy asserts that the following are true: +// - The signed PCR policy created by computeDynamicPolicy is valid and has been satisfied (by way of a PolicyAuthorize assertion, +// which allows the PCR policy to be updated without creating a new sealed key object). +// - Knowledge of the the authorization value for the entity on which the policy session is used has been demonstrated by the +// caller (in SealedKeyObject.UnsealFromTPM where the policy session is used for authorizing unsealing the sealed key object, +// this means that the PIN / passhphrase has been provided). func computeStaticPolicy(alg tpm2.HashAlgorithmId, input *staticPolicyComputeParams) (*staticPolicyData, tpm2.Digest, error) { - trial, _ := tpm2.ComputeAuthPolicy(alg) - keyName, err := input.key.Name() if err != nil { return nil, nil, xerrors.Errorf("cannot compute name of signing key for dynamic policy authorization: %w", err) } - pinIndexName, err := input.pinIndexPub.Name() - if err != nil { - return nil, nil, xerrors.Errorf("cannot compute name of PIN NV index: %w", err) + pcrPolicyCounterHandle := tpm2.HandleNull + var pcrPolicyCounterName tpm2.Name + if input.pcrPolicyCounterPub != nil { + pcrPolicyCounterHandle = input.pcrPolicyCounterPub.Index + pcrPolicyCounterName, err = input.pcrPolicyCounterPub.Name() + if err != nil { + return nil, nil, xerrors.Errorf("cannot compute name of PCR policy counter: %w", err) + } } - trial.PolicyAuthorize(nil, keyName) - trial.PolicySecret(pinIndexName, nil) - trial.PolicyNV(input.lockIndexName, nil, 0, tpm2.OpEq) + trial, _ := tpm2.ComputeAuthPolicy(alg) + trial.PolicyAuthorize(computePcrPolicyRefFromCounterName(pcrPolicyCounterName), keyName) + trial.PolicyAuthValue() return &staticPolicyData{ - AuthPublicKey: input.key, - PinIndexHandle: input.pinIndexPub.Index, - PinIndexAuthPolicies: input.pinIndexAuthPolicies}, trial.GetDigest(), nil + authPublicKey: input.key, + pcrPolicyCounterHandle: pcrPolicyCounterHandle}, trial.GetDigest(), nil } // computePolicyORData computes data required to perform a sequence of TPM2_PolicyOR assertions in order to support compound @@ -578,13 +520,16 @@ return data } -// computeDynamicPolicy computes the part of an authorization policy associated with a sealed key object that can change and be -// updated. +// computeDynamicPolicy computes the PCR policy associated with a sealed key object, and can be updated without having to create a +// new sealed key object as it takes advantage of the PolicyAuthorize assertion. The PCR policy asserts that the following are true: +// - The selected PCRs contain expected values - ie, one of the sets of permitted values specified by the caller to this function, +// indicating that the device is in an expected state. This is done by a single PolicyPCR assertion and then one or more PolicyOR +// assertions (depending on how many sets of permitted PCR values there are). +// - The PCR policy hasn't been revoked. This is done using a PolicyNV assertion to assert that the value of an optional NV counter +// is not greater than the expected value. +// The computed PCR policy digest is signed with the supplied asymmetric key, and the signature of this is validated before executing +// the corresponding PolicyAuthorize assertion as part of the static policy. func computeDynamicPolicy(version uint32, alg tpm2.HashAlgorithmId, input *dynamicPolicyComputeParams) (*dynamicPolicyData, error) { - // We only have a single metadata version at the moment (version 0) - if version != 0 { - return nil, errors.New("invalid version") - } if len(input.pcrDigests) == 0 { return nil, errors.New("no PCR digests specified") } @@ -600,35 +545,57 @@ trial, _ := tpm2.ComputeAuthPolicy(alg) pcrOrData := computePolicyORData(alg, trial, pcrOrDigests) - operandB := make([]byte, 8) - binary.BigEndian.PutUint64(operandB, input.policyCount) - trial.PolicyNV(input.policyCountIndexName, operandB, 0, tpm2.OpUnsignedLE) + if len(input.policyCounterName) > 0 { + operandB := make([]byte, 8) + binary.BigEndian.PutUint64(operandB, input.policyCount) + trial.PolicyNV(input.policyCounterName, operandB, 0, tpm2.OpUnsignedLE) + } authorizedPolicy := trial.GetDigest() // Create a digest to sign h := input.signAlg.NewHash() h.Write(authorizedPolicy) + if version > 0 { + h.Write(computePcrPolicyRefFromCounterName(input.policyCounterName)) + } // Sign the digest - sig, err := rsa.SignPSS(rand.Reader, input.key, input.signAlg.GetHash(), h.Sum(nil), &rsa.PSSOptions{SaltLength: rsa.PSSSaltLengthEqualsHash}) - if err != nil { - return nil, xerrors.Errorf("cannot provide signature for initializing NV index: %w", err) + var signature tpm2.Signature + if version == 0 { + sig, err := rsa.SignPSS(rand.Reader, input.key.(*rsa.PrivateKey), input.signAlg.GetHash(), h.Sum(nil), + &rsa.PSSOptions{SaltLength: rsa.PSSSaltLengthEqualsHash}) + if err != nil { + return nil, xerrors.Errorf("cannot provide signature for initializing NV index: %w", err) + } + + signature = tpm2.Signature{ + SigAlg: tpm2.SigSchemeAlgRSAPSS, + Signature: tpm2.SignatureU{ + Data: &tpm2.SignatureRSAPSS{ + Hash: input.signAlg, + Sig: tpm2.PublicKeyRSA(sig)}}} + } else { + sigR, sigS, err := ecdsa.Sign(rand.Reader, input.key.(*ecdsa.PrivateKey), h.Sum(nil)) + if err != nil { + return nil, xerrors.Errorf("cannot provide signature for initializing NV index: %w", err) + } + + signature = tpm2.Signature{ + SigAlg: tpm2.SigSchemeAlgECDSA, + Signature: tpm2.SignatureU{ + Data: &tpm2.SignatureECDSA{ + Hash: input.signAlg, + SignatureR: sigR.Bytes(), + SignatureS: sigS.Bytes()}}} } - signature := tpm2.Signature{ - SigAlg: tpm2.SigSchemeAlgRSAPSS, - Signature: tpm2.SignatureU{ - Data: &tpm2.SignatureRSAPSS{ - Hash: input.signAlg, - Sig: tpm2.PublicKeyRSA(sig)}}} - return &dynamicPolicyData{ - PCRSelection: input.pcrs, - PCROrData: pcrOrData, - PolicyCount: input.policyCount, - AuthorizedPolicy: authorizedPolicy, - AuthorizedPolicySignature: &signature}, nil + pcrSelection: input.pcrs, + pcrOrData: pcrOrData, + policyCount: input.policyCount, + authorizedPolicy: authorizedPolicy, + authorizedPolicySignature: &signature}, nil } type staticPolicyDataError struct { @@ -713,13 +680,13 @@ // executePolicySession executes an authorization policy session using the supplied metadata. On success, the supplied policy // session can be used for authorization. -func executePolicySession(tpm *tpm2.TPMContext, policySession tpm2.SessionContext, staticInput *staticPolicyData, +func executePolicySession(tpm *tpm2.TPMContext, policySession tpm2.SessionContext, version uint32, staticInput *staticPolicyData, dynamicInput *dynamicPolicyData, pin string, hmacSession tpm2.SessionContext) error { - if err := tpm.PolicyPCR(policySession, nil, dynamicInput.PCRSelection); err != nil { + if err := tpm.PolicyPCR(policySession, nil, dynamicInput.pcrSelection); err != nil { return xerrors.Errorf("cannot execute PCR assertion: %w", err) } - if err := executePolicyORAssertions(tpm, policySession, dynamicInput.PCROrData); err != nil { + if err := executePolicyORAssertions(tpm, policySession, dynamicInput.pcrOrData); err != nil { switch { case tpm2.IsTPMError(err, tpm2.AnyErrorCode, tpm2.CommandPolicyGetDigest): return xerrors.Errorf("cannot execute OR assertions: %w", err) @@ -730,144 +697,171 @@ return dynamicPolicyDataError{xerrors.Errorf("cannot complete OR assertions: %w", err)} } - pinIndexHandle := staticInput.PinIndexHandle - if pinIndexHandle.Type() != tpm2.HandleTypeNVIndex { - return staticPolicyDataError{errors.New("invalid handle type for PIN NV index")} - } - pinIndex, err := tpm.CreateResourceContextFromTPM(pinIndexHandle) - switch { - case tpm2.IsResourceUnavailableError(err, pinIndexHandle): - // If there is no NV index at the expected handle then the key file is invalid and must be recreated. - return staticPolicyDataError{errors.New("no PIN NV index found")} - case err != nil: - return xerrors.Errorf("cannot obtain context for PIN NV index: %w", err) - } - pinIndexPub, _, err := tpm.NVReadPublic(pinIndex) - if err != nil { - return xerrors.Errorf("cannot read public area for PIN NV index: %w", err) - } - if !pinIndexPub.NameAlg.Supported() { - //If the NV index has an unsupported name algorithm, then this key file is invalid and must be recreated. - return staticPolicyDataError{errors.New("PIN NV index has an unsupported name algorithm")} + pcrPolicyCounterHandle := staticInput.pcrPolicyCounterHandle + if (pcrPolicyCounterHandle != tpm2.HandleNull || version == 0) && pcrPolicyCounterHandle.Type() != tpm2.HandleTypeNVIndex { + return staticPolicyDataError{errors.New("invalid handle for PCR policy counter")} } - revocationCheckSession, err := tpm.StartAuthSession(nil, nil, tpm2.SessionTypePolicy, nil, pinIndexPub.NameAlg) - if err != nil { - return xerrors.Errorf("cannot create session for dynamic authorization policy revocation check: %w", err) - } - defer tpm.FlushContext(revocationCheckSession) + var policyCounter tpm2.ResourceContext + if pcrPolicyCounterHandle != tpm2.HandleNull { + var err error + policyCounter, err = tpm.CreateResourceContextFromTPM(pcrPolicyCounterHandle) + switch { + case tpm2.IsResourceUnavailableError(err, pcrPolicyCounterHandle): + // If there is no NV index at the expected handle then the key file is invalid and must be recreated. + return staticPolicyDataError{errors.New("no PCR policy counter found")} + case err != nil: + return xerrors.Errorf("cannot obtain context for PCR policy counter: %w", err) + } - if err := tpm.PolicyCommandCode(revocationCheckSession, tpm2.CommandPolicyNV); err != nil { - return xerrors.Errorf("cannot execute assertion for dynamic authorization policy revocation check: %w", err) - } - if err := tpm.PolicyOR(revocationCheckSession, staticInput.PinIndexAuthPolicies); err != nil { - if tpm2.IsTPMParameterError(err, tpm2.ErrorValue, tpm2.CommandPolicyOR, 1) { - // staticInput.PinIndexAuthPolicies is invalid. - return staticPolicyDataError{errors.New("authorization policy metadata for PIN NV index is invalid")} + var revocationCheckSession tpm2.SessionContext + if version == 0 { + policyCounterPub, _, err := tpm.NVReadPublic(policyCounter) + if err != nil { + return xerrors.Errorf("cannot read public area for PCR policy counter: %w", err) + } + if !policyCounterPub.NameAlg.Supported() { + //If the NV index has an unsupported name algorithm, then this key file is invalid and must be recreated. + return staticPolicyDataError{errors.New("PCR policy counter has an unsupported name algorithm")} + } + + revocationCheckSession, err = tpm.StartAuthSession(nil, nil, tpm2.SessionTypePolicy, nil, policyCounterPub.NameAlg) + if err != nil { + return xerrors.Errorf("cannot create session for PCR policy revocation check: %w", err) + } + defer tpm.FlushContext(revocationCheckSession) + + // See the comment for computeV0PinNVIndexPostInitAuthPolicies for a description of the authorization policy + // for the v0 NV index. Because the v0 NV index was also used for the PIN, it needed an authorization policy to + // permit using the counter value in an assertion without knowing the authorization value of the index. + if err := tpm.PolicyCommandCode(revocationCheckSession, tpm2.CommandPolicyNV); err != nil { + return xerrors.Errorf("cannot execute assertion for PCR policy revocation check: %w", err) + } + if err := tpm.PolicyOR(revocationCheckSession, staticInput.v0PinIndexAuthPolicies); err != nil { + if tpm2.IsTPMParameterError(err, tpm2.ErrorValue, tpm2.CommandPolicyOR, 1) { + // staticInput.v0PinIndexAuthPolicies is invalid. + return staticPolicyDataError{errors.New("authorization policy metadata for PCR policy counter is invalid")} + } + return xerrors.Errorf("cannot execute assertion for PCR policy revocation check: %w", err) + } } - return xerrors.Errorf("cannot execute assertion for dynamic authorization policy revocation check: %w", err) - } - operandB := make([]byte, 8) - binary.BigEndian.PutUint64(operandB, dynamicInput.PolicyCount) - if err := tpm.PolicyNV(pinIndex, pinIndex, policySession, operandB, 0, tpm2.OpUnsignedLE, revocationCheckSession); err != nil { - switch { - case tpm2.IsTPMError(err, tpm2.ErrorPolicy, tpm2.CommandPolicyNV): - // The dynamic authorization policy has been revoked. - return dynamicPolicyDataError{errors.New("the dynamic authorization policy has been revoked")} - case tpm2.IsTPMSessionError(err, tpm2.ErrorPolicyFail, tpm2.CommandPolicyNV, 1): - // Either staticInput.PinIndexAuthPolicies is invalid or the NV index isn't what's expected, so the key file is invalid. - return staticPolicyDataError{errors.New("invalid PIN NV index or associated authorization policy metadata")} + operandB := make([]byte, 8) + binary.BigEndian.PutUint64(operandB, dynamicInput.policyCount) + if err := tpm.PolicyNV(policyCounter, policyCounter, policySession, operandB, 0, tpm2.OpUnsignedLE, revocationCheckSession); err != nil { + switch { + case tpm2.IsTPMError(err, tpm2.ErrorPolicy, tpm2.CommandPolicyNV): + // The PCR policy has been revoked. + return dynamicPolicyDataError{errors.New("the PCR policy has been revoked")} + case tpm2.IsTPMSessionError(err, tpm2.ErrorPolicyFail, tpm2.CommandPolicyNV, 1): + // Either staticInput.v0PinIndexAuthPolicies is invalid or the NV index isn't what's expected, so the key file is invalid. + return staticPolicyDataError{errors.New("invalid PCR policy counter or associated authorization policy metadata")} + } + return xerrors.Errorf("PCR policy revocation check failed: %w", err) } - return xerrors.Errorf("dynamic authorization policy revocation check failed: %w", err) } - authPublicKey := staticInput.AuthPublicKey + authPublicKey := staticInput.authPublicKey if !authPublicKey.NameAlg.Supported() { - return staticPolicyDataError{errors.New("public area of dynamic authorization policy signature verification key has an unsupported name algorithm")} + return staticPolicyDataError{errors.New("public area of dynamic authorization policy signing key has an unsupported name algorithm")} } authorizeKey, err := tpm.LoadExternal(nil, authPublicKey, tpm2.HandleOwner) if err != nil { if tpm2.IsTPMParameterError(err, tpm2.AnyErrorCode, tpm2.CommandLoadExternal, 2) { // staticInput.AuthPublicKey is invalid - return staticPolicyDataError{errors.New("public area of dynamic authorization policy signature verification key is invalid")} + return staticPolicyDataError{errors.New("public area of dynamic authorization policy signing key is invalid")} } - return xerrors.Errorf("cannot load public area for dynamic authorization policy signature verification key: %w", err) + return xerrors.Errorf("cannot load public area for dynamic authorization policy signing key: %w", err) } defer tpm.FlushContext(authorizeKey) + var pcrPolicyRef tpm2.Nonce + if version > 0 { + // The authorized PCR policy signature contains a reference for > v0 metadata, which limits the scope of it for authorizing + // PCR policy. In future, the key that authorizes this policy may be used to authorize other policy digests for the purposes of, + // eg, recovery with a signed assertion. + pcrPolicyRef = computePcrPolicyRefFromCounterContext(policyCounter) + } + h := authPublicKey.NameAlg.NewHash() - h.Write(dynamicInput.AuthorizedPolicy) + h.Write(dynamicInput.authorizedPolicy) + h.Write(pcrPolicyRef) - authorizeTicket, err := tpm.VerifySignature(authorizeKey, h.Sum(nil), dynamicInput.AuthorizedPolicySignature) + authorizeTicket, err := tpm.VerifySignature(authorizeKey, h.Sum(nil), dynamicInput.authorizedPolicySignature) if err != nil { if tpm2.IsTPMParameterError(err, tpm2.AnyErrorCode, tpm2.CommandVerifySignature, 2) { - // dynamicInput.AuthorizedPolicySignature is invalid. - return dynamicPolicyDataError{errors.New("cannot verify dynamic authorization policy signature")} + // dynamicInput.AuthorizedPolicySignature or the computed policy ref is invalid. + // XXX: It's not possible to determine whether this is broken dynamic or static metadata - + // we should just do away with the distinction here tbh + return dynamicPolicyDataError{errors.New("cannot verify PCR policy signature")} } - return xerrors.Errorf("cannot verify dynamic authorization policy signature: %w", err) + return xerrors.Errorf("cannot verify PCR policy signature: %w", err) } - if err := tpm.PolicyAuthorize(policySession, dynamicInput.AuthorizedPolicy, nil, authorizeKey.Name(), authorizeTicket); err != nil { + if err := tpm.PolicyAuthorize(policySession, dynamicInput.authorizedPolicy, pcrPolicyRef, authorizeKey.Name(), authorizeTicket); err != nil { if tpm2.IsTPMParameterError(err, tpm2.ErrorValue, tpm2.CommandPolicyAuthorize, 1) { // dynamicInput.AuthorizedPolicy is invalid. - return dynamicPolicyDataError{errors.New("the dynamic authorization policy is invalid")} + return dynamicPolicyDataError{errors.New("the PCR policy is invalid")} } - return xerrors.Errorf("dynamic authorization policy check failed: %w", err) + return xerrors.Errorf("PCR policy check failed: %w", err) } - pinIndex.SetAuthValue([]byte(pin)) - if _, _, err := tpm.PolicySecret(pinIndex, policySession, nil, nil, 0, hmacSession); err != nil { - return xerrors.Errorf("cannot execute PolicySecret assertion: %w", err) + if version == 0 { + // For metadata version 0, PIN support is implemented by asserting knowlege of the authorization value + // for the PCR policy counter. + policyCounter.SetAuthValue([]byte(pin)) + if _, _, err := tpm.PolicySecret(policyCounter, policySession, nil, nil, 0, hmacSession); err != nil { + return xerrors.Errorf("cannot execute PolicySecret assertion: %w", err) + } + } else { + // For metadata versions > 0, PIN support is implemented by requiring knowlege of the authorization value for + // the sealed key object when this policy session is used to unseal it. + if err := tpm.PolicyAuthValue(policySession); err != nil { + return xerrors.Errorf("cannot execute PolicyAuthValue assertion: %w", err) + } } - lockIndex, err := tpm.CreateResourceContextFromTPM(lockNVHandle) - if err != nil { - return xerrors.Errorf("cannot obtain context for lock NV index: %w", err) - } - if err := tpm.PolicyNV(lockIndex, lockIndex, policySession, nil, 0, tpm2.OpEq, hmacSession); err != nil { - return xerrors.Errorf("policy lock check failed: %w", err) + if version == 0 { + // Execute required TPM2_PolicyNV assertion that was used for legacy locking with v0 files - + // this is only here because the existing policy for v0 files requires it. It is not expected that + // this will fail unless the NV index has been removed or altered, at which point the key is + // non-recoverable anyway. + index, err := tpm.CreateResourceContextFromTPM(lockNVHandle) + if err != nil { + return xerrors.Errorf("cannot obtain context for lock NV index: %w", err) + } + if err := tpm.PolicyNV(index, index, policySession, nil, 0, tpm2.OpEq, nil); err != nil { + return xerrors.Errorf("policy lock check failed: %w", err) + } } return nil } -// LockAccessToSealedKeys locks access to keys sealed by this package until the next TPM restart (equivalent to eg, system resume -// from suspend-to-disk) or TPM reset (equivalent to booting after a system restart). This works for all keys sealed by this package -// regardless of their PCR protection profile. +// BlockPCRProtectionPolicies inserts a fence in to the specific PCRs for all active PCR banks, in order to +// make PCR policies that depend on the specified PCRs and are satisfiable by the current PCR values invalid +// until the next TPM restart (equivalent to eg, system resume from suspend-to-disk) or TPM reset +// (equivalent to booting after a system reset). // -// On success, subsequent calls to SealedKeyObject.UnsealFromTPM will fail with a ErrSealedKeyAccessLocked error until the next TPM -// restart or TPM reset. -func LockAccessToSealedKeys(tpm *TPMConnection) error { +// This acts as a barrier between the environment in which a sealed key should be permitted to be unsealed +// (eg, the initramfs), and the environment in which a sealed key should not be permitted to be unsealed +// (eg, the OS runtime). +func BlockPCRProtectionPolicies(tpm *TPMConnection, pcrs []int) error { session := tpm.HmacSession() - handles, err := tpm.GetCapabilityHandles(lockNVHandle, 1, session.IncludeAttrs(tpm2.AttrAudit)) - if err != nil { - return xerrors.Errorf("cannot obtain handles from TPM: %w", err) - } - if len(handles) == 0 || handles[0] != lockNVHandle { - // Not provisioned, so no keys created by this package can be unsealed by this TPM - return nil - } - lock, err := tpm.CreateResourceContextFromTPM(lockNVHandle) - if err != nil { - return xerrors.Errorf("cannot obtain context for lock NV index: %w", err) - } - lockPublic, _, err := tpm.NVReadPublic(lock, session.IncludeAttrs(tpm2.AttrAudit)) - if err != nil { - return xerrors.Errorf("cannot read public area of lock NV index: %w", err) - } - if lockPublic.Attrs != lockNVIndexAttrs|tpm2.AttrNVWritten { - // Definitely not an index created by us, so no keys created by this package can be unsealed by this TPM. - return nil - } - if err := tpm.NVReadLock(lock, lock, session); err != nil { - if isAuthFailError(err, tpm2.CommandNVReadLock, 1) { - // The index has an authorization value, so it wasn't created by this package and no keys created by this package can be unsealed - // by this TPM. - return nil + // The fence is a hash of uint32(0), which is the same as EV_SEPARATOR (which can be uint32(0) or uint32(-1)) + fence := make([]byte, 4) + + // Insert PCR fence + for _, pcr := range pcrs { + seq, err := tpm.HashSequenceStart(nil, tpm2.HashAlgorithmNull) + if err != nil { + return xerrors.Errorf("cannot being hash sequence: %w", err) + } + if _, err := tpm.EventSequenceExecute(tpm.PCRHandleContext(pcr), seq, fence, session, nil); err != nil { + return xerrors.Errorf("cannot execute hash sequence: %w", err) } - return xerrors.Errorf("cannot lock NV index for reading: %w", err) } + return nil } diff -Nru snapd-2.47.1+20.10.1build1/vendor/github.com/snapcore/secboot/provisioning.go snapd-2.48+21.04/vendor/github.com/snapcore/secboot/provisioning.go --- snapd-2.47.1+20.10.1build1/vendor/github.com/snapcore/secboot/provisioning.go 2020-09-02 10:31:40.000000000 +0000 +++ snapd-2.48+21.04/vendor/github.com/snapcore/secboot/provisioning.go 2020-11-18 09:44:21.000000000 +0000 @@ -20,7 +20,7 @@ package secboot import ( - "bytes" + "errors" "fmt" "os" @@ -43,47 +43,22 @@ lockoutRecovery uint32 = 86400 ) -// ProvisionStatusAttributes correspond to the state of the TPM with regards to provisioning for full disk encryption. -type ProvisionStatusAttributes int - -const ( - // AttrValidSRK indicates that the TPM contains a valid primary storage key with the expected properties at the - // expected location. Note that this does not mean that the object was created with the same template that ProvisionTPM - // uses, and is no guarantee that a call to ProvisionTPM wouldn't result in a different key being created. - AttrValidSRK ProvisionStatusAttributes = 1 << iota - - // AttrValidEK indicates that the TPM contains a valid endorsement key at the expected location. On a TPMConnection created - // with SecureConnectToDefaultTPM, it means that the TPM contains the key associated with the verified endorsement certificate. - // On a TPMConnection created with ConnectToDefaultTPM, it means that the TPM contains a valid primary key with the expected - // properties at the expected location, but does not mean that the object was created with the the same template that - // ProvisionTPM uses, and is no guarantee that a call to ProvisionTPM wouldn't result in a different key being created. - AttrValidEK - - AttrDAParamsOK // The dictionary attack lockout parameters are configured correctly. - AttrOwnerClearDisabled // The ability to clear the TPM with owner authorization is disabled. - - // AttrLockoutAuthSet indicates that the lockout hierarchy has an authorization value defined. This - // doesn't necessarily mean that the authorization value is the same one that was originally provided - // to ProvisionTPM - it could have been changed outside of our control. - AttrLockoutAuthSet - - AttrValidLockNVIndex // The TPM has a valid NV index used for locking access to keys sealed with SealKeyToTPM -) - -// ProvisionMode is used to control the behaviour of ProvisionTPM. +// ProvisionMode is used to control the behaviour of TPMConnection.EnsureProvisioned. type ProvisionMode int const ( - // ProvisionModeClear specifies that the TPM should be fully provisioned after clearing it. - ProvisionModeClear ProvisionMode = iota - - // ProvisionModeWithoutLockout specifies that the TPM should be refreshed without performing operations that require knowledge of - // the lockout hierarchy authorization value. Operations that won't be performed in this mode are disabling owner clear, configuring - // the dictionary attack parameters and setting the authorization value for the lockout hierarchy. - ProvisionModeWithoutLockout + // ProvisionModeWithoutLockout specifies that the TPM should be refreshed without performing operations that require the use of the + // lockout hierarchy. Operations that won't be performed in this mode are disabling owner clear, configuring the dictionary attack + // parameters, and setting the authorization value for the lockout hierarchy. + ProvisionModeWithoutLockout ProvisionMode = iota - // ProvisionModeFull specifies that the TPM should be fully provisioned without clearing it. + // ProvisionModeFull specifies that the TPM should be fully provisioned without clearing it. This requires use of the lockout + // hierarchy. ProvisionModeFull + + // ProvisionModeClear specifies that the TPM should be fully provisioned after clearing it. This requires use of the lockout + // hierarchy. + ProvisionModeClear ) func provisionPrimaryKey(tpm *tpm2.TPMContext, hierarchy tpm2.ResourceContext, template *tpm2.Public, handle tpm2.Handle, session tpm2.SessionContext) (tpm2.ResourceContext, error) { @@ -115,62 +90,63 @@ return obj, nil } -// ProvisionTPM prepares the TPM associated with the tpm parameter for full disk encryption. The mode parameter specifies the -// behaviour of this function. +// EnsureProvisioned prepares the TPM for full disk encryption. The mode parameter specifies the behaviour of this function. // // If mode is ProvisionModeClear, this function will attempt to clear the TPM before provisioning it. If owner clear has been // disabled (which will be the case if the TPM has previously been provisioned with this function), then ErrTPMClearRequiresPPI // will be returned. In this case, the TPM must be cleared via the physical presence interface by calling RequestTPMClearUsingPPI -// and performing a system restart. +// and performing a system restart. Note that clearing the TPM makes all previously sealed keys permanently unrecoverable. This +// mode should normally be used when resetting a device to factory settings (ie, performing a new installation). // -// If mode is ProvisionModeClear or ProvisionModeFull then the authorization value for the lockout hierarchy will be set to -// newLockoutAuth, owner clear will be disabled, and the parameters of the TPM's dictionary attack logic will be configured. These -// operations require knowledge of the lockout hierarchy authorization value, which must be provided by calling +// If mode is ProvisionModeClear or ProvisionModeFull, then the authorization value for the lockout hierarchy will be set to +// newLockoutAuth, owner clear will be disabled, and the parameters of the TPM's dictionary attack logic will be configured to +// appropriate values. +// +// If mode is ProvisionModeClear or ProvisionModeFull, this function performs operations that require the use of the lockout +// hierarchy (detailed above), and knowledge of the lockout hierarchy's authorization value. This must be provided by calling // TPMConnection.LockoutHandleContext().SetAuthValue() prior to this call. If the wrong lockout hierarchy authorization value is // provided, then a AuthFailError error will be returned. If this happens, the TPM will have entered dictionary attack lockout mode // for the lockout hierarchy. Further calls will result in a ErrTPMLockout error being returned. The only way to recover from this is // to either wait for the pre-programmed recovery time to expire, or to clear the TPM via the physical presence interface by calling -// RequestTPMClearUsingPPI. If the lockout hierarchy authorization value is not known or the caller wants to skip the operations that -// require use of the lockout hierarchy, then mode can be set to ProvisionModeWithoutLockout. +// RequestTPMClearUsingPPI. If the lockout hierarchy authorization value is not known then mode should be set to +// ProvisionModeWithoutLockout, with the caveat that this mode cannot fully provision the TPM. // -// If mode is ProvisionModeFull or ProvisionModeWithoutLockout, this function performs operations that require knowledge of the -// storage and endorsement hierarchies (creation of primary keys and NV indices, detailed below). Whilst these will be empty after -// clearing the TPM, if they have been set since clearing the TPM then they will need to be provided by calling -// TPMConnection.EndorsementHandleContext().SetAuthValue() and TPMConnection.OwnerHandleContext().SetAuthValue() prior to calling -// this function. If the wrong value is provided for either authorization, then a AuthFailError error will be returned. If the correct -// authorization values are not known, then the only way to recover from this is to clear the TPM either by calling this function with -// mode set to ProvisionModeClear, or by using the physical presence interface. +// If mode is ProvisionModeFull or ProvisionModeWithoutLockout, this function will not affect the ability to recover sealed keys that +// can currently be recovered. // // In all modes, this function will create and persist both a storage root key and an endorsement key. Both of these will be created // using the RSA templates defined in and persisted at the handles specified in the "TCG EK Credential Profile for TPM Family 2.0" // and "TCG TPM v2.0 Provisioning Guidance" specifications. If there are any objects already stored at the locations required for -// either primary key, then this function will evict them automatically from the TPM. +// either primary key, then this function will evict them automatically from the TPM. These operations both require the use of the +// storage and endorsement hierarchies. If mode is ProvisionModeFull or ProvisionModeWithoutLockout, then knowledge of the +// authorization values for these hierarchies is required. Whilst these will be empty after clearing the TPM, if they have been set +// since clearing the TPM then they will need to be provided by calling TPMConnection.EndorsementHandleContext().SetAuthValue() and +// TPMConnection.OwnerHandleContext().SetAuthValue() prior to calling this function. If the wrong value is provided for either +// authorization, then a AuthFailError error will be returned. If the correct authorization values are not known, then the only way +// to recover from this is to clear the TPM either by calling this function with mode set to ProvisionModeClear (and providing the +// correct authorization value for the lockout hierarchy), or by using the physical presence interface. // -// In all modes, this function will also create a pair of NV indices used for locking access to sealed key objects, if necessary. -// These indices will be created at handles 0x01801100 and 0x01801101. If there are already NV indices defined at either of the -// required handles but they don't meet the requirements of this function, a TPMResourceExistsError error will be returned. In this -// case, the caller will either need to manually undefine these using TPMConnection.NVUndefineSpace, or clear the TPM. -func ProvisionTPM(tpm *TPMConnection, mode ProvisionMode, newLockoutAuth []byte) error { - status, err := ProvisionStatus(tpm) - if err != nil { - return xerrors.Errorf("cannot determine the current TPM status: %w", err) - } +// If mode is ProvisionModeWithoutLockout but the TPM indicates that use of the lockout hierarchy is required to fully provision the +// TPM (eg, to disable owner clear, set the lockout hierarchy authorization value or configure the DA lockout parameters), then a +// ErrTPMProvisioningRequiresLockout error will be returned. In this scenario, the function will complete all operations that can be +// completed without using the lockout hierarchy, but the function should be called again either with mode set to ProvisionModeFull +// (if the authorization value for the lockout hierarchy is known), or ProvisionModeClear. +func (t *TPMConnection) EnsureProvisioned(mode ProvisionMode, newLockoutAuth []byte) error { + session := t.HmacSession() - // Create an initial session for HMAC authorizations - session, err := tpm.StartAuthSession(nil, nil, tpm2.SessionTypeHMAC, nil, defaultSessionHashAlgorithm, nil) + props, err := t.GetCapabilityTPMProperties(tpm2.PropertyPermanent, 1, session.IncludeAttrs(tpm2.AttrAudit)) if err != nil { - return xerrors.Errorf("cannot start session: %w", err) + return xerrors.Errorf("cannot fetch permanent properties: %w", err) + } + if props[0].Property != tpm2.PropertyPermanent { + return errors.New("TPM returned value for the wrong property") } - defer tpm.FlushContext(session) - - session.SetAttrs(tpm2.AttrContinueSession) - if mode == ProvisionModeClear { - if status&AttrOwnerClearDisabled > 0 { + if tpm2.PermanentAttributes(props[0].Value)&tpm2.AttrDisableClear > 0 { return ErrTPMClearRequiresPPI } - if err := tpm.Clear(tpm.LockoutHandleContext(), session); err != nil { + if err := t.Clear(t.LockoutHandleContext(), session); err != nil { switch { case isAuthFailError(err, tpm2.CommandClear, 1): return AuthFailError{tpm2.HandleLockout} @@ -179,12 +155,10 @@ } return xerrors.Errorf("cannot clear the TPM: %w", err) } - - status = 0 } // Provision an endorsement key - if _, err := provisionPrimaryKey(tpm.TPMContext, tpm.EndorsementHandleContext(), tcg.EKTemplate, tcg.EKHandle, session); err != nil { + if _, err := provisionPrimaryKey(t.TPMContext, t.EndorsementHandleContext(), tcg.EKTemplate, tcg.EKHandle, session); err != nil { switch { case isAuthFailError(err, tpm2.CommandEvictControl, 1): return AuthFailError{tpm2.HandleOwner} @@ -195,20 +169,19 @@ } } - // Close the existing session and create a new session that's salted with a value protected with the newly provisioned EK. + // Reinitialize the connection, which creates a new session that's salted with a value protected with the newly provisioned EK. // This will have a symmetric algorithm for parameter encryption during HierarchyChangeAuth. - tpm.FlushContext(session) - if err := tpm.init(); err != nil { + if err := t.init(); err != nil { var verifyErr verificationError if xerrors.As(err, &verifyErr) { return TPMVerificationError{fmt.Sprintf("cannot reinitialize TPM connection after provisioning endorsement key: %v", err)} } return xerrors.Errorf("cannot reinitialize TPM connection after provisioning endorsement key: %w", err) } - session = tpm.HmacSession() + session = t.HmacSession() // Provision a storage root key - srk, err := provisionPrimaryKey(tpm.TPMContext, tpm.OwnerHandleContext(), tcg.SRKTemplate, tcg.SRKHandle, session) + srk, err := provisionPrimaryKey(t.TPMContext, t.OwnerHandleContext(), tcg.SRKTemplate, tcg.SRKHandle, session) if err != nil { switch { case isAuthFailError(err, tpm2.AnyCommandCode, 1): @@ -217,25 +190,39 @@ return xerrors.Errorf("cannot provision storage root key: %w", err) } } - tpm.provisionedSrk = srk + t.provisionedSrk = srk - // Provision a lock NV index - if err := ensureLockNVIndex(tpm.TPMContext, session); err != nil { - var e *tpmErrorWithHandle - if tpm2.IsTPMError(err, tpm2.ErrorNVDefined, tpm2.AnyCommandCode) && xerrors.As(err, &e) { - return TPMResourceExistsError{e.handle} + if mode == ProvisionModeWithoutLockout { + props, err := t.GetCapabilityTPMProperties(tpm2.PropertyPermanent, 1, session.IncludeAttrs(tpm2.AttrAudit)) + if err != nil { + return xerrors.Errorf("cannot fetch permanent properties to determine if lockout hierarchy is required: %w", err) + } + if props[0].Property != tpm2.PropertyPermanent { + return errors.New("TPM returned value for the wrong property") + } + required := tpm2.AttrLockoutAuthSet | tpm2.AttrDisableClear + if tpm2.PermanentAttributes(props[0].Value)&required != required { + return ErrTPMProvisioningRequiresLockout + } + + props, err = t.GetCapabilityTPMProperties(tpm2.PropertyMaxAuthFail, 3, session.IncludeAttrs(tpm2.AttrAudit)) + if err != nil { + return xerrors.Errorf("cannot fetch DA parameters to determine if lockout hierarchy is required: %w", err) + } + if props[0].Property != tpm2.PropertyMaxAuthFail || props[1].Property != tpm2.PropertyLockoutInterval || props[2].Property != tpm2.PropertyLockoutRecovery { + return errors.New("TPM returned values for the wrong properties") + } + if props[0].Value > maxTries || props[1].Value < recoveryTime || props[2].Value < lockoutRecovery { + return ErrTPMProvisioningRequiresLockout } - return xerrors.Errorf("cannot create lock NV index: %w", err) - } - if mode == ProvisionModeWithoutLockout { return nil } // Perform actions that require the lockout hierarchy authorization. // Set the DA parameters. - if err := tpm.DictionaryAttackParameters(tpm.LockoutHandleContext(), maxTries, recoveryTime, lockoutRecovery, session); err != nil { + if err := t.DictionaryAttackParameters(t.LockoutHandleContext(), maxTries, recoveryTime, lockoutRecovery, session); err != nil { switch { case isAuthFailError(err, tpm2.CommandDictionaryAttackParameters, 1): return AuthFailError{tpm2.HandleLockout} @@ -246,14 +233,13 @@ } // Disable owner clear - if err := tpm.ClearControl(tpm.LockoutHandleContext(), true, session); err != nil { + if err := t.ClearControl(t.LockoutHandleContext(), true, session); err != nil { // Lockout auth failure or lockout mode would have been caught by DictionaryAttackParameters return xerrors.Errorf("cannot disable owner clear: %w", err) } // Set the lockout hierarchy authorization. - if err := tpm.HierarchyChangeAuth(tpm.LockoutHandleContext(), tpm2.Auth(newLockoutAuth), - session.IncludeAttrs(tpm2.AttrCommandEncrypt)); err != nil { + if err := t.HierarchyChangeAuth(t.LockoutHandleContext(), newLockoutAuth, session.IncludeAttrs(tpm2.AttrCommandEncrypt)); err != nil { return xerrors.Errorf("cannot set the lockout hierarchy authorization value: %w", err) } @@ -276,81 +262,3 @@ return nil } - -// ProvisionStatus returns the provisioning status for the specified TPM. -func ProvisionStatus(tpm *TPMConnection) (ProvisionStatusAttributes, error) { - var out ProvisionStatusAttributes - - session := tpm.HmacSession().IncludeAttrs(tpm2.AttrAudit) - - ek, err := tpm.CreateResourceContextFromTPM(tcg.EKHandle, session) - switch { - case err != nil && !tpm2.IsResourceUnavailableError(err, tcg.EKHandle): - // Unexpected error - return 0, err - case tpm2.IsResourceUnavailableError(err, tcg.EKHandle): - // Nothing to do - default: - if ekInit, err := tpm.EndorsementKey(); err == nil && bytes.Equal(ekInit.Name(), ek.Name()) { - out |= AttrValidEK - } - } - - srk, err := tpm.CreateResourceContextFromTPM(tcg.SRKHandle, session) - switch { - case err != nil && !tpm2.IsResourceUnavailableError(err, tcg.SRKHandle): - // Unexpected error - return 0, err - case tpm2.IsResourceUnavailableError(err, tcg.SRKHandle): - // Nothing to do - case tpm.provisionedSrk != nil: - // ProvisionTPM has been called with this TPMConnection. Make sure it's the same object - if bytes.Equal(tpm.provisionedSrk.Name(), srk.Name()) { - out |= AttrValidSRK - } - default: - // ProvisionTPM hasn't been called with this TPMConnection, but there is an object at tcg.SRKHandle. Make sure it looks like a storage - // primary key. - ok, err := isObjectPrimaryKeyWithTemplate(tpm.TPMContext, tpm.OwnerHandleContext(), srk, tcg.SRKTemplate, tpm.HmacSession()) - switch { - case err != nil: - return 0, xerrors.Errorf("cannot determine if object at %v is a primary key in the storage hierarchy: %w", tcg.SRKHandle, err) - case ok: - out |= AttrValidSRK - } - } - - props, err := tpm.GetCapabilityTPMProperties(tpm2.PropertyMaxAuthFail, 3) - if err != nil { - return 0, xerrors.Errorf("cannot fetch DA parameters: %w", err) - } - if props[0].Value <= maxTries && props[1].Value >= recoveryTime && props[2].Value >= lockoutRecovery { - out |= AttrDAParamsOK - } - - props, err = tpm.GetCapabilityTPMProperties(tpm2.PropertyPermanent, 1) - if err != nil { - return 0, xerrors.Errorf("cannot fetch permanent properties: %w", err) - } - if tpm2.PermanentAttributes(props[0].Value)&tpm2.AttrDisableClear > 0 { - out |= AttrOwnerClearDisabled - } - if tpm2.PermanentAttributes(props[0].Value)&tpm2.AttrLockoutAuthSet > 0 { - out |= AttrLockoutAuthSet - } - - lockIndex, err := tpm.CreateResourceContextFromTPM(lockNVHandle, session) - switch { - case err != nil && !tpm2.IsResourceUnavailableError(err, lockNVHandle): - // Unexpected error - return 0, err - case tpm2.IsResourceUnavailableError(err, lockNVHandle): - // Nothing to do - default: - if _, err := readAndValidateLockNVIndexPublic(tpm.TPMContext, lockIndex, session); err == nil { - out |= AttrValidLockNVIndex - } - } - - return out, nil -} diff -Nru snapd-2.47.1+20.10.1build1/vendor/github.com/snapcore/secboot/sdefistub_policy.go snapd-2.48+21.04/vendor/github.com/snapcore/secboot/sdefistub_policy.go --- snapd-2.47.1+20.10.1build1/vendor/github.com/snapcore/secboot/sdefistub_policy.go 2020-04-17 16:31:58.000000000 +0000 +++ snapd-2.48+21.04/vendor/github.com/snapcore/secboot/sdefistub_policy.go 2020-11-18 09:44:21.000000000 +0000 @@ -24,7 +24,7 @@ "errors" "github.com/canonical/go-tpm2" - "github.com/chrisccoulson/tcglog-parser" + "github.com/canonical/tcglog-parser" "golang.org/x/xerrors" ) diff -Nru snapd-2.47.1+20.10.1build1/vendor/github.com/snapcore/secboot/seal.go snapd-2.48+21.04/vendor/github.com/snapcore/secboot/seal.go --- snapd-2.47.1+20.10.1build1/vendor/github.com/snapcore/secboot/seal.go 2020-09-29 09:59:28.000000000 +0000 +++ snapd-2.48+21.04/vendor/github.com/snapcore/secboot/seal.go 2020-11-18 09:44:21.000000000 +0000 @@ -20,10 +20,11 @@ package secboot import ( + "bytes" "crypto" + "crypto/ecdsa" + "crypto/elliptic" "crypto/rand" - "crypto/rsa" - "crypto/x509" "errors" "fmt" "os" @@ -43,19 +44,24 @@ Params: tpm2.PublicParamsU{Data: &tpm2.KeyedHashParams{Scheme: tpm2.KeyedHashScheme{Scheme: tpm2.KeyedHashSchemeNull}}}} } -func computeSealedKeyDynamicAuthPolicy(tpm *tpm2.TPMContext, version uint32, alg, signAlg tpm2.HashAlgorithmId, authKey *rsa.PrivateKey, - countIndexPub *tpm2.NVPublic, countIndexAuthPolicies tpm2.DigestList, pcrProfile *PCRProtectionProfile, +func computeSealedKeyDynamicAuthPolicy(tpm *tpm2.TPMContext, version uint32, alg, signAlg tpm2.HashAlgorithmId, authKey crypto.PrivateKey, + counterPub *tpm2.NVPublic, counterAuthPolicies tpm2.DigestList, pcrProfile *PCRProtectionProfile, session tpm2.SessionContext) (*dynamicPolicyData, error) { - // Obtain the count for the new dynamic authorization policy - nextPolicyCount, err := readDynamicPolicyCounter(tpm, countIndexPub, countIndexAuthPolicies, session) - if err != nil { - return nil, xerrors.Errorf("cannot read dynamic policy counter: %w", err) - } - nextPolicyCount += 1 + // Obtain the count for the new policy + var nextPolicyCount uint64 + var counterName tpm2.Name + if counterPub != nil { + var err error + nextPolicyCount, err = readPcrPolicyCounter(tpm, version, counterPub, counterAuthPolicies, session) + if err != nil { + return nil, xerrors.Errorf("cannot read policy counter: %w", err) + } + nextPolicyCount += 1 - countIndexName, _ := countIndexPub.Name() - if err != nil { - return nil, xerrors.Errorf("cannot compute name of dynamic policy counter: %w", err) + counterName, err = counterPub.Name() + if err != nil { + return nil, xerrors.Errorf("cannot compute name of policy counter: %w", err) + } } supportedPcrs, err := tpm.GetCapabilityPCRs(session.IncludeAttrs(tpm2.AttrAudit)) @@ -94,12 +100,12 @@ // Use the PCR digests and NV index names to generate a single signed dynamic authorization policy digest policyParams := dynamicPolicyComputeParams{ - key: authKey, - signAlg: signAlg, - pcrs: pcrs, - pcrDigests: pcrDigests, - policyCountIndexName: countIndexName, - policyCount: nextPolicyCount} + key: authKey, + signAlg: signAlg, + pcrs: pcrs, + pcrDigests: pcrDigests, + policyCounterName: counterName, + policyCount: nextPolicyCount} policyData, err := computeDynamicPolicy(version, alg, &policyParams) if err != nil { @@ -114,43 +120,72 @@ // PCRProfile defines the profile used to generate a PCR protection policy for the newly created sealed key file. PCRProfile *PCRProtectionProfile - // PINHandle is the handle at which to create a NV index for PIN support. The handle must be a valid NV index handle (MSO == 0x01) - // and the choice of handle should take in to consideration the reserved indices from the "Registry of reserved TPM 2.0 handles and - // localities" specification. It is recommended that the handle is in the block reserved for owner objects (0x01800000 - 0x01bfffff). - PINHandle tpm2.Handle + // PCRPolicyCounterHandle is the handle at which to create a NV index for dynamic authorization poliy revocation support. The handle + // must either be tpm2.HandleNull (in which case, no NV index will be created and the sealed key will not benefit from dynamic + // authorization policy revocation support), or it must be a valid NV index handle (MSO == 0x01). The choice of handle should take + // in to consideration the reserved indices from the "Registry of reserved TPM 2.0 handles and localities" specification. It is + // recommended that the handle is in the block reserved for owner objects (0x01800000 - 0x01bfffff). + PCRPolicyCounterHandle tpm2.Handle + + // AuthKey can be set to chose an auhorisation key whose + // private part will be used for authorizing PCR policy + // updates with UpdateKeyPCRProtectionPolicy + // If set a key from elliptic.P256 must be used, + // if not set one is generated. + AuthKey *ecdsa.PrivateKey } -// SealKeyToTPM seals the supplied disk encryption key to the storage hierarchy of the TPM. The sealed key object and associated -// metadata that is required during early boot in order to unseal the key again and unlock the associated encrypted volume is written -// to a file at the path specified by keyPath. Additional data that is required in order to update the authorization policy for the -// sealed key is written to a file at the path specified by policyUpdatePath. This file must live inside the encrypted volume -// protected by the sealed key. -// -// The supplied key must be 64-bytes long. An error will be returned if it isn't. +// SealKeyRequest corresponds to a key that should be sealed by SealKeyToTPMMultiple +// to a file at the specified path. +type SealKeyRequest struct { + Key []byte + Path string +} + +// SealKeyToTPMMultiple seals the supplied disk encryption keys to the storage hierarchy of the TPM. The keys are specified by +// the keys argument, which is a slice of associated key and corresponding file path. The sealed key objects and associated +// metadata that is required during early boot in order to unseal the keys again and unlock the associated encrypted volumes +// are written to files at the specifed paths. // // This function requires knowledge of the authorization value for the storage hierarchy, which must be provided by calling // TPMConnection.OwnerHandleContext().SetAuthValue() prior to calling this function. If the provided authorization value is incorrect, // a AuthFailError error will be returned. // -// If the TPM is not correctly provisioned, a ErrTPMProvisioning error will be returned. In this case, ProvisionTPM must be called -// before proceeding. +// This function expects there to be no files at the specified paths. If the keys argument references a file that already exists, a +// wrapped *os.PathError error will be returned with an underlying error of syscall.EEXIST. A wrapped *os.PathError error will be +// returned if any file cannot be created and opened for writing. // -// This function expects there to be no files at the specified paths. If either path references a file that already exists, a wrapped -// *os.PathError error will be returned with an underlying error of syscall.EEXIST. A wrapped *os.PathError error will be returned if -// either file cannot be created and opened for writing. +// This function will create a NV index at the handle specified by the PCRPolicyCounterHandle field of the params argument if it is +// not tpm2.HandleNull. If the handle is already in use, a TPMResourceExistsError error will be returned. In this case, the caller +// will need to either choose a different handle or undefine the existing one. If it is not tpm2.HandleNull, then it must be a valid +// NV index handle (MSO == 0x01), and the choice of handle should take in to consideration the reserved indices from the "Registry of +// reserved TPM 2.0 handles and localities" specification. It is recommended that the handle is in the block reserved for owner +// objects (0x01800000 - 0x01bfffff). // -// This function will create a NV index at the handle specified by the PINHandle field of the params argument. If the handle is already -// in use, a TPMResourceExistsError error will be returned. In this case, the caller will need to either choose a different handle or -// undefine the existing one. The handle must be a valid NV index handle (MSO == 0x01), and the choice of handle should take in to -// consideration the reserved indices from the "Registry of reserved TPM 2.0 handles and localities" specification. It is recommended -// that the handle is in the block reserved for owner objects (0x01800000 - 0x01bfffff). +// All keys will be created with the same authorization policy, and will be protected with a PCR policy computed from the +// PCRProtectionProfile supplied via the PCRProfile field of the params argument. // -// The key will be protected with a PCR policy computed from the PCRProtectionProfile supplied via the PCRProfile field of the params -// argument. -func SealKeyToTPM(tpm *TPMConnection, key []byte, keyPath, policyUpdatePath string, params *KeyCreationParams) error { +// If any part of this function fails, no sealed keys will be created. +// +// On success, this function returns the private part of the key used for authorizing PCR policy updates with +// UpdateKeyPCRProtectionPolicyMultiple. This key doesn't need to be stored anywhere, and certainly mustn't be stored outside of the +// encrypted volume protected with this sealed key file. The key is stored encrypted inside this sealed key file and returned from +// future calls to SealedKeyObject.UnsealFromTPM. +// +// The authorization key can also be chosen and provided by setting +// AuthKey in the params argument. +func SealKeyToTPMMultiple(tpm *TPMConnection, keys []*SealKeyRequest, params *KeyCreationParams) (authKey TPMPolicyAuthKey, err error) { // params is mandatory. if params == nil { - return errors.New("no KeyCreationParams provided") + return nil, errors.New("no KeyCreationParams provided") + } + if len(keys) == 0 { + return nil, errors.New("no keys provided") + } + + // Perform some sanity checks on params. + if params.AuthKey != nil && params.AuthKey.Curve != elliptic.P256() { + return nil, errors.New("provided AuthKey must be from elliptic.P256, no other curve is supported") } // Use the HMAC session created when the connection was opened rather than creating a new one. @@ -166,125 +201,72 @@ srk, err = provisionPrimaryKey(tpm.TPMContext, tpm.OwnerHandleContext(), tcg.SRKTemplate, tcg.SRKHandle, session) switch { case isAuthFailError(err, tpm2.AnyCommandCode, 1): - return AuthFailError{tpm2.HandleOwner} + return nil, AuthFailError{tpm2.HandleOwner} case err != nil: - return xerrors.Errorf("cannot provision storage root key: %w", err) + return nil, xerrors.Errorf("cannot provision storage root key: %w", err) } } - // Validate that the lock NV index is valid and obtain its name - lockIndex, err := tpm.CreateResourceContextFromTPM(lockNVHandle) - switch { - case tpm2.IsResourceUnavailableError(err, lockNVHandle): - return ErrTPMProvisioning - case err != nil: - return xerrors.Errorf("cannot create context for lock NV index: %w", err) - } - - lockIndexPub, err := readAndValidateLockNVIndexPublic(tpm.TPMContext, lockIndex, session) - if err != nil { - return ErrTPMProvisioning - } - lockIndexName, err := lockIndexPub.Name() - if err != nil { - return xerrors.Errorf("cannot compute name of global lock NV index: %w", err) - } - succeeded := false - // Create destination files - keyFile, err := os.OpenFile(keyPath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600) + // Compute metadata. + + var goAuthKey *ecdsa.PrivateKey + // Use the provided authorization key, + // otherwise create an asymmetric key for signing + // authorization policy updates, and authorizing dynamic + // authorization policy revocations. + if params.AuthKey != nil { + goAuthKey = params.AuthKey + } else { + goAuthKey, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return nil, xerrors.Errorf("cannot generate key for signing dynamic authorization policies: %w", err) + } + } + authPublicKey := createTPMPublicAreaForECDSAKey(&goAuthKey.PublicKey) + authKeyName, err := authPublicKey.Name() if err != nil { - return xerrors.Errorf("cannot create key data file: %w", err) + return nil, xerrors.Errorf("cannot compute name of signing key for dynamic policy authorization: %w", err) } - defer func() { - keyFile.Close() - if succeeded { - return - } - os.Remove(keyPath) - }() + authKey = goAuthKey.D.Bytes() - var policyUpdateFile *os.File - if policyUpdatePath != "" { - var err error - policyUpdateFile, err = os.OpenFile(policyUpdatePath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600) - if err != nil { - return xerrors.Errorf("cannot create private data file: %w", err) + // Create PCR policy counter, if requested. + var pcrPolicyCounterPub *tpm2.NVPublic + if params.PCRPolicyCounterHandle != tpm2.HandleNull { + pcrPolicyCounterPub, err = createPcrPolicyCounter(tpm.TPMContext, params.PCRPolicyCounterHandle, authKeyName, session) + switch { + case tpm2.IsTPMError(err, tpm2.ErrorNVDefined, tpm2.CommandNVDefineSpace): + return nil, TPMResourceExistsError{params.PCRPolicyCounterHandle} + case isAuthFailError(err, tpm2.CommandNVDefineSpace, 1): + return nil, AuthFailError{tpm2.HandleOwner} + case err != nil: + return nil, xerrors.Errorf("cannot create new dynamic authorization policy counter: %w", err) } defer func() { - policyUpdateFile.Close() if succeeded { return } - os.Remove(policyUpdatePath) + index, err := tpm2.CreateNVIndexResourceContextFromPublic(pcrPolicyCounterPub) + if err != nil { + return + } + tpm.NVUndefineSpace(tpm.OwnerHandleContext(), index, session) }() } - // Create an asymmetric key for signing authorization policy updates, and authorizing dynamic authorization policy revocations. - authKey, err := rsa.GenerateKey(rand.Reader, 2048) - if err != nil { - return xerrors.Errorf("cannot generate RSA key pair for signing dynamic authorization policies: %w", err) - } - authPublicKey := createPublicAreaForRSASigningKey(&authKey.PublicKey) - authKeyName, err := authPublicKey.Name() - if err != nil { - return xerrors.Errorf("cannot compute name of signing key for dynamic policy authorization: %w", err) - } - - // Create pin NV index - pinIndexPub, pinIndexAuthPolicies, err := createPinNVIndex(tpm.TPMContext, params.PINHandle, authKeyName, session) - switch { - case tpm2.IsTPMError(err, tpm2.ErrorNVDefined, tpm2.CommandNVDefineSpace): - return TPMResourceExistsError{params.PINHandle} - case isAuthFailError(err, tpm2.CommandNVDefineSpace, 1): - return AuthFailError{tpm2.HandleOwner} - case err != nil: - return xerrors.Errorf("cannot create new pin NV index: %w", err) - } - defer func() { - if succeeded { - return - } - index, err := tpm2.CreateNVIndexResourceContextFromPublic(pinIndexPub) - if err != nil { - return - } - tpm.NVUndefineSpace(tpm.OwnerHandleContext(), index, session) - }() - template := makeSealedKeyTemplate() // Compute the static policy - this never changes for the lifetime of this key file staticPolicyData, authPolicy, err := computeStaticPolicy(template.NameAlg, &staticPolicyComputeParams{ - key: authPublicKey, - pinIndexPub: pinIndexPub, - pinIndexAuthPolicies: pinIndexAuthPolicies, - lockIndexName: lockIndexName}) + key: authPublicKey, + pcrPolicyCounterPub: pcrPolicyCounterPub}) if err != nil { - return xerrors.Errorf("cannot compute static authorization policy: %w", err) + return nil, xerrors.Errorf("cannot compute static authorization policy: %w", err) } // Define the template for the sealed key object, using the computed policy digest template.AuthPolicy = authPolicy - sensitive := tpm2.SensitiveCreate{Data: key} - - // Have the digest of the private data recorded in the creation data for the sealed data object. - authKeyBytes := x509.MarshalPKCS1PrivateKey(authKey) - h := crypto.SHA256.New() - if _, err := mu.MarshalToWriter(h, authKeyBytes); err != nil { - panic(fmt.Sprintf("cannot marshal dynamic authorization policy update data: %v", err)) - } - creationInfo := h.Sum(nil) - - // Now create the sealed key object. The command is integrity protected so if the object at the handle we expect the SRK to reside - // at has a different name (ie, if we're connected via a resource manager and somebody swapped the object with another one), this - // command will fail. We take advantage of parameter encryption here too. - priv, pub, creationData, _, creationTicket, err := - tpm.Create(srk, &sensitive, template, creationInfo, nil, session.IncludeAttrs(tpm2.AttrCommandEncrypt)) - if err != nil { - return xerrors.Errorf("cannot create sealed data object for key: %w", err) - } // Create a dynamic authorization policy pcrProfile := params.PCRProfile @@ -292,75 +274,126 @@ pcrProfile = &PCRProtectionProfile{} } dynamicPolicyData, err := computeSealedKeyDynamicAuthPolicy(tpm.TPMContext, currentMetadataVersion, template.NameAlg, - authPublicKey.NameAlg, authKey, pinIndexPub, pinIndexAuthPolicies, pcrProfile, session) + authPublicKey.NameAlg, goAuthKey, pcrPolicyCounterPub, nil, pcrProfile, session) if err != nil { - return xerrors.Errorf("cannot compute dynamic authorization policy: %w", err) + return nil, xerrors.Errorf("cannot compute dynamic authorization policy: %w", err) } - // Marshal the entire object (sealed key object and auxiliary data) to disk - data := keyData{ - version: currentMetadataVersion, - keyPrivate: priv, - keyPublic: pub, - authModeHint: AuthModeNone, - staticPolicyData: staticPolicyData, - dynamicPolicyData: dynamicPolicyData} - - if err := data.write(keyFile); err != nil { - return xerrors.Errorf("cannot write key data file: %w", err) - } - - if policyUpdateFile != nil { - policyUpdateData := keyPolicyUpdateData{ - version: currentMetadataVersion, - authKey: authKey, - creationInfo: creationInfo, - creationData: creationData, - creationTicket: creationTicket} - - // Marshal the private data to disk - if err := policyUpdateData.write(policyUpdateFile); err != nil { - return xerrors.Errorf("cannot write dynamic authorization policy update data file: %w", err) + // Clean up files on failure. + defer func() { + if succeeded { + return + } + for _, key := range keys { + os.Remove(key.Path) + } + }() + + // Seal each key. + for _, key := range keys { + // Create the destination file + f, err := os.OpenFile(key.Path, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600) + if err != nil { + return nil, xerrors.Errorf("cannot create key data file %s: %w", key.Path, err) + } + // We'll close this at the end of this loop, but make sure it is closed if the function + // returns early + defer f.Close() + + // Create the sensitive data + sealedData, err := mu.MarshalToBytes(sealedData{Key: key.Key, AuthPrivateKey: authKey}) + if err != nil { + panic(fmt.Sprintf("cannot marshal sensitive data: %v", err)) + } + sensitive := tpm2.SensitiveCreate{Data: sealedData} + + // Now create the sealed key object. The command is integrity protected so if the object at the handle we expect the SRK to reside + // at has a different name (ie, if we're connected via a resource manager and somebody swapped the object with another one), this + // command will fail. We take advantage of parameter encryption here too. + priv, pub, _, _, _, err := tpm.Create(srk, &sensitive, template, nil, nil, session.IncludeAttrs(tpm2.AttrCommandEncrypt)) + if err != nil { + return nil, xerrors.Errorf("cannot create sealed data object for key: %w", err) + } + + // Marshal the entire object (sealed key object and auxiliary data) to disk + data := keyData{ + version: currentMetadataVersion, + keyPrivate: priv, + keyPublic: pub, + authModeHint: AuthModeNone, + staticPolicyData: staticPolicyData, + dynamicPolicyData: dynamicPolicyData} + + if err := data.write(f); err != nil { + return nil, xerrors.Errorf("cannot write key data file: %w", err) } + + f.Close() } - if err := incrementDynamicPolicyCounter(tpm.TPMContext, pinIndexPub, pinIndexAuthPolicies, authKey, authPublicKey, session); err != nil { - return xerrors.Errorf("cannot increment dynamic policy counter: %w", err) + // Increment the PCR policy counter for the first time. + if pcrPolicyCounterPub != nil { + if err := incrementPcrPolicyCounter(tpm.TPMContext, currentMetadataVersion, pcrPolicyCounterPub, nil, goAuthKey, authPublicKey, + session); err != nil { + return nil, xerrors.Errorf("cannot increment PCR policy counter: %w", err) + } } succeeded = true - return nil + return authKey, nil } -// UpdateKeyPCRProtectionPolicy updates the PCR protection policy for the sealed key at the path specified by the keyPath argument -// to the profile defined by the pcrProfile argument. In order to do this, the caller must also specify the path to the policy update -// data file that was saved by SealKeyToTPM. +// SealKeyToTPM seals the supplied disk encryption key to the storage hierarchy of the TPM. The sealed key object and associated +// metadata that is required during early boot in order to unseal the key again and unlock the associated encrypted volume is written +// to a file at the path specified by keyPath. // -// If either file cannot be opened, a wrapped *os.PathError error will be returned. +// This function requires knowledge of the authorization value for the storage hierarchy, which must be provided by calling +// TPMConnection.OwnerHandleContext().SetAuthValue() prior to calling this function. If the provided authorization value is incorrect, +// a AuthFailError error will be returned. // -// If either file cannot be deserialized correctly or validation of the files fails, a InvalidKeyFileError error will be returned. +// If the TPM is not correctly provisioned, a ErrTPMProvisioning error will be returned. In this case, ProvisionTPM must be called +// before proceeding. // -// On success, the sealed key data file is updated atomically with an updated authorization policy that includes a PCR policy -// computed from the supplied PCRProtectionProfile. -func UpdateKeyPCRProtectionPolicy(tpm *TPMConnection, keyPath, policyUpdatePath string, pcrProfile *PCRProtectionProfile) error { - // Use the HMAC session created when the connection was opened rather than creating a new one. - session := tpm.HmacSession() +// This function expects there to be no file at the specified path. If keyPath references a file that already exists, a wrapped +// *os.PathError error will be returned with an underlying error of syscall.EEXIST. A wrapped *os.PathError error will be returned if +// the file cannot be created and opened for writing. +// +// This function will create a NV index at the handle specified by the PCRPolicyCounterHandle field of the params argument if it is not +// tpm2.HandleNull. If the handle is already in use, a TPMResourceExistsError error will be returned. In this case, the caller will +// need to either choose a different handle or undefine the existing one. If it is not tpm2.HandleNull, then it must be a valid NV +// index handle (MSO == 0x01), and the choice of handle should take in to consideration the reserved indices from the "Registry of +// reserved TPM 2.0 handles and localities" specification. It is recommended that the handle is in the block reserved for owner +// objects (0x01800000 - 0x01bfffff). +// +// The key will be protected with a PCR policy computed from the PCRProtectionProfile supplied via the PCRProfile field of the params +// argument. +// +// On success, this function returns the private part of the key used for authorizing PCR policy updates with +// UpdateKeyPCRProtectionPolicy. This key doesn't need to be stored anywhere, and certainly mustn't be stored outside of the encrypted +// volume protected with this sealed key file. The key is stored encrypted inside this sealed key file and returned from future calls +// to SealedKeyObject.UnsealFromTPM. +// +// The authorization key can also be chosen and provided by setting +// AuthKey in the params argument. +func SealKeyToTPM(tpm *TPMConnection, key []byte, keyPath string, params *KeyCreationParams) (authKey TPMPolicyAuthKey, err error) { + return SealKeyToTPMMultiple(tpm, []*SealKeyRequest{{Key: key, Path: keyPath}}, params) +} - // Open the key data file - keyFile, err := os.Open(keyPath) - if err != nil { - return xerrors.Errorf("cannot open key data file: %w", err) +func updateKeyPCRProtectionPolicyCommon(tpm *tpm2.TPMContext, keyPaths []string, authData interface{}, pcrProfile *PCRProtectionProfile, session tpm2.SessionContext) error { + if len(keyPaths) == 0 { + return errors.New("no key files supplied") } - defer keyFile.Close() - // Open the policy update data file - policyUpdateFile, err := os.Open(policyUpdatePath) + var datas []*keyData + // Open the primary data file + keyFile, err := os.Open(keyPaths[0]) if err != nil { - return xerrors.Errorf("cannot open private data file: %w", err) + return xerrors.Errorf("cannot open key data file: %w", err) } - defer policyUpdateFile.Close() + defer keyFile.Close() - data, policyUpdateData, pinIndexPublic, err := decodeAndValidateKeyData(tpm.TPMContext, keyFile, policyUpdateFile, session) + // Validate the primary file + primaryData, authKey, pcrPolicyCounterPub, err := decodeAndValidateKeyData(tpm, keyFile, authData, session) if err != nil { if isKeyFileError(err) { return InvalidKeyFileError{err.Error()} @@ -368,31 +401,117 @@ // FIXME: Turn the missing lock NV index in to ErrProvisioning return xerrors.Errorf("cannot read and validate key data file: %w", err) } + datas = append(datas, primaryData) - authKey := policyUpdateData.authKey - authPublicKey := data.staticPolicyData.AuthPublicKey - pinIndexAuthPolicies := data.staticPolicyData.PinIndexAuthPolicies + // Open and validate secondary files and make sure they are related + for _, p := range keyPaths[1:] { + keyFile, err := os.Open(p) + if err != nil { + return xerrors.Errorf("cannot open related key data file: %w", err) + } + defer keyFile.Close() + + data, _, _, err := decodeAndValidateKeyData(tpm, keyFile, nil, session) + if err != nil { + if isKeyFileError(err) { + return InvalidKeyFileError{err.Error() + " (" + p + ")"} + } + // FIXME: Turn the missing lock NV index in to ErrProvisioning + return xerrors.Errorf("cannot read and validate related key data file: %w", err) + } + // The metadata is valid and consistent with the object's static authorization policy. + // Verify that it also has the same static authorization policy as the first key object passed + // to this function. This policy digest includes a cryptographic record of the PCR policy counter + // and dynamic authorization policy signing key, so this is the only check required to determine + // if 2 keys are related. + if !bytes.Equal(data.keyPublic.AuthPolicy, primaryData.keyPublic.AuthPolicy) { + return InvalidKeyFileError{"key data file " + p + " is not a related key file"} + } + datas = append(datas, data) + } + + authPublicKey := primaryData.staticPolicyData.authPublicKey + v0PinIndexAuthPolicies := primaryData.staticPolicyData.v0PinIndexAuthPolicies // Compute a new dynamic authorization policy if pcrProfile == nil { pcrProfile = &PCRProtectionProfile{} } - policyData, err := computeSealedKeyDynamicAuthPolicy(tpm.TPMContext, data.version, data.keyPublic.NameAlg, authPublicKey.NameAlg, - authKey, pinIndexPublic, pinIndexAuthPolicies, pcrProfile, session) + policyData, err := computeSealedKeyDynamicAuthPolicy(tpm, primaryData.version, primaryData.keyPublic.NameAlg, authPublicKey.NameAlg, authKey, + pcrPolicyCounterPub, v0PinIndexAuthPolicies, pcrProfile, session) if err != nil { return xerrors.Errorf("cannot compute dynamic authorization policy: %w", err) } - // Atomically update the key data file - data.dynamicPolicyData = policyData + // Atomically update the key data files + for i, data := range datas { + data.dynamicPolicyData = policyData + + if err := data.writeToFileAtomic(keyPaths[i]); err != nil { + return xerrors.Errorf("cannot write key data file: %v", err) + } + } - if err := data.writeToFileAtomic(keyPath); err != nil { - return xerrors.Errorf("cannot write key data file: %v", err) + if pcrPolicyCounterPub == nil { + return nil } - if err := incrementDynamicPolicyCounter(tpm.TPMContext, pinIndexPublic, pinIndexAuthPolicies, authKey, authPublicKey, session); err != nil { - return xerrors.Errorf("cannot revoke old dynamic authorization policies: %w", err) + if err := incrementPcrPolicyCounter(tpm, primaryData.version, pcrPolicyCounterPub, v0PinIndexAuthPolicies, authKey, authPublicKey, session); err != nil { + return xerrors.Errorf("cannot revoke old PCR policies: %w", err) } return nil } + +// UpdateKeyPCRProtectionPolicyV0 updates the PCR protection policy for the sealed key at the path specified by the keyPath argument +// to the profile defined by the pcrProfile argument. This function only works with version 0 sealed key files. In order to do this, +// the caller must also specify the path to the policy update data file that was originally saved by SealKeyToTPM. +// +// If either file cannot be opened, a wrapped *os.PathError error will be returned. +// +// If either file cannot be deserialized correctly or validation of the files fails, a InvalidKeyFileError error will be returned. +// +// On success, the sealed key data file is updated atomically with an updated authorization policy that includes a PCR policy +// computed from the supplied PCRProtectionProfile. +func UpdateKeyPCRProtectionPolicyV0(tpm *TPMConnection, keyPath, policyUpdatePath string, pcrProfile *PCRProtectionProfile) error { + policyUpdateFile, err := os.Open(policyUpdatePath) + if err != nil { + return xerrors.Errorf("cannot open private data file: %w", err) + } + defer policyUpdateFile.Close() + + return updateKeyPCRProtectionPolicyCommon(tpm.TPMContext, []string{keyPath}, policyUpdateFile, pcrProfile, tpm.HmacSession()) +} + +// UpdateKeyPCRProtectionPolicy updates the PCR protection policy for the sealed key at the path specified by the keyPath argument +// to the profile defined by the pcrProfile argument. In order to do this, the caller must also specify the private part of the +// authorization key that was either returned by SealKeyToTPM or SealedKeyObject.UnsealFromTPM. +// +// If the file cannot be opened, a wrapped *os.PathError error will be returned. +// +// If the file cannot be deserialized correctly or validation of the file fails, a InvalidKeyFileError error will be returned. +// +// On success, the sealed key data file is updated atomically with an updated authorization policy that includes a PCR policy +// computed from the supplied PCRProtectionProfile. If the sealed key data file was created with a PCR policy counter, the +// previous PCR policy will be revoked. +func UpdateKeyPCRProtectionPolicy(tpm *TPMConnection, keyPath string, authKey TPMPolicyAuthKey, pcrProfile *PCRProtectionProfile) error { + return updateKeyPCRProtectionPolicyCommon(tpm.TPMContext, []string{keyPath}, authKey, pcrProfile, tpm.HmacSession()) +} + +// UpdateKeyPCRProtectionPolicyMultiple updates the PCR protection policy for the sealed keys at the paths specified +// by the keyPaths argument to the profile defined by the pcrProfile argument. The keys must all be related (ie, they +// were created using SealKeyToTPMMultiple). If any key in the supplied set is not related, an error will be returned. +// +// If any file cannot be opened, a wrapped *os.PathError error will be returned. +// +// If any file cannot be deserialized correctly or validation of a file fails, a InvalidKeyFileError error will +// be returned. +// +// On success, each sealed key data file is updated atomically with an updated authorization policy that includes a PCR +// policy computed from the supplied PCRProtectionProfile. If the sealed key data files were created with a PCR policy +// counter, the previous PCR policy will be revoked only when all of the sealed key data files have been updated +// successfully. If any file is not updated successfully, the previous PCR policy will not be revoked and the associated +// error will be returned. +func UpdateKeyPCRProtectionPolicyMultiple(tpm *TPMConnection, keyPaths []string, authKey TPMPolicyAuthKey, pcrProfile *PCRProtectionProfile) error { + return updateKeyPCRProtectionPolicyCommon(tpm.TPMContext, keyPaths, authKey, pcrProfile, tpm.HmacSession()) +} Binary files /tmp/tmpjRJ7OM/rjGEpcHPxV/snapd-2.47.1+20.10.1build1/vendor/github.com/snapcore/secboot/secboot.test and /tmp/tmpjRJ7OM/BRNzVSDwVe/snapd-2.48+21.04/vendor/github.com/snapcore/secboot/secboot.test differ diff -Nru snapd-2.47.1+20.10.1build1/vendor/github.com/snapcore/secboot/secureboot_policy.go snapd-2.48+21.04/vendor/github.com/snapcore/secboot/secureboot_policy.go --- snapd-2.47.1+20.10.1build1/vendor/github.com/snapcore/secboot/secureboot_policy.go 2020-09-29 09:59:28.000000000 +0000 +++ snapd-2.48+21.04/vendor/github.com/snapcore/secboot/secureboot_policy.go 2020-11-18 09:44:21.000000000 +0000 @@ -35,7 +35,7 @@ "strings" "github.com/canonical/go-tpm2" - "github.com/chrisccoulson/tcglog-parser" + "github.com/canonical/tcglog-parser" "github.com/snapcore/secboot/internal/efi" "github.com/snapcore/secboot/internal/pe1.14" "github.com/snapcore/snapd/osutil" @@ -72,12 +72,12 @@ ) var ( - shimGuid = tcglog.NewEFIGUID(0x605dab50, 0xe046, 0x4300, 0xabb6, [...]uint8{0x3d, 0xd8, 0x10, 0xdd, 0x8b, 0x23}) // SHIM_LOCK_GUID - efiGlobalVariableGuid = tcglog.NewEFIGUID(0x8be4df61, 0x93ca, 0x11d2, 0xaa0d, [...]uint8{0x00, 0xe0, 0x98, 0x03, 0x2b, 0x8c}) // EFI_GLOBAL_VARIABLE - efiImageSecurityDatabaseGuid = tcglog.NewEFIGUID(0xd719b2cb, 0x3d3a, 0x4596, 0xa3bc, [...]uint8{0xda, 0xd0, 0x0e, 0x67, 0x65, 0x6f}) // EFI_IMAGE_SECURITY_DATABASE_GUID + shimGuid = tcglog.MakeEFIGUID(0x605dab50, 0xe046, 0x4300, 0xabb6, [...]uint8{0x3d, 0xd8, 0x10, 0xdd, 0x8b, 0x23}) // SHIM_LOCK_GUID + efiGlobalVariableGuid = tcglog.MakeEFIGUID(0x8be4df61, 0x93ca, 0x11d2, 0xaa0d, [...]uint8{0x00, 0xe0, 0x98, 0x03, 0x2b, 0x8c}) // EFI_GLOBAL_VARIABLE + efiImageSecurityDatabaseGuid = tcglog.MakeEFIGUID(0xd719b2cb, 0x3d3a, 0x4596, 0xa3bc, [...]uint8{0xda, 0xd0, 0x0e, 0x67, 0x65, 0x6f}) // EFI_IMAGE_SECURITY_DATABASE_GUID - efiCertX509Guid = tcglog.NewEFIGUID(0xa5c059a1, 0x94e4, 0x4aa7, 0x87b5, [...]uint8{0xab, 0x15, 0x5c, 0x2b, 0xf0, 0x72}) // EFI_CERT_X509_GUID - efiCertTypePkcs7Guid = tcglog.NewEFIGUID(0x4aafd29d, 0x68df, 0x49ee, 0x8aa9, [...]uint8{0x34, 0x7d, 0x37, 0x56, 0x65, 0xa7}) // EFI_CERT_TYPE_PKCS7_GUID + efiCertX509Guid = tcglog.MakeEFIGUID(0xa5c059a1, 0x94e4, 0x4aa7, 0x87b5, [...]uint8{0xab, 0x15, 0x5c, 0x2b, 0xf0, 0x72}) // EFI_CERT_X509_GUID + efiCertTypePkcs7Guid = tcglog.MakeEFIGUID(0x4aafd29d, 0x68df, 0x49ee, 0x8aa9, [...]uint8{0x34, 0x7d, 0x37, 0x56, 0x65, 0xa7}) // EFI_CERT_TYPE_PKCS7_GUID oidSha256 = asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 2, 1} @@ -131,7 +131,7 @@ return out, int(hdr.Length), nil case winCertTypeEfiGuid: out := &winCertificateUefiGuid{} - if err := binary.Read(r, binary.LittleEndian, &out.CertType); err != nil { + if _, err := io.ReadFull(r, out.CertType[:]); err != nil { return nil, 0, xerrors.Errorf("cannot read WIN_CERTIFICATE_UEFI_GUID.CertType: %w", err) } out.Data = make([]byte, int(hdr.Length)-binary.Size(hdr)-binary.Size(out.CertType)) @@ -196,23 +196,23 @@ // nextSignatureList returns the SignatureType, SignatureHeader and EFI_SIGNATURE_DATA entries associated with the next // EFI_SIGNATURE_LIST. -func (d *secureBootDbIterator) nextSignatureList() (*tcglog.EFIGUID, []byte, [][]byte, error) { +func (d *secureBootDbIterator) nextSignatureList() (tcglog.EFIGUID, []byte, [][]byte, error) { start, _ := d.r.Seek(0, io.SeekCurrent) // Decode EFI_SIGNATURE_LIST.SignatureType var signatureType tcglog.EFIGUID - if err := binary.Read(d.r, binary.LittleEndian, &signatureType); err != nil { + if _, err := io.ReadFull(d.r, signatureType[:]); err != nil { if err == io.EOF { - return nil, nil, nil, err + return tcglog.EFIGUID{}, nil, nil, err } - return nil, nil, nil, xerrors.Errorf("cannot read EFI_SIGNATURE_LIST.SignatureType: %w", err) + return tcglog.EFIGUID{}, nil, nil, xerrors.Errorf("cannot read EFI_SIGNATURE_LIST.SignatureType: %w", err) } // Decode EFI_SIGNATURE_LIST.SignatureListSize, which indicates the size of the entire EFI_SIGNATURE_LIST, // including all of the EFI_SIGNATURE_DATA entries. var signatureListSize uint32 if err := binary.Read(d.r, binary.LittleEndian, &signatureListSize); err != nil { - return nil, nil, nil, xerrors.Errorf("cannot read EFI_SIGNATURE_LIST.SignatureListSize: %w", err) + return tcglog.EFIGUID{}, nil, nil, xerrors.Errorf("cannot read EFI_SIGNATURE_LIST.SignatureListSize: %w", err) } // Decode EFI_SIGNATURE_LIST.SignatureHeaderSize, which indicates the size of the optional header data between @@ -220,28 +220,28 @@ // Always zero for the signature types we care about. var signatureHeaderSize uint32 if err := binary.Read(d.r, binary.LittleEndian, &signatureHeaderSize); err != nil { - return nil, nil, nil, xerrors.Errorf("cannot read EFI_SIGNATURE_LIST.SignatureHeaderSize: %w", err) + return tcglog.EFIGUID{}, nil, nil, xerrors.Errorf("cannot read EFI_SIGNATURE_LIST.SignatureHeaderSize: %w", err) } // Decode EFI_SIGNATURE_LIST.SignatureSize, which indicates the size of each EFI_SIGNATURE_DATA entry. var signatureSize uint32 if err := binary.Read(d.r, binary.LittleEndian, &signatureSize); err != nil { - return nil, nil, nil, xerrors.Errorf("cannot read EFI_SIGNATURE_LIST.SignatureSize: %w", err) + return tcglog.EFIGUID{}, nil, nil, xerrors.Errorf("cannot read EFI_SIGNATURE_LIST.SignatureSize: %w", err) } if signatureSize < 16 { - return nil, nil, nil, errors.New("EFI_SIGNATURE_LIST.SignatureSize is invalid") + return tcglog.EFIGUID{}, nil, nil, errors.New("EFI_SIGNATURE_LIST.SignatureSize is invalid") } signatureHeader := make([]byte, signatureHeaderSize) if _, err := io.ReadFull(d.r, signatureHeader); err != nil { - return nil, nil, nil, xerrors.Errorf("cannot read EFI_SIGNATURE_LIST.SignatureHeader: %w", err) + return tcglog.EFIGUID{}, nil, nil, xerrors.Errorf("cannot read EFI_SIGNATURE_LIST.SignatureHeader: %w", err) } // Calculate the number of EFI_SIGNATURE_DATA entries endOfHeader, _ := d.r.Seek(0, io.SeekCurrent) signatureDataSize := int64(signatureListSize) - endOfHeader + start if signatureDataSize%int64(signatureSize) != 0 { - return nil, nil, nil, errors.New("EFI_SIGNATURE_LIST has inconsistent SignatureListSize, SignatureHeaderSize and SignatureSize fields") + return tcglog.EFIGUID{}, nil, nil, errors.New("EFI_SIGNATURE_LIST has inconsistent SignatureListSize, SignatureHeaderSize and SignatureSize fields") } numOfSignatures := signatureDataSize / int64(signatureSize) @@ -251,12 +251,12 @@ for i := int64(0); i < numOfSignatures; i++ { signature := make([]byte, signatureSize) if _, err := io.ReadFull(d.r, signature); err != nil { - return nil, nil, nil, xerrors.Errorf("cannot read EFI_SIGNATURE_DATA entry at index %d: %w", i, err) + return tcglog.EFIGUID{}, nil, nil, xerrors.Errorf("cannot read EFI_SIGNATURE_DATA entry at index %d: %w", i, err) } signatures = append(signatures, signature) } - return &signatureType, signatureHeader, signatures, nil + return signatureType, signatureHeader, signatures, nil } // efiSignatureData corresponds to a EFI_SIGNATURE_DATA entry from a secure boot database, with the inclusion of the SignatureType @@ -268,7 +268,7 @@ } func (e *efiSignatureData) encode(buf io.Writer) error { - if err := binary.Write(buf, binary.LittleEndian, e.owner); err != nil { + if _, err := buf.Write(e.owner[:]); err != nil { return fmt.Errorf("cannot write signature owner: %v", err) } if _, err := buf.Write(e.data); err != nil { @@ -297,7 +297,7 @@ // Decode EFI_SIGNATURE_DATA.SignatureOwner var signatureOwner tcglog.EFIGUID - if err := binary.Read(sr, binary.LittleEndian, &signatureOwner); err != nil { + if _, err := io.ReadFull(sr, signatureOwner[:]); err != nil { return nil, xerrors.Errorf("cannot decode EFI_SIGNATURE_DATA.SignatureOwner for signature at index %d in list index %d: %w", j, i, err) } @@ -306,7 +306,7 @@ return nil, xerrors.Errorf("cannot obtain EFI_SIGNATURE_DATA.SignatureData for signature at index %d in list index %d: %w", j, i, err) } - out = append(out, &efiSignatureData{signatureType: *sigType, owner: signatureOwner, data: data}) + out = append(out, &efiSignatureData{signatureType: sigType, owner: signatureOwner, data: data}) } } @@ -338,8 +338,8 @@ cert = c.(*winCertificateUefiGuid) } - if cert.CertType != *efiCertTypePkcs7Guid { - return nil, fmt.Errorf("update has invalid value for EFI_VARIABLE_AUTHENTICATION_2.AuthInfo.CertType (%s)", &cert.CertType) + if cert.CertType != efiCertTypePkcs7Guid { + return nil, fmt.Errorf("update has invalid value for EFI_VARIABLE_AUTHENTICATION_2.AuthInfo.CertType (%s)", cert.CertType) } filteredUpdate := new(bytes.Buffer) @@ -369,7 +369,7 @@ } return nil, xerrors.Errorf("cannot obtain signature list from target at index %d: %w", j, err) } - if *sigType != *updateSigType { + if sigType != updateSigType { // EFI_SIGNATURE_LIST.SignatureType doesn't match continue } @@ -560,7 +560,7 @@ } // isSecureBootConfigMeasurementEvent determines if event corresponds to the measurement of a secure boot configuration. -func isSecureBootConfigMeasurementEvent(event *tcglog.Event, guid *tcglog.EFIGUID, name string) bool { +func isSecureBootConfigMeasurementEvent(event *tcglog.Event, guid tcglog.EFIGUID, name string) bool { if event.PCRIndex != secureBootPCR { return false } @@ -568,12 +568,12 @@ return false } - efiVarData, isEfiVar := event.Data.(*tcglog.EFIVariableEventData) + efiVarData, isEfiVar := event.Data.(*tcglog.EFIVariableData) if !isEfiVar { return false } - return efiVarData.VariableName == *guid && efiVarData.UnicodeName == name + return efiVarData.VariableName == guid && efiVarData.UnicodeName == name } // isKEKMeasurementEvent determines if event corresponds to the measurement of KEK. @@ -717,9 +717,9 @@ // omputeAndExtendVariableMeasurement computes a EFI variable measurement from the supplied arguments and extends that to // this branch. -func (b *secureBootPolicyGenBranch) computeAndExtendVariableMeasurement(varName *tcglog.EFIGUID, unicodeName string, varData []byte) error { - data := tcglog.EFIVariableEventData{ - VariableName: *varName, +func (b *secureBootPolicyGenBranch) computeAndExtendVariableMeasurement(varName tcglog.EFIGUID, unicodeName string, varData []byte) error { + data := tcglog.EFIVariableData{ + VariableName: varName, UnicodeName: unicodeName, VariableData: varData} h := b.gen.pcrAlgorithm.NewHash() @@ -732,7 +732,7 @@ // processSignatureDbMeasurementEvent computes a EFI signature database measurement for the specified database and with the supplied // updates, and then extends that in to this branch. -func (b *secureBootPolicyGenBranch) processSignatureDbMeasurementEvent(guid *tcglog.EFIGUID, name, filename string, updates []*secureBootDbUpdate, updateQuirkMode sigDbUpdateQuirkMode) ([]byte, error) { +func (b *secureBootPolicyGenBranch) processSignatureDbMeasurementEvent(guid tcglog.EFIGUID, name, filename string, updates []*secureBootDbUpdate, updateQuirkMode sigDbUpdateQuirkMode) ([]byte, error) { db, err := ioutil.ReadFile(filepath.Join(efi.EFIVarsPath, filename)) if err != nil && !os.IsNotExist(err) { return nil, xerrors.Errorf("cannot read current variable: %w", err) @@ -789,7 +789,7 @@ return xerrors.Errorf("cannot decode DB contents: %w", err) } - b.dbSet.uefiDb = &secureBootDb{variableName: *efiImageSecurityDatabaseGuid, unicodeName: dbName, signatures: sigs} + b.dbSet.uefiDb = &secureBootDb{variableName: efiImageSecurityDatabaseGuid, unicodeName: dbName, signatures: sigs} return nil } @@ -851,9 +851,9 @@ // processShimExecutableLaunch updates the context in this branch with the supplied shim vendor certificate so that it can be used // later on when computing verification events in secureBootPolicyGenBranch.computeAndExtendVerificationMeasurement. func (b *secureBootPolicyGenBranch) processShimExecutableLaunch(vendorCert []byte) { - b.dbSet.shimDb = &secureBootDb{variableName: *shimGuid, unicodeName: shimName} + b.dbSet.shimDb = &secureBootDb{variableName: shimGuid, unicodeName: shimName} if vendorCert != nil { - b.dbSet.shimDb.signatures = append(b.dbSet.shimDb.signatures, &efiSignatureData{signatureType: *efiCertX509Guid, data: vendorCert}) + b.dbSet.shimDb.signatures = append(b.dbSet.shimDb.signatures, &efiSignatureData{signatureType: efiCertX509Guid, data: vendorCert}) } b.shimVerificationEvents = nil } @@ -913,7 +913,7 @@ for _, caSig := range db.signatures { // Ignore signatures that aren't X509 certificates - if caSig.signatureType != *efiCertX509Guid { + if caSig.signatureType != efiCertX509Guid { continue } @@ -961,7 +961,7 @@ } // Create event data, compute digest and perform extension for verification of this executable - eventData := tcglog.EFIVariableEventData{ + eventData := tcglog.EFIVariableData{ VariableName: authority.source.variableName, UnicodeName: authority.source.unicodeName, VariableData: varData.Bytes()} @@ -1319,26 +1319,17 @@ if err != nil { return xerrors.Errorf("cannot open TCG event log: %w", err) } - log, err := tcglog.NewLog(eventLog, tcglog.LogOptions{}) + log, err := tcglog.ParseLog(eventLog, &tcglog.LogOptions{}) if err != nil { - return xerrors.Errorf("cannot parse TCG event log header: %w", err) + return xerrors.Errorf("cannot parse TCG event log: %w", err) } if !log.Algorithms.Contains(tcglog.AlgorithmId(params.PCRAlgorithm)) { return errors.New("cannot compute secure boot policy profile: the TCG event log does not have the requested algorithm") } - // Parse events and make sure that the current boot is sane. - var events []*tcglog.Event - for { - event, err := log.NextEvent() - if err == io.EOF { - break - } - if err != nil { - return xerrors.Errorf("cannot parse TCG event log: %w", err) - } - + // Make sure that the current boot is sane. + for _, event := range log.Events { switch event.PCRIndex { case bootManagerCodePCR: if event.EventType == tcglog.EventTypeEFIAction && event.Data.String() == returningFromEfiApplicationEvent { @@ -1349,11 +1340,11 @@ case secureBootPCR: switch event.EventType { case tcglog.EventTypeEFIVariableDriverConfig: - efiVarData, isEfiVar := event.Data.(*tcglog.EFIVariableEventData) - if !isEfiVar { - return fmt.Errorf("%s secure boot policy event has invalid event data", event.EventType) + if err, isErr := event.Data.(error); isErr { + return fmt.Errorf("%s secure boot policy event has invalid event data: %v", event.EventType, err) } - if efiVarData.VariableName == *efiGlobalVariableGuid && efiVarData.UnicodeName == sbStateName { + efiVarData := event.Data.(*tcglog.EFIVariableData) + if efiVarData.VariableName == efiGlobalVariableGuid && efiVarData.UnicodeName == sbStateName { switch { case event.Index > 0: // The spec says that secure boot policy must be measured again if the system supports changing it before ExitBootServices @@ -1365,11 +1356,11 @@ } } case tcglog.EventTypeEFIVariableAuthority: - efiVarData, isEfiVar := event.Data.(*tcglog.EFIVariableEventData) - if !isEfiVar { - return fmt.Errorf("%s secure boot policy event has invalid event data", event.EventType) + if err, isErr := event.Data.(error); isErr { + return fmt.Errorf("%s secure boot policy event has invalid event data: %v", event.EventType, err) } - if efiVarData.VariableName == *shimGuid && efiVarData.UnicodeName == mokSbStateName { + efiVarData := event.Data.(*tcglog.EFIVariableData) + if efiVarData.VariableName == shimGuid && efiVarData.UnicodeName == mokSbStateName { // MokSBState is set to 0x01 if secure boot enforcement is disabled in shim. The variable is deleted when secure boot enforcement // is enabled, so don't bother looking at the value here. It doesn't make a lot of sense to create a policy if secure boot // enforcement is disabled in shim @@ -1377,7 +1368,6 @@ } } } - events = append(events, event) } // Initialize the secure boot PCR to 0 @@ -1390,12 +1380,12 @@ } // Find the verification event corresponding to the load of the first OS binary. - initialOSVerificationEvent, err := identifyInitialOSLaunchVerificationEvent(events) + initialOSVerificationEvent, err := identifyInitialOSLaunchVerificationEvent(log.Events) if err != nil { return xerrors.Errorf("cannot identify initial OS launch verification event: %w", err) } - gen := &secureBootPolicyGen{params.PCRAlgorithm, params.LoadSequences, events, initialOSVerificationEvent, sigDbUpdates} + gen := &secureBootPolicyGen{params.PCRAlgorithm, params.LoadSequences, log.Events, initialOSVerificationEvent, sigDbUpdates} profile1 := NewPCRProtectionProfile() if err := gen.run(profile1, sigDbUpdateQuirkModeNone); err != nil { diff -Nru snapd-2.47.1+20.10.1build1/vendor/github.com/snapcore/secboot/unseal.go snapd-2.48+21.04/vendor/github.com/snapcore/secboot/unseal.go --- snapd-2.47.1+20.10.1build1/vendor/github.com/snapcore/secboot/unseal.go 2020-09-02 10:31:40.000000000 +0000 +++ snapd-2.48+21.04/vendor/github.com/snapcore/secboot/unseal.go 2020-11-18 09:44:21.000000000 +0000 @@ -21,6 +21,7 @@ import ( "github.com/canonical/go-tpm2" + "github.com/canonical/go-tpm2/mu" "github.com/snapcore/secboot/internal/tcg" "golang.org/x/xerrors" @@ -61,30 +62,28 @@ // If the provided PIN is incorrect, then a ErrPINFail error will be returned and the TPM's dictionary attack counter will be // incremented. // -// If access to sealed key objects created by this package is disallowed until the next TPM reset or TPM restart, then a -// ErrSealedKeyAccessLocked error will be returned. -// // If the authorization policy check fails during unsealing, then a InvalidKeyFileError error will be returned. Note that this // condition can also occur as the result of an incorrectly provisioned TPM, which will be detected during a subsequent call to // SealKeyToTPM. // -// On success, the unsealed cleartext key is returned. -func (k *SealedKeyObject) UnsealFromTPM(tpm *TPMConnection, pin string) ([]byte, error) { +// On success, the unsealed cleartext key is returned as the first return value, and the private part of the key used for +// authorizing PCR policy updates with UpdateKeyPCRProtectionPolicy is returned as the second return value. +func (k *SealedKeyObject) UnsealFromTPM(tpm *TPMConnection, pin string) (key []byte, authKey TPMPolicyAuthKey, err error) { // Check if the TPM is in lockout mode props, err := tpm.GetCapabilityTPMProperties(tpm2.PropertyPermanent, 1) if err != nil { - return nil, xerrors.Errorf("cannot fetch properties from TPM: %w", err) + return nil, nil, xerrors.Errorf("cannot fetch properties from TPM: %w", err) } if tpm2.PermanentAttributes(props[0].Value)&tpm2.AttrInLockout > 0 { - return nil, ErrTPMLockout + return nil, nil, ErrTPMLockout } // Use the HMAC session created when the connection was opened for parameter encryption rather than creating a new one. hmacSession := tpm.HmacSession() // Load the key data - key, err := k.data.load(tpm.TPMContext, hmacSession) + keyObject, err := k.data.load(tpm.TPMContext, hmacSession) switch { case isKeyFileError(err): // A keyFileError can be as a result of an improperly provisioned TPM - detect if the object at tcg.SRKHandle is a valid primary key @@ -94,60 +93,73 @@ srk, err2 := tpm.CreateResourceContextFromTPM(tcg.SRKHandle) switch { case tpm2.IsResourceUnavailableError(err2, tcg.SRKHandle): - return nil, ErrTPMProvisioning + return nil, nil, ErrTPMProvisioning case err2 != nil: - return nil, xerrors.Errorf("cannot create context for SRK: %w", err2) + return nil, nil, xerrors.Errorf("cannot create context for SRK: %w", err2) } ok, err2 := isObjectPrimaryKeyWithTemplate(tpm.TPMContext, tpm.OwnerHandleContext(), srk, tcg.SRKTemplate, tpm.HmacSession()) switch { case err2 != nil: - return nil, xerrors.Errorf("cannot determine if object at 0x%08x is a primary key in the storage hierarchy: %w", tcg.SRKHandle, err2) + return nil, nil, xerrors.Errorf("cannot determine if object at 0x%08x is a primary key in the storage hierarchy: %w", tcg.SRKHandle, err2) case !ok: - return nil, ErrTPMProvisioning + return nil, nil, ErrTPMProvisioning } // This is probably a broken key file, but it could still be a provisioning error because we don't know if the SRK object was // created with the same template that ProvisionTPM uses. - return nil, InvalidKeyFileError{err.Error()} + return nil, nil, InvalidKeyFileError{err.Error()} case tpm2.IsResourceUnavailableError(err, tcg.SRKHandle): - return nil, ErrTPMProvisioning + return nil, nil, ErrTPMProvisioning case err != nil: - return nil, err + return nil, nil, err } - defer tpm.FlushContext(key) + defer tpm.FlushContext(keyObject) // Begin and execute policy session policySession, err := tpm.StartAuthSession(nil, nil, tpm2.SessionTypePolicy, nil, k.data.keyPublic.NameAlg) if err != nil { - return nil, xerrors.Errorf("cannot start policy session: %w", err) + return nil, nil, xerrors.Errorf("cannot start policy session: %w", err) } defer tpm.FlushContext(policySession) - if err := executePolicySession(tpm.TPMContext, policySession, k.data.staticPolicyData, k.data.dynamicPolicyData, pin, hmacSession); err != nil { + if err := executePolicySession(tpm.TPMContext, policySession, k.data.version, k.data.staticPolicyData, k.data.dynamicPolicyData, pin, hmacSession); err != nil { err = xerrors.Errorf("cannot complete authorization policy assertions: %w", err) switch { case isDynamicPolicyDataError(err): // TODO: Add a separate error for this - return nil, InvalidKeyFileError{err.Error()} + return nil, nil, InvalidKeyFileError{err.Error()} case isStaticPolicyDataError(err): - return nil, InvalidKeyFileError{err.Error()} + return nil, nil, InvalidKeyFileError{err.Error()} case isAuthFailError(err, tpm2.CommandPolicySecret, 1): - return nil, ErrPINFail + return nil, nil, ErrPINFail case tpm2.IsResourceUnavailableError(err, lockNVHandle): - return nil, ErrTPMProvisioning - case tpm2.IsTPMError(err, tpm2.ErrorNVLocked, tpm2.CommandPolicyNV): - return nil, ErrSealedKeyAccessLocked + return nil, nil, InvalidKeyFileError{"required legacy lock NV index is not present"} } - return nil, err + return nil, nil, err } + // For metadata version > 0, the PIN is the auth value for the sealed key object, and the authorization + // policy asserts that this value is known when the policy session is used. + keyObject.SetAuthValue([]byte(pin)) + // Unseal - keyData, err := tpm.Unseal(key, policySession, hmacSession.IncludeAttrs(tpm2.AttrResponseEncrypt)) + keyData, err := tpm.Unseal(keyObject, policySession, hmacSession.IncludeAttrs(tpm2.AttrResponseEncrypt)) switch { case tpm2.IsTPMSessionError(err, tpm2.ErrorPolicyFail, tpm2.CommandUnseal, 1): - return nil, InvalidKeyFileError{"the authorization policy check failed during unsealing"} + return nil, nil, InvalidKeyFileError{"the authorization policy check failed during unsealing"} + case isAuthFailError(err, tpm2.CommandUnseal, 1): + return nil, nil, ErrPINFail case err != nil: - return nil, xerrors.Errorf("cannot unseal key: %w", err) + return nil, nil, xerrors.Errorf("cannot unseal key: %w", err) + } + + if k.data.version == 0 { + return keyData, nil, nil + } + + var sealedData sealedData + if _, err := mu.UnmarshalFromBytes(keyData, &sealedData); err != nil { + return nil, nil, InvalidKeyFileError{err.Error()} } - return keyData, nil + return sealedData.Key, sealedData.AuthPrivateKey, nil } diff -Nru snapd-2.47.1+20.10.1build1/vendor/github.com/snapcore/secboot/utils.go snapd-2.48+21.04/vendor/github.com/snapcore/secboot/utils.go --- snapd-2.47.1+20.10.1build1/vendor/github.com/snapcore/secboot/utils.go 2020-09-29 09:59:28.000000000 +0000 +++ snapd-2.48+21.04/vendor/github.com/snapcore/secboot/utils.go 2020-11-18 09:44:21.000000000 +0000 @@ -21,8 +21,11 @@ import ( "bytes" - "crypto/rsa" + "crypto/ecdsa" + "crypto/elliptic" + "errors" "fmt" + "math/big" "os" "github.com/canonical/go-tpm2" @@ -119,20 +122,76 @@ return true, nil } -// createPublicAreaForRSASigningKey creates a *tpm2.Public from a go *rsa.PublicKey, which is suitable for loading +func bigIntToBytesZeroExtended(x *big.Int, bytes int) (out []byte) { + b := x.Bytes() + if len(b) > bytes { + return b + } + out = make([]byte, bytes) + copy(out[bytes-len(b):], b) + return +} + +// createPublicAreaForECDSAKey creates a *tpm2.Public from a go *ecdsa.PublicKey, which is suitable for loading // in to a TPM with TPMContext.LoadExternal. -func createPublicAreaForRSASigningKey(key *rsa.PublicKey) *tpm2.Public { +func createTPMPublicAreaForECDSAKey(key *ecdsa.PublicKey) *tpm2.Public { + var curve tpm2.ECCCurve + switch key.Curve { + case elliptic.P224(): + curve = tpm2.ECCCurveNIST_P224 + case elliptic.P256(): + curve = tpm2.ECCCurveNIST_P256 + case elliptic.P384(): + curve = tpm2.ECCCurveNIST_P384 + case elliptic.P521(): + curve = tpm2.ECCCurveNIST_P521 + default: + panic("unsupported curve") + } + return &tpm2.Public{ - Type: tpm2.ObjectTypeRSA, + Type: tpm2.ObjectTypeECC, NameAlg: tpm2.HashAlgorithmSHA256, Attrs: tpm2.AttrSensitiveDataOrigin | tpm2.AttrUserWithAuth | tpm2.AttrSign, Params: tpm2.PublicParamsU{ - Data: &tpm2.RSAParams{ + Data: &tpm2.ECCParams{ Symmetric: tpm2.SymDefObject{Algorithm: tpm2.SymObjectAlgorithmNull}, - Scheme: tpm2.RSAScheme{Scheme: tpm2.RSASchemeNull}, - KeyBits: uint16(key.N.BitLen()), - Exponent: uint32(key.E)}}, - Unique: tpm2.PublicIDU{Data: tpm2.PublicKeyRSA(key.N.Bytes())}} + Scheme: tpm2.ECCScheme{ + Scheme: tpm2.ECCSchemeECDSA, + Details: tpm2.AsymSchemeU{Data: &tpm2.SigSchemeECDSA{HashAlg: tpm2.HashAlgorithmSHA256}}}, + CurveID: curve, + KDF: tpm2.KDFScheme{Scheme: tpm2.KDFAlgorithmNull}}}, + Unique: tpm2.PublicIDU{ + Data: &tpm2.ECCPoint{ + X: bigIntToBytesZeroExtended(key.X, key.Params().BitSize/8), + Y: bigIntToBytesZeroExtended(key.Y, key.Params().BitSize/8)}}} +} + +func createECDSAPrivateKeyFromTPM(public *tpm2.Public, private tpm2.ECCParameter) (*ecdsa.PrivateKey, error) { + if public.Type != tpm2.ObjectTypeECC { + return nil, errors.New("unsupported type") + } + + var curve elliptic.Curve + switch public.Params.ECCDetail().CurveID { + case tpm2.ECCCurveNIST_P224: + curve = elliptic.P224() + case tpm2.ECCCurveNIST_P256: + curve = elliptic.P256() + case tpm2.ECCCurveNIST_P384: + curve = elliptic.P384() + case tpm2.ECCCurveNIST_P521: + curve = elliptic.P521() + default: + return nil, errors.New("unsupported curve") + } + + return &ecdsa.PrivateKey{ + PublicKey: ecdsa.PublicKey{ + Curve: curve, + X: new(big.Int).SetBytes(public.Unique.ECC().X), + Y: new(big.Int).SetBytes(public.Unique.ECC().Y)}, + D: new(big.Int).SetBytes(private)}, nil } // digestListContains indicates whether the specified digest is present in the list of digests. diff -Nru snapd-2.47.1+20.10.1build1/vendor/maze.io/x/crypto/afis/afis.go snapd-2.48+21.04/vendor/maze.io/x/crypto/afis/afis.go --- snapd-2.47.1+20.10.1build1/vendor/maze.io/x/crypto/afis/afis.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48+21.04/vendor/maze.io/x/crypto/afis/afis.go 2020-10-28 17:13:30.000000000 +0000 @@ -0,0 +1,119 @@ +package afis // import "maze.io/x/crypto/afis" + +import ( + "crypto/rand" + "crypto/sha1" + "encoding/binary" + "errors" + "hash" + "io" + "math" +) + +// Errors. +var ( + ErrMinStripe = errors.New("afis: at least one stripe is required") + ErrDataLen = errors.New("afis: data length is not multiple of stripes") +) + +// DefaultHash is our default hashing function. +var DefaultHash = sha1.New + +// Split data using the default SHA-1 hash. +func Split(data []byte, stripes int) ([]byte, error) { + return SplitHash(data, stripes, DefaultHash) +} + +// SplitHash splits data using the selected hash function. +func SplitHash(data []byte, stripes int, hashFunc func() hash.Hash) ([]byte, error) { + if stripes < 1 { + return nil, ErrMinStripe + } + var ( + blockSize = len(data) + block = make([]byte, blockSize) + random = make([]byte, blockSize) + splitted []byte + ) + for i := 0; i < stripes-1; i++ { + if _, err := io.ReadFull(rand.Reader, random); err != nil { + return nil, err + } + splitted = append(splitted, random...) + + xor(block, random, block) + block = diffuse(block, blockSize, hashFunc) + } + + size := len(splitted) + splitted = append(splitted, make([]byte, blockSize)...) + xor(splitted[size:], block, data) + + return splitted, nil +} + +// Merge data splitted previously with Split using the default SHA-1 hash. +func Merge(data []byte, stripes int) ([]byte, error) { + return MergeHash(data, stripes, DefaultHash) +} + +// MergeHash merges data splitted previously with the selected hash function. +func MergeHash(data []byte, stripes int, hashFunc func() hash.Hash) ([]byte, error) { + if len(data)%stripes != 0 { + return nil, ErrDataLen + } + + var ( + blockSize = len(data) / stripes + block = make([]byte, blockSize) + ) + for i := 0; i < stripes-1; i++ { + offset := i * blockSize + xor(block, data[offset:offset+blockSize], block) + block = diffuse(block, blockSize, hashFunc) + } + + xor(block, data[(stripes-1)*blockSize:], block) + return block, nil +} + +func xor(dst, src1, src2 []byte) { + for i := range dst { + dst[i] = src1[i] ^ src2[i] + } +} + +func diffuse(block []byte, size int, hashFunc func() hash.Hash) []byte { + var ( + hash = hashFunc() + digestSize = hash.Size() + blocks = int(math.Floor(float64(len(block)) / float64(digestSize))) + padding = len(block) % digestSize + diffused []byte + ) + + // Hash full blocks + for i := 0; i < blocks; i++ { + offset := i * digestSize + hash.Reset() + hash.Write(packInt(i)) + hash.Write(block[offset : offset+digestSize]) + diffused = append(diffused, hash.Sum(nil)...) + } + + // Hash remainder + if padding > 0 { + hash.Reset() + hash.Write(packInt(blocks)) + hash.Write(block[blocks*digestSize:]) + diffused = append(diffused, hash.Sum(nil)[:padding]...) + } + + return diffused +} + +func packInt(i int) []byte { + var packed [4]byte + binary.BigEndian.PutUint32(packed[:], uint32(i)) + return packed[:] +} diff -Nru snapd-2.47.1+20.10.1build1/vendor/maze.io/x/crypto/afis/doc.go snapd-2.48+21.04/vendor/maze.io/x/crypto/afis/doc.go --- snapd-2.47.1+20.10.1build1/vendor/maze.io/x/crypto/afis/doc.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48+21.04/vendor/maze.io/x/crypto/afis/doc.go 2020-10-28 17:13:30.000000000 +0000 @@ -0,0 +1,26 @@ +/* +Package afis implements Anti-Forensic Information Splitting + +The splitter supports secure data destruction crucial for secure on-disk key +management. The key idea is to bloat information and therefor improving the +chance of destroying a single bit of it. The information is bloated in such a +way, that a single missing bit causes the original information become +unrecoverable. The theory behind AFsplitter is presented in TKS1. + +The interface is simple. It consists of two functions: + + Split(data, stripes) + Merge(data, stripes) + +Split operates on data and returns information splitted data. Merge does +just the opposite: uses the information stored in data to recover the original +splitted data. + + +References + +AFsplitter reference implementation at http://clemens.endorphin.org/AFsplitter + +TKS1 paper at http://clemens.endorphin.org/TKS1-draft.pdf +*/ +package afis diff -Nru snapd-2.47.1+20.10.1build1/vendor/maze.io/x/crypto/LICENSE snapd-2.48+21.04/vendor/maze.io/x/crypto/LICENSE --- snapd-2.47.1+20.10.1build1/vendor/maze.io/x/crypto/LICENSE 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.48+21.04/vendor/maze.io/x/crypto/LICENSE 2020-10-28 17:13:30.000000000 +0000 @@ -0,0 +1,5 @@ +MIT License +Copyright (c) +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff -Nru snapd-2.47.1+20.10.1build1/vendor/vendor.json snapd-2.48+21.04/vendor/vendor.json --- snapd-2.47.1+20.10.1build1/vendor/vendor.json 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/vendor/vendor.json 2020-11-19 16:51:02.000000000 +0000 @@ -25,10 +25,10 @@ "revisionTime": "2020-08-24T11:54:14Z" }, { - "checksumSHA1": "eDjzake0GpHm9kfTH7FMUWX8zVA=", - "path": "github.com/chrisccoulson/tcglog-parser", - "revision": "7b0f085a85398d368e10382a21a44ec2226c35b3", - "revisionTime": "2020-02-28T14:36:39Z" + "checksumSHA1": "QGwWyf2f/5+FacNj2iKpjp8N5hg=", + "path": "github.com/canonical/tcglog-parser", + "revision": "12a3a7bcf5a14486e838fea211e8d4aa280e9671", + "revisionTime": "2020-09-08T16:50:21Z" }, { "checksumSHA1": "zg16zjZTQ9R89+UOLmEZxHgxDtM=", @@ -116,40 +116,40 @@ "revisionTime": "2017-09-28T14:21:59Z" }, { - "checksumSHA1": "bNQROczU0gF+BeQHApftqWNUMe8=", + "checksumSHA1": "G2sH9o/0sihaKYbjmfWBOFU3Avs=", "path": "github.com/snapcore/secboot", - "revision": "7e933de20d914ff47ad080f25d2ed93cef6e8530", - "revisionTime": "2020-09-10T15:49:09Z" + "revision": "fa14f1ac3b14d38025312da287728054f7c06b67", + "revisionTime": "2020-11-11T08:01:43Z" }, { "checksumSHA1": "c7jHLQSWFWbymTcFWZMQH0C5Wik=", "path": "github.com/snapcore/secboot/internal/efi", - "revision": "7e933de20d914ff47ad080f25d2ed93cef6e8530", - "revisionTime": "2020-09-10T15:49:09Z" + "revision": "bd7a6eabe9371024327d0133a5e1915df27c9eed", + "revisionTime": "2020-10-27T12:12:33Z" }, { "checksumSHA1": "loFEiH6evGaDnDSlQgk3ugemkcU=", "path": "github.com/snapcore/secboot/internal/pe1.14", - "revision": "7e933de20d914ff47ad080f25d2ed93cef6e8530", - "revisionTime": "2020-09-10T15:49:09Z" + "revision": "bd7a6eabe9371024327d0133a5e1915df27c9eed", + "revisionTime": "2020-10-27T12:12:33Z" }, { "checksumSHA1": "kDay47kq9OgDplpkrYw0/a8Z+YY=", "path": "github.com/snapcore/secboot/internal/tcg", - "revision": "7e933de20d914ff47ad080f25d2ed93cef6e8530", - "revisionTime": "2020-09-10T15:49:09Z" + "revision": "bd7a6eabe9371024327d0133a5e1915df27c9eed", + "revisionTime": "2020-10-27T12:12:33Z" }, { "checksumSHA1": "PRS8ACUu14shrvAgb747Izc25ns=", "path": "github.com/snapcore/secboot/internal/tcti", - "revision": "7e933de20d914ff47ad080f25d2ed93cef6e8530", - "revisionTime": "2020-09-10T15:49:09Z" + "revision": "bd7a6eabe9371024327d0133a5e1915df27c9eed", + "revisionTime": "2020-10-27T12:12:33Z" }, { "checksumSHA1": "TnfofdyojXYWOwdWCKMY5RCeI7s=", "path": "github.com/snapcore/secboot/internal/truststore", - "revision": "7e933de20d914ff47ad080f25d2ed93cef6e8530", - "revisionTime": "2020-09-10T15:49:09Z" + "revision": "bd7a6eabe9371024327d0133a5e1915df27c9eed", + "revisionTime": "2020-10-27T12:12:33Z" }, { "checksumSHA1": "3AmEm18mKj8XxBuru/ix4OOpRkE=", @@ -301,6 +301,13 @@ "path": "gopkg.in/yaml.v2", "revision": "86f5ed62f8a0ee96bd888d2efdfd6d4fb100a4eb", "revisionTime": "2018-03-26T05:07:29Z" + }, + { + "checksumSHA1": "tZ9GNzrjTZEPDhoJEkouGPKRmZk=", + "origin": "github.com/pedronis/maze.io-x-crypto/afis", + "path": "maze.io/x/crypto/afis", + "revision": "9b94c9afe06676a7da39a608a48ed4e31f5d125f", + "revisionTime": "2019-01-31T09:06:03Z" } ], "rootPath": "github.com/snapcore/build-area/snapd-tmp" diff -Nru snapd-2.47.1+20.10.1build1/wrappers/core18.go snapd-2.48+21.04/wrappers/core18.go --- snapd-2.47.1+20.10.1build1/wrappers/core18.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/wrappers/core18.go 2020-11-19 16:51:02.000000000 +0000 @@ -26,6 +26,7 @@ "os" "path/filepath" "regexp" + "syscall" "time" "github.com/snapcore/snapd/dirs" @@ -137,7 +138,7 @@ return nil } - sysd := systemd.New(dirs.GlobalRootDir, systemd.SystemMode, inter) + sysd := systemd.New(systemd.SystemMode, inter) if err := writeSnapdToolingMountUnit(sysd, s.MountDir()); err != nil { return err @@ -383,7 +384,7 @@ return err } - sysd := systemd.New(dirs.GlobalRootDir, systemd.GlobalUserMode, inter) + sysd := systemd.New(systemd.GlobalUserMode, inter) serviceUnits, err := filepath.Glob(filepath.Join(s.MountDir(), "usr/lib/systemd/user/*.service")) if err != nil { @@ -451,7 +452,7 @@ // deployed in the filesystem as part of snapd snap installation. This should // only be executed as part of a controlled undo path. func undoSnapdUserServicesOnCore(s *snap.Info, inter interacter) error { - sysd := systemd.New(dirs.GlobalRootDir, systemd.GlobalUserMode, inter) + sysd := systemd.NewUnderRoot(dirs.GlobalRootDir, systemd.GlobalUserMode, inter) // list user service and socket units present in the snapd snap serviceUnits, err := filepath.Glob(filepath.Join(s.MountDir(), "usr/lib/systemd/user/*.service")) @@ -516,15 +517,40 @@ return sessionContent, systemContent, nil } +func isReadOnlyFsError(err error) bool { + if err == nil { + return false + } + if e, ok := err.(*os.PathError); ok { + err = e.Err + } + if e, ok := err.(syscall.Errno); ok { + return e == syscall.EROFS + } + return false +} + +var ensureDirState = osutil.EnsureDirState + func writeSnapdDbusConfigOnCore(s *snap.Info) error { sessionContent, systemContent, err := DeriveSnapdDBusConfig(s) if err != nil { return err } - _, _, err = osutil.EnsureDirState(dirs.SnapDBusSessionPolicyDir, "snapd.*.conf", sessionContent) + _, _, err = ensureDirState(dirs.SnapDBusSessionPolicyDir, "snapd.*.conf", sessionContent) if err != nil { - return err + if isReadOnlyFsError(err) { + // If /etc/dbus-1/session.d is read-only (which may be the case on very old core18), then + // err is os.PathError with syscall.Errno underneath. Hitting this prevents snapd refresh, + // so log the error but carry on. This fixes LP: 1899664. + // XXX: ideally we should regenerate session files elsewhere if we fail here (otherwise + // this will only happen on future snapd refresh), but realistically this + // is not relevant on core18 devices. + logger.Noticef("%s appears to be read-only, could not write snapd dbus config files", dirs.SnapDBusSessionPolicyDir) + } else { + return err + } } _, _, err = osutil.EnsureDirState(dirs.SnapDBusSystemPolicyDir, "snapd.*.conf", systemContent) @@ -558,7 +584,7 @@ return nil } - sysd := systemd.New(dirs.GlobalRootDir, systemd.SystemMode, inter) + sysd := systemd.NewUnderRoot(dirs.GlobalRootDir, systemd.SystemMode, inter) if err := undoSnapdDbusConfigOnCore(s); err != nil { return err diff -Nru snapd-2.47.1+20.10.1build1/wrappers/core18_test.go snapd-2.48+21.04/wrappers/core18_test.go --- snapd-2.47.1+20.10.1build1/wrappers/core18_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/wrappers/core18_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -23,10 +23,12 @@ "fmt" "os" "path/filepath" + "syscall" . "gopkg.in/check.v1" "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/logger" "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/progress" "github.com/snapcore/snapd/release" @@ -100,9 +102,9 @@ s := fmt.Sprintf("Type=oneshot\nId=%s\nActiveState=inactive\nUnitFileState=enabled\n", cmd[2]) return []byte(s), nil } - if len(cmd) == 4 && cmd[2] == "is-enabled" { + if len(cmd) == 2 && cmd[0] == "is-enabled" { // pretend snapd.socket is disabled - if cmd[3] == "snapd.socket" { + if cmd[1] == "snapd.socket" { return []byte("disabled"), &mockSystemctlError{msg: "disabled", exitCode: 1} } return []byte("enabled"), nil @@ -173,34 +175,34 @@ // check the systemctl calls c.Check(s.sysdLog, DeepEquals, [][]string{ {"daemon-reload"}, - {"--root", dirs.GlobalRootDir, "enable", "usr-lib-snapd.mount"}, + {"enable", "usr-lib-snapd.mount"}, {"stop", "usr-lib-snapd.mount"}, {"show", "--property=ActiveState", "usr-lib-snapd.mount"}, {"start", "usr-lib-snapd.mount"}, {"daemon-reload"}, - {"--root", dirs.GlobalRootDir, "is-enabled", "snapd.autoimport.service"}, - {"--root", dirs.GlobalRootDir, "is-enabled", "snapd.service"}, - {"--root", dirs.GlobalRootDir, "is-enabled", "snapd.snap-repair.timer"}, + {"is-enabled", "snapd.autoimport.service"}, + {"is-enabled", "snapd.service"}, + {"is-enabled", "snapd.snap-repair.timer"}, // test pretends snapd.socket is disabled and needs enabling - {"--root", dirs.GlobalRootDir, "is-enabled", "snapd.socket"}, - {"--root", dirs.GlobalRootDir, "enable", "snapd.socket"}, - {"--root", dirs.GlobalRootDir, "is-enabled", "snapd.system-shutdown.service"}, - {"--root", dirs.GlobalRootDir, "is-active", "snapd.autoimport.service"}, + {"is-enabled", "snapd.socket"}, + {"enable", "snapd.socket"}, + {"is-enabled", "snapd.system-shutdown.service"}, + {"is-active", "snapd.autoimport.service"}, {"stop", "snapd.autoimport.service"}, {"show", "--property=ActiveState", "snapd.autoimport.service"}, {"start", "snapd.autoimport.service"}, - {"--root", dirs.GlobalRootDir, "is-active", "snapd.snap-repair.timer"}, + {"is-active", "snapd.snap-repair.timer"}, {"stop", "snapd.snap-repair.timer"}, {"show", "--property=ActiveState", "snapd.snap-repair.timer"}, {"start", "snapd.snap-repair.timer"}, - {"--root", dirs.GlobalRootDir, "is-active", "snapd.socket"}, + {"is-active", "snapd.socket"}, {"start", "snapd.service"}, {"start", "--no-block", "snapd.seeded.service"}, {"start", "--no-block", "snapd.autoimport.service"}, - {"--user", "--global", "--root", dirs.GlobalRootDir, "disable", "snapd.session-agent.service"}, - {"--user", "--global", "--root", dirs.GlobalRootDir, "enable", "snapd.session-agent.service"}, - {"--user", "--global", "--root", dirs.GlobalRootDir, "disable", "snapd.session-agent.socket"}, - {"--user", "--global", "--root", dirs.GlobalRootDir, "enable", "snapd.session-agent.socket"}, + {"--user", "--global", "disable", "snapd.session-agent.service"}, + {"--user", "--global", "enable", "snapd.session-agent.service"}, + {"--user", "--global", "disable", "snapd.session-agent.socket"}, + {"--user", "--global", "enable", "snapd.session-agent.socket"}, }) } @@ -227,6 +229,39 @@ c.Check(s.sysdLog, IsNil) } +func (s *servicesTestSuite) TestAddSessionServicesWithReadOnlyFilesystem(c *C) { + restore := release.MockOnClassic(false) + defer restore() + + restoreEnsureDirState := wrappers.MockEnsureDirState(func(dir string, glob string, content map[string]osutil.FileState) (changed, removed []string, err error) { + return nil, nil, &os.PathError{Err: syscall.EROFS} + }) + defer restoreEnsureDirState() + + info := makeMockSnapdSnap(c) + + logBuf, restore := logger.MockLogger() + defer restore() + + // add the snapd service + err := wrappers.AddSnapdSnapServices(info, progress.Null) + + // didn't fail despite of read-only SnapDBusSessionPolicyDir + c.Assert(err, IsNil) + + // check that snapd services were *not* created + c.Check(osutil.FileExists(filepath.Join(dirs.SnapServicesDir, "snapd.service")), Equals, true) + c.Check(osutil.FileExists(filepath.Join(dirs.SnapServicesDir, "snapd.autoimport.service")), Equals, true) + c.Check(osutil.FileExists(filepath.Join(dirs.SnapServicesDir, "snapd.system-shutdown.service")), Equals, true) + c.Check(osutil.FileExists(filepath.Join(dirs.SnapServicesDir, "usr-lib-snapd.mount")), Equals, true) + c.Check(osutil.FileExists(filepath.Join(dirs.SnapUserServicesDir, "snapd.session-agent.service")), Equals, true) + c.Check(osutil.FileExists(filepath.Join(dirs.SnapUserServicesDir, "snapd.session-agent.socket")), Equals, true) + c.Check(osutil.FileExists(filepath.Join(dirs.SnapDBusSystemPolicyDir, "snapd.system-services.conf")), Equals, true) + c.Check(osutil.FileExists(filepath.Join(dirs.SnapDBusSessionPolicyDir, "snapd.session-services.conf")), Equals, false) + + c.Assert(logBuf.String(), testutil.Contains, "/etc/dbus-1/session.d appears to be read-only, could not write snapd dbus config files") +} + func (s *servicesTestSuite) TestAddSnapdServicesWithNonSnapd(c *C) { restore := release.MockOnClassic(false) defer restore() diff -Nru snapd-2.47.1+20.10.1build1/wrappers/export_test.go snapd-2.48+21.04/wrappers/export_test.go --- snapd-2.47.1+20.10.1build1/wrappers/export_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/wrappers/export_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -21,6 +21,8 @@ import ( "time" + + "github.com/snapcore/snapd/osutil" ) // some internal helper exposed for testing @@ -50,3 +52,11 @@ killWait = oldKillWait } } + +func MockEnsureDirState(f func(dir string, glob string, content map[string]osutil.FileState) (changed, removed []string, err error)) (restore func()) { + oldEnsureDirState := ensureDirState + ensureDirState = f + return func() { + ensureDirState = oldEnsureDirState + } +} diff -Nru snapd-2.47.1+20.10.1build1/wrappers/services.go snapd-2.48+21.04/wrappers/services.go --- snapd-2.47.1+20.10.1build1/wrappers/services.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/wrappers/services.go 2020-11-19 16:51:02.000000000 +0000 @@ -31,7 +31,6 @@ "text/template" "time" - "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/logger" "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/osutil/sys" @@ -144,8 +143,8 @@ var enabled []string var userEnabled []string - systemSysd := systemd.New(dirs.GlobalRootDir, systemd.SystemMode, inter) - userSysd := systemd.New(dirs.GlobalRootDir, systemd.GlobalUserMode, inter) + systemSysd := systemd.New(systemd.SystemMode, inter) + userSysd := systemd.New(systemd.GlobalUserMode, inter) disableEnabledServices := func() { for _, srvName := range enabled { @@ -204,8 +203,8 @@ // are services. Service units will be started in the order provided by the // caller. func StartServices(apps []*snap.AppInfo, disabledSvcs []string, flags *StartServicesFlags, inter interacter, tm timings.Measurer) (err error) { - systemSysd := systemd.New(dirs.GlobalRootDir, systemd.SystemMode, inter) - userSysd := systemd.New(dirs.GlobalRootDir, systemd.GlobalUserMode, inter) + systemSysd := systemd.New(systemd.SystemMode, inter) + userSysd := systemd.New(systemd.GlobalUserMode, inter) cli := client.New() systemServices := make([]string, 0, len(apps)) @@ -372,7 +371,8 @@ // TODO: remove once services get enabled on start and not when created. preseeding := opts.Preseeding - sysd := systemd.New(dirs.GlobalRootDir, systemd.SystemMode, inter) + // note, sysd is not used when preseeding + sysd := systemd.New(systemd.SystemMode, inter) var written []string var writtenSystem, writtenUser bool var disableEnabledServices func() @@ -467,6 +467,7 @@ toEnable = append(toEnable, app) } + // this is no-op when preseeding because of empty toEnable list disableEnabledServices, err = enableServices(toEnable, inter) if err != nil { return err @@ -492,7 +493,7 @@ // the first boot of a pre-seeded image with service files already in place but not enabled. // XXX: it should go away once services are fixed and enabled on start. func EnableSnapServices(s *snap.Info, inter interacter) (err error) { - sysd := systemd.New(dirs.GlobalRootDir, systemd.SystemMode, inter) + sysd := systemd.New(systemd.SystemMode, inter) for _, app := range s.Apps { if app.IsService() { svcName := app.ServiceName() @@ -512,7 +513,7 @@ // StopServices stops and optionally disables service units for the applications // from the snap which are services. func StopServices(apps []*snap.AppInfo, flags *StopServicesFlags, reason snap.ServiceStopReason, inter interacter, tm timings.Measurer) error { - sysd := systemd.New(dirs.GlobalRootDir, systemd.SystemMode, inter) + sysd := systemd.New(systemd.SystemMode, inter) if flags == nil { flags = &StopServicesFlags{} } @@ -567,7 +568,7 @@ // ServicesEnableState returns a map of service names from the given snap, // together with their enable/disable status. func ServicesEnableState(s *snap.Info, inter interacter) (map[string]bool, error) { - sysd := systemd.New(dirs.GlobalRootDir, systemd.SystemMode, inter) + sysd := systemd.New(systemd.SystemMode, inter) // loop over all services in the snap, querying systemd for the current // systemd state of the snaps @@ -596,8 +597,8 @@ if s.Type() == snap.TypeSnapd { return fmt.Errorf("internal error: removing explicit services for snapd snap is unexpected") } - systemSysd := systemd.New(dirs.GlobalRootDir, systemd.SystemMode, inter) - userSysd := systemd.New(dirs.GlobalRootDir, systemd.GlobalUserMode, inter) + systemSysd := systemd.New(systemd.SystemMode, inter) + userSysd := systemd.New(systemd.GlobalUserMode, inter) var removedSystem, removedUser bool for _, app := range s.Apps { @@ -1225,7 +1226,7 @@ // Restart or reload services; if reload flag is set then "systemctl reload-or-restart" is attempted. func RestartServices(svcs []*snap.AppInfo, flags *RestartServicesFlags, inter interacter, tm timings.Measurer) error { - sysd := systemd.New(dirs.GlobalRootDir, systemd.SystemMode, inter) + sysd := systemd.New(systemd.SystemMode, inter) for _, srv := range svcs { // they're *supposed* to be all services, but checking doesn't hurt diff -Nru snapd-2.47.1+20.10.1build1/wrappers/services_test.go snapd-2.48+21.04/wrappers/services_test.go --- snapd-2.47.1+20.10.1build1/wrappers/services_test.go 2020-10-08 07:30:44.000000000 +0000 +++ snapd-2.48+21.04/wrappers/services_test.go 2020-11-19 16:51:02.000000000 +0000 @@ -96,7 +96,7 @@ err := wrappers.AddSnapServices(info, nil, nil, progress.Null) c.Assert(err, IsNil) c.Check(s.sysdLog, DeepEquals, [][]string{ - {"--root", dirs.GlobalRootDir, "enable", filepath.Base(svcFile)}, + {"enable", filepath.Base(svcFile)}, {"daemon-reload"}, }) @@ -124,7 +124,7 @@ c.Assert(err, IsNil) c.Check(osutil.FileExists(svcFile), Equals, false) c.Assert(s.sysdLog, HasLen, 2) - c.Check(s.sysdLog[0], DeepEquals, []string{"--root", dirs.GlobalRootDir, "disable", filepath.Base(svcFile)}) + c.Check(s.sysdLog[0], DeepEquals, []string{"disable", filepath.Base(svcFile)}) c.Check(s.sysdLog[1], DeepEquals, []string{"daemon-reload"}) } @@ -139,7 +139,7 @@ err := wrappers.AddSnapServices(info, nil, nil, progress.Null) c.Assert(err, IsNil) c.Check(s.sysdLog, DeepEquals, [][]string{ - {"--user", "--global", "--root", dirs.GlobalRootDir, "enable", filepath.Base(svcFile)}, + {"--user", "--global", "enable", filepath.Base(svcFile)}, {"--user", "daemon-reload"}, }) @@ -164,7 +164,7 @@ c.Check(osutil.FileExists(svcFile), Equals, false) c.Assert(s.sysdLog, HasLen, 2) c.Check(s.sysdLog, DeepEquals, [][]string{ - {"--user", "--global", "--root", dirs.GlobalRootDir, "disable", filepath.Base(svcFile)}, + {"--user", "--global", "disable", filepath.Base(svcFile)}, {"--user", "daemon-reload"}, }) } @@ -342,12 +342,12 @@ // is non-deterministic, so manually check each call c.Assert(r.Calls(), HasLen, 2) for _, call := range r.Calls() { - c.Assert(call, HasLen, 5) - c.Assert(call[:4], DeepEquals, []string{"systemctl", "--root", s.tempdir, "is-enabled"}) - switch call[4] { + c.Assert(call, HasLen, 3) + c.Assert(call[:2], DeepEquals, []string{"systemctl", "is-enabled"}) + switch call[2] { case svc1File, svc2File: default: - c.Errorf("unknown service for systemctl call: %s", call[4]) + c.Errorf("unknown service for systemctl call: %s", call[2]) } } } @@ -389,7 +389,7 @@ c.Assert(err, ErrorMatches, ".*is-enabled snap.hello-snap.svc1.service\\] failed with exit status 1: whoops\n.*") c.Assert(r.Calls(), DeepEquals, [][]string{ - {"systemctl", "--root", s.tempdir, "is-enabled", svc1File}, + {"systemctl", "is-enabled", svc1File}, }) } @@ -441,7 +441,7 @@ // only svc2 should be enabled c.Assert(r.Calls(), DeepEquals, [][]string{ - {"systemctl", "--root", s.tempdir, "enable", "snap.hello-snap.svc2.service"}, + {"systemctl", "enable", "snap.hello-snap.svc2.service"}, {"systemctl", "daemon-reload"}, }) } @@ -495,7 +495,7 @@ // actually enabling them, so we just see a daemon-reload call and not any // enable calls c.Assert(r.Calls(), DeepEquals, [][]string{ - {"systemctl", "--root", s.tempdir, "enable", "snap.hello-snap.svc1.service"}, + {"systemctl", "enable", "snap.hello-snap.svc1.service"}, {"systemctl", "daemon-reload"}, }) } @@ -549,7 +549,7 @@ // actually enabling them, so we just see a daemon-reload call and not any // enable calls c.Assert(r.Calls(), DeepEquals, [][]string{ - {"systemctl", "--root", s.tempdir, "enable", "snap.hello-snap.svc1.service"}, + {"systemctl", "enable", "snap.hello-snap.svc1.service"}, {"systemctl", "daemon-reload"}, }) } @@ -633,7 +633,7 @@ c.Assert(err, IsNil) c.Assert(s.sysdLog, DeepEquals, [][]string{ - {"--root", s.tempdir, "is-enabled", filepath.Base(svcFile)}, + {"is-enabled", filepath.Base(svcFile)}, {"start", filepath.Base(svcFile)}, }) } @@ -650,7 +650,7 @@ c.Assert(err, IsNil) c.Assert(s.sysdLog, DeepEquals, [][]string{ - {"--user", "--global", "--root", s.tempdir, "is-enabled", filepath.Base(svcFile)}, + {"--user", "--global", "is-enabled", filepath.Base(svcFile)}, {"--user", "start", filepath.Base(svcFile)}, }) } @@ -663,7 +663,7 @@ c.Assert(err, IsNil) c.Assert(s.sysdLog, DeepEquals, [][]string{ - {"--root", s.tempdir, "enable", filepath.Base(svcFile)}, + {"enable", filepath.Base(svcFile)}, }) } @@ -698,7 +698,7 @@ c.Assert(err, IsNil) c.Assert(r.Calls(), DeepEquals, [][]string{ - {"systemctl", "--root", s.tempdir, "is-enabled", filepath.Base(svcFile)}, + {"systemctl", "is-enabled", filepath.Base(svcFile)}, }) } @@ -782,9 +782,9 @@ svcFiles, _ = filepath.Glob(filepath.Join(dirs.SnapServicesDir, "snap.hello-snap.*.service")) c.Check(svcFiles, HasLen, 0) c.Check(sysdLog, DeepEquals, [][]string{ - {"--root", dirs.GlobalRootDir, "enable", svc1Name}, - {"--root", dirs.GlobalRootDir, "enable", svc2Name}, // this one fails - {"--root", dirs.GlobalRootDir, "disable", svc1Name}, + {"enable", svc1Name}, + {"enable", svc2Name}, // this one fails + {"disable", svc1Name}, {"daemon-reload"}, }) } @@ -830,11 +830,11 @@ svcFiles, _ = filepath.Glob(filepath.Join(dirs.SnapServicesDir, "snap.hello-snap.*.service")) c.Check(svcFiles, HasLen, 0) c.Check(sysdLog, DeepEquals, [][]string{ - {"--root", dirs.GlobalRootDir, "enable", svc1Name}, - {"--root", dirs.GlobalRootDir, "enable", svc2Name}, + {"enable", svc1Name}, + {"enable", svc2Name}, {"daemon-reload"}, // this one fails - {"--root", dirs.GlobalRootDir, "disable", svc1Name}, - {"--root", dirs.GlobalRootDir, "disable", svc2Name}, + {"disable", svc1Name}, + {"disable", svc2Name}, {"daemon-reload"}, // so does this one :-) }) } @@ -902,9 +902,9 @@ svcFiles, _ = filepath.Glob(filepath.Join(dirs.SnapUserServicesDir, "snap.hello-snap.*.service")) c.Check(svcFiles, HasLen, 0) c.Check(sysdLog, DeepEquals, [][]string{ - {"--user", "--global", "--root", dirs.GlobalRootDir, "enable", svc1Name}, - {"--user", "--global", "--root", dirs.GlobalRootDir, "enable", svc2Name}, // this one fails - {"--user", "--global", "--root", dirs.GlobalRootDir, "disable", svc1Name}, + {"--user", "--global", "enable", svc1Name}, + {"--user", "--global", "enable", svc2Name}, // this one fails + {"--user", "--global", "disable", svc1Name}, {"--user", "daemon-reload"}, }) } @@ -955,11 +955,11 @@ svcFiles, _ = filepath.Glob(filepath.Join(dirs.SnapUserServicesDir, "snap.hello-snap.*.service")) c.Check(svcFiles, HasLen, 0) c.Check(sysdLog, DeepEquals, [][]string{ - {"--user", "--global", "--root", dirs.GlobalRootDir, "enable", svc1Name}, - {"--user", "--global", "--root", dirs.GlobalRootDir, "enable", svc2Name}, + {"--user", "--global", "enable", svc1Name}, + {"--user", "--global", "enable", svc2Name}, {"--user", "daemon-reload"}, // this one fails - {"--user", "--global", "--root", dirs.GlobalRootDir, "disable", svc1Name}, - {"--user", "--global", "--root", dirs.GlobalRootDir, "disable", svc2Name}, + {"--user", "--global", "disable", svc1Name}, + {"--user", "--global", "disable", svc2Name}, {"--user", "daemon-reload"}, // so does this one :-) }) } @@ -1097,8 +1097,8 @@ c.Assert(err, ErrorMatches, "failed") c.Assert(sysdLog, HasLen, 8, Commentf("len: %v calls: %v", len(sysdLog), sysdLog)) c.Check(sysdLog, DeepEquals, [][]string{ - {"--root", s.tempdir, "is-enabled", svc1Name}, - {"--root", s.tempdir, "is-enabled", svc2Name}, + {"is-enabled", svc1Name}, + {"is-enabled", svc2Name}, {"start", svc1Name}, {"start", svc2Name}, // one of the services fails {"stop", svc2Name}, @@ -1152,21 +1152,21 @@ c.Logf("sysdlog: %v", sysdLog) c.Assert(sysdLog, HasLen, 17, Commentf("len: %v calls: %v", len(sysdLog), sysdLog)) c.Check(sysdLog, DeepEquals, [][]string{ - {"--root", s.tempdir, "is-enabled", svc1Name}, - {"--root", s.tempdir, "enable", svc2SocketName}, + {"is-enabled", svc1Name}, + {"enable", svc2SocketName}, {"start", svc2SocketName}, - {"--root", s.tempdir, "enable", svc3SocketName}, + {"enable", svc3SocketName}, {"start", svc3SocketName}, // start failed, what follows is the cleanup {"stop", svc3SocketName}, {"show", "--property=ActiveState", svc3SocketName}, {"stop", svc3Name}, {"show", "--property=ActiveState", svc3Name}, - {"--root", s.tempdir, "disable", svc3SocketName}, + {"disable", svc3SocketName}, {"stop", svc2SocketName}, {"show", "--property=ActiveState", svc2SocketName}, {"stop", svc2Name}, {"show", "--property=ActiveState", svc2Name}, - {"--root", s.tempdir, "disable", svc2SocketName}, + {"disable", svc2SocketName}, {"stop", svc1Name}, {"show", "--property=ActiveState", svc1Name}, }, Commentf("calls: %v", sysdLog)) @@ -1209,8 +1209,8 @@ c.Assert(err, ErrorMatches, "some user services failed to start") c.Assert(sysdLog, HasLen, 10, Commentf("len: %v calls: %v", len(sysdLog), sysdLog)) c.Check(sysdLog, DeepEquals, [][]string{ - {"--user", "--global", "--root", s.tempdir, "is-enabled", svc1Name}, - {"--user", "--global", "--root", s.tempdir, "is-enabled", svc2Name}, + {"--user", "--global", "is-enabled", svc1Name}, + {"--user", "--global", "is-enabled", svc2Name}, {"--user", "start", svc1Name}, {"--user", "start", svc2Name}, // one of the services fails // session agent attempts to stop the non-failed services @@ -1259,9 +1259,9 @@ c.Assert(err, IsNil) c.Assert(sysdLog, HasLen, 6, Commentf("len: %v calls: %v", len(sysdLog), sysdLog)) c.Check(sysdLog, DeepEquals, [][]string{ - {"--root", s.tempdir, "is-enabled", svc1Name}, - {"--root", s.tempdir, "is-enabled", svc3Name}, - {"--root", s.tempdir, "is-enabled", svc2Name}, + {"is-enabled", svc1Name}, + {"is-enabled", svc3Name}, + {"is-enabled", svc2Name}, {"start", svc1Name}, {"start", svc3Name}, {"start", svc2Name}, @@ -1275,9 +1275,9 @@ c.Assert(err, IsNil) c.Assert(sysdLog, HasLen, 12, Commentf("len: %v calls: %v", len(sysdLog), sysdLog)) c.Check(sysdLog[6:], DeepEquals, [][]string{ - {"--root", s.tempdir, "is-enabled", svc3Name}, - {"--root", s.tempdir, "is-enabled", svc1Name}, - {"--root", s.tempdir, "is-enabled", svc2Name}, + {"is-enabled", svc3Name}, + {"is-enabled", svc1Name}, + {"is-enabled", svc2Name}, {"start", svc3Name}, {"start", svc1Name}, {"start", svc2Name}, @@ -1401,7 +1401,7 @@ err := wrappers.AddSnapServices(info, nil, nil, progress.Null) c.Assert(err, IsNil) c.Check(s.sysdLog, DeepEquals, [][]string{ - {"--root", dirs.GlobalRootDir, "enable", filepath.Base(survivorFile)}, + {"enable", filepath.Base(survivorFile)}, {"daemon-reload"}, }) @@ -1453,7 +1453,7 @@ err := wrappers.AddSnapServices(info, nil, nil, progress.Null) c.Assert(err, IsNil) c.Check(s.sysdLog, DeepEquals, [][]string{ - {"--root", dirs.GlobalRootDir, "enable", filepath.Base(survivorFile)}, + {"enable", filepath.Base(survivorFile)}, {"daemon-reload"}, }) @@ -1516,10 +1516,10 @@ c.Assert(err, IsNil) c.Assert(s.sysdLog, HasLen, 6, Commentf("len: %v calls: %v", len(s.sysdLog), s.sysdLog)) c.Check(s.sysdLog, DeepEquals, [][]string{ - {"--root", dirs.GlobalRootDir, "is-enabled", svc1Name}, - {"--root", dirs.GlobalRootDir, "enable", svc2Sock}, + {"is-enabled", svc1Name}, + {"enable", svc2Sock}, {"start", svc2Sock}, - {"--user", "--global", "--root", dirs.GlobalRootDir, "enable", svc3Sock}, + {"--user", "--global", "enable", svc3Sock}, {"--user", "start", svc3Sock}, {"start", svc1Name}, }, Commentf("calls: %v", s.sysdLog)) @@ -1549,10 +1549,10 @@ c.Assert(err, IsNil) c.Assert(s.sysdLog, HasLen, 6, Commentf("len: %v calls: %v", len(s.sysdLog), s.sysdLog)) c.Check(s.sysdLog, DeepEquals, [][]string{ - {"--root", dirs.GlobalRootDir, "is-enabled", svc1Name}, - {"--root", dirs.GlobalRootDir, "enable", svc2Timer}, + {"is-enabled", svc1Name}, + {"enable", svc2Timer}, {"start", svc2Timer}, - {"--user", "--global", "--root", dirs.GlobalRootDir, "enable", svc3Timer}, + {"--user", "--global", "enable", svc3Timer}, {"--user", "start", svc3Timer}, {"start", svc1Name}, }, Commentf("calls: %v", s.sysdLog)) @@ -1586,14 +1586,14 @@ c.Assert(err, ErrorMatches, "failed") c.Assert(sysdLog, HasLen, 10, Commentf("len: %v calls: %v", len(sysdLog), sysdLog)) c.Check(sysdLog, DeepEquals, [][]string{ - {"--root", dirs.GlobalRootDir, "is-enabled", svc1Name}, - {"--root", dirs.GlobalRootDir, "enable", svc2Timer}, + {"is-enabled", svc1Name}, + {"enable", svc2Timer}, {"start", svc2Timer}, // this call fails {"stop", svc2Timer}, {"show", "--property=ActiveState", svc2Timer}, {"stop", svc2Name}, {"show", "--property=ActiveState", svc2Name}, - {"--root", dirs.GlobalRootDir, "disable", svc2Timer}, + {"disable", svc2Timer}, {"stop", svc1Name}, {"show", "--property=ActiveState", svc1Name}, }, Commentf("calls: %v", sysdLog)) @@ -1742,7 +1742,7 @@ c.Assert(s.sysdLog, HasLen, 2, Commentf("len: %v calls: %v", len(s.sysdLog), s.sysdLog)) c.Check(s.sysdLog, DeepEquals, [][]string{ // only svc3 gets started during boot - {"--root", dirs.GlobalRootDir, "enable", svc3Name}, + {"enable", svc3Name}, {"daemon-reload"}, }, Commentf("calls: %v", s.sysdLog)) } @@ -1836,6 +1836,6 @@ c.Check(s.sysdLog, DeepEquals, [][]string{ {"stop", svcFile}, {"show", "--property=ActiveState", svcFile}, - {"--root", s.tempdir, "disable", svcFile}, + {"disable", svcFile}, }) }