diff --git a/rauc/custom.go b/rauc/custom.go index 6f1aa355908c58395ddd5b45ff3eb0feb062f211..cf061b1076e0a9fab4a5ee8e9279ae5da4b92e37 100644 --- a/rauc/custom.go +++ b/rauc/custom.go @@ -10,6 +10,7 @@ import ( "os" "git.ostc-eu.org/OSTC/OHOS/components/sysota/boot" + "git.ostc-eu.org/OSTC/OHOS/components/sysota/ota" ) // CustomBootBackend is an interface between boot loader and RAUC. @@ -161,3 +162,197 @@ func runSetSlotStateCmd(backend CustomBootBackend, args []string) error { return ErrUnexpectedArgument } } + +// ErrProtocolViolation records unexpected usage of RAUC custom boot backend hook. +// +// This can happen if the logic in SystemOTA is flawed or if RAUC changes +// radically, extending the protocol in a way that requires updates to +// SystemOTA. +var ErrProtocolViolation = errors.New("violation of protocol between SystemOTA and RAUC") + +// BootProtocolAdapter uses SystemOTA boot.Protocol and ota.SystemState to implement CustomBootBackend. +// +// TODO(zyga): this adapter does not yet use boot.Protocol.Reboot, which is required +// for correct operation. This will change once SystemOTA can be used as a RAUC +// install hook. +type BootProtocolAdapter struct { + proto boot.Protocol + state *ota.SystemState +} + +// NewBootProtocolAdapter returns a RAUC CustomBootBackend implemented boot.Protocol and boot.Mode. +// +// RAUC and SystemOTA differ in how the boot loader is handled. RAUC uses a +// stateful protocol, where each slot has a persistent state and there is a +// persistent primary slot used for booting. SystemOTA uses a transactional +// interface and has only one element of state that is not captured in the boot +// loader itself, namely boot.Mode, where the system may be expected to normally +// or in the one-off try mode. +// +func NewBootProtocolAdapter(proto boot.Protocol, state *ota.SystemState) *BootProtocolAdapter { + return &BootProtocolAdapter{proto: proto, state: state} +} + +// PrimarySlot returns the name of the RAUC primary boot slot. +// +// SystemOTA does not model the primary slot directly. The system boot.Mode +// state is used to pick either active slot (in normal mode) or inactive slot +// (in try mode). +func (adapter *BootProtocolAdapter) PrimarySlot() (boot.Slot, error) { + switch adapter.state.BootMode { + case boot.Normal: + return adapter.proto.QueryActive() + case boot.Try: + return adapter.proto.QueryInactive() + default: + return boot.InvalidSlot, boot.ErrInvalidBootMode + } +} + +// SetPrimarySlot sets the name of the RAUC primary boot slot. +// +// SystemOTA does not model the primary slot directly. When in normal boot mode, +// attempt to set the inactive boot slot as primary commences an update +// transaction. This transaction is finished with a call to SetSlotState, which +// either commits or rolls back the transaction. +// +// In case of invalid input the error is boot.ErrInvalidSlot, Other errors are +// those from the concrete implementation of boot.Protocol or +// ErrProtocolViolation. +func (adapter *BootProtocolAdapter) SetPrimarySlot(slot boot.Slot) error { + if slot != boot.SlotA && slot != boot.SlotB { + return boot.ErrInvalidSlot + } + + active, err := adapter.proto.QueryActive() + if err != nil { + return err + } + + inactive := boot.SynthesizeInactiveSlot(active) + + switch { + case adapter.state.BootMode == boot.Normal && slot == active: + // Affirmation of an expected, synthesized state. + return nil + case adapter.state.BootMode == boot.Normal && slot == inactive: + // Begin update transaction. + if err := adapter.proto.TrySwitch(slot); err != nil { + return err + } + + adapter.state.BootMode = boot.Try + + return nil + case adapter.state.BootMode == boot.Try && slot == inactive: + // Commmit of an update transaction. + if err := adapter.proto.CommitSwitch(); err != nil { + return err + } + + adapter.state.BootMode = boot.Normal + + return nil + case adapter.state.BootMode == boot.Try && slot == inactive: + // Affirmation of an expected, synthesized state. + return nil + default: + return ErrProtocolViolation + } +} + +// SlotState returns the good/bad state of a given slot. +// +// SystemOTA does not model the state of each slot directly. It is assumed that the +// active slot is always good and that the inactive slot is always bad. Note +// that boot.Protocol.TrySwitch does not modify the active slot, so the system +// does attempt to boot into a known "bad" slot, this is intentional and matches +// what RAUC expects. +func (adapter *BootProtocolAdapter) SlotState(slot boot.Slot) (boot.SlotState, error) { + if slot != boot.SlotA && slot != boot.SlotB { + return boot.InvalidSlotState, boot.ErrInvalidSlot + } + + active, err := adapter.proto.QueryActive() + if err != nil { + return boot.InvalidSlotState, err + } + + if slot == active { + return boot.GoodSlot, nil + } + + return boot.BadSlot, nil +} + +// SetSlotState sets the good/bad state of a given slot. +// +// SystemOTA does not model the state of each slot directly. The only state is +// boot.Mode - either normal boot or try-boot and whatever the bootloader +// considers to be the active slot. +// +// SetSlotState is expected to be called to initiate and finalize a bundle +// install operation, or in other words a system update. +// +// When SetSlotState is called to initialize the update operation, the inactive +// slot is marked as bad and then set as the primary boot slot. SystemOTA +// recognizes this, verifies that the state is set to bad and does not do +// anything more. +// +// When SetSlotState is called called to finalize a successful update operation +// the state of the primary slot is changed to good. Note that at this stage +// boot.Mode is Try and that the primary slot is still the inactive slot. +// +// When SetSlotState is called to finalize a failed or aborted update operation +// the only difference from the previous situation is that the state is bad +// instead of good. +// +// In case of invalid input the error is boot.ErrInvalidSlot, +// boot.ErrInvalidSlotState. Other errors are those from the concrete +// implementation of boot.Protocol or ErrProtocolViolation. +func (adapter *BootProtocolAdapter) SetSlotState(slot boot.Slot, state boot.SlotState) error { + if slot != boot.SlotA && slot != boot.SlotB { + return boot.ErrInvalidSlot + } + + if state != boot.GoodSlot && state != boot.BadSlot { + return boot.ErrInvalidSlotState + } + + active, err := adapter.proto.QueryActive() + if err != nil { + return err + } + + inactive := boot.SynthesizeInactiveSlot(active) + + switch { + case adapter.state.BootMode == boot.Normal && slot == active && state == boot.GoodSlot: + // Affirmation of an expected, synthesized state. + return nil + case adapter.state.BootMode == boot.Normal && slot == inactive && state == boot.BadSlot: + // Affirmation of an expected, synthesized state. + return nil + case adapter.state.BootMode == boot.Try && slot == inactive && state == boot.GoodSlot: + // Commmit of an update transaction. + if err := adapter.proto.CommitSwitch(); err != nil { + return err + } + + adapter.state.BootMode = boot.Normal + + return nil + case adapter.state.BootMode == boot.Try && slot == inactive && state == boot.BadSlot: + // Rollback of an update transaction. + // Note that we set boot mode even if CancelSwitch failed because by + // this time, we have rolled back already. + adapter.state.BootMode = boot.Normal + if err := adapter.proto.CancelSwitch(); err != nil { + return err + } + + return nil + default: + return ErrProtocolViolation + } +}