osbase/efivarfs: implement OsIndications

This allows easy interfacing with EFI's OsIndications mechanism.

https: //uefi.org/specs/UEFI/2.10/08_Services_Runtime_Services.html#exchanging-information-between-the-os-and-firmware
Change-Id: I6187a9a002ac06a82138ea10676641e3eb00c7a5
Reviewed-on: https://review.monogon.dev/c/monogon/+/3388
Tested-by: Jenkins CI
Reviewed-by: Jan Schär <jan@monogon.tech>
diff --git a/osbase/efivarfs/osindications.go b/osbase/efivarfs/osindications.go
new file mode 100644
index 0000000..a112163
--- /dev/null
+++ b/osbase/efivarfs/osindications.go
@@ -0,0 +1,94 @@
+package efivarfs
+
+import (
+	"encoding/binary"
+	"errors"
+	"fmt"
+	"os"
+	"sync"
+)
+
+// OSIndications is a bitset used to indicate firmware support for various
+// features as well as to trigger some of these features.
+// If a constant ends in Supported, it cannot be triggered, the others
+// can be.
+type OSIndications uint64
+
+const (
+	// Indicates that on next boot firmware should boot to a firmware-provided
+	// UI instead of the normal boot order.
+	BootToFirmwareUI = OSIndications(1 << iota)
+	// Indicates that firmware supports timestamp-based revocation and the
+	// "dbt" authorized timestamp database variable.
+	TimestampRevocationSupported
+	// Indicates that on next boot firmware should look for an EFI update
+	// capsule on an EFI system partition and try to install it.
+	FileCapsuleDelivery
+	// Indicates that firmware supports UEFI FMP update capsules.
+	FirmwareManagementProtocolCapsuleSupported
+	// Indicates that firmware supports reporting results of deferred (i.e.
+	// processed on next boot) capsule installs via variables.
+	CapsuleResultVarSupported
+	// Indicates that firmware should skip Boot# processing on next boot
+	// and instead use OsRecovery# for selecting a load option.
+	StartOSRecovery
+	// Indicates that firmware should skip Boot# processing on next boot
+	// and instead use PlatformRecovery# for selecting a load option.
+	StartPlatformRecovery
+	// Indicates that firmware should collect the current config and report
+	// the data to the EFI system configuration table on next boot.
+	JSONConfigDataRefresh
+)
+
+// osIndicationMutex protects against race conditions in read-modify-write
+// sequences on the OsIndications EFI variable.
+var osIndicationsMutex sync.Mutex
+
+// OSIndicationsSupported indicates which of the OS indication features and
+// actions that the firmware supports.
+func OSIndicationsSupported() (OSIndications, error) {
+	osIndicationsRaw, _, err := Read(ScopeGlobal, "OsIndicationsSupported")
+	if err != nil {
+		return 0, fmt.Errorf("unable to read OsIndicationsSupported: %w", err)
+	}
+	if len(osIndicationsRaw) != 8 {
+		return 0, fmt.Errorf("value of OsIndicationsSupported is not 8 bytes / 64 bits, is %d bytes", len(osIndicationsRaw))
+	}
+	return OSIndications(binary.LittleEndian.Uint64(osIndicationsRaw)), nil
+}
+
+// SetOSIndications sets all OS indication bits set in i in firmware. It does
+// not clear any already-set bits, use ClearOSIndications for that.
+func SetOSIndications(i OSIndications) error {
+	return modifyOSIndications(func(prev OSIndications) OSIndications {
+		return prev | i
+	})
+}
+
+// ClearOSIndications clears all OS indication bits set in i in firmware.
+// Note that this effectively inverts i, bits set in i will be cleared.
+func ClearOSIndications(i OSIndications) error {
+	return modifyOSIndications(func(prev OSIndications) OSIndications {
+		return prev & ^i
+	})
+}
+
+func modifyOSIndications(f func(prev OSIndications) OSIndications) error {
+	osIndicationsMutex.Lock()
+	defer osIndicationsMutex.Unlock()
+
+	var osIndications OSIndications
+	rawIn, _, err := Read(ScopeGlobal, "OsIndications")
+	if err == nil && len(rawIn) == 8 {
+		osIndications = OSIndications(binary.LittleEndian.Uint64(rawIn))
+	} else if err != nil && !errors.Is(err, os.ErrNotExist) {
+		return fmt.Errorf("unable to read OsIndications variable: %w", err)
+	}
+	osIndications = f(osIndications)
+	var raw [8]byte
+	binary.LittleEndian.PutUint64(raw[:], uint64(osIndications))
+	if err := Write(ScopeGlobal, "OsIndications", AttrNonVolatile|AttrRuntimeAccess, raw[:]); err != nil {
+		return fmt.Errorf("failed to write OSIndications variable: %w", err)
+	}
+	return nil
+}