m/p/efivarfs: allow multiple paths in LoadOption

The FilePathList in an EFI LOAD_OPTION technically allows multiple
device paths separated by end of path markers. These additional paths
beyond the first one do not have any standardized function, but some
EFI vendors use them to store data. Currently we fail to parse them
because they contain more than one end of path marker. Fix this by
adding an ExtraPaths field to LoadOption and implementing marshalling/
unmarshalling for it. I chose to use a separate field in the Go
struct because these additional paths do not have the same functionality
as the first one (they are not bootable).

Change-Id: I96df17915966a973a7553fe9864a0b3e6f6b61e8
Reviewed-on: https://review.monogon.dev/c/monogon/+/2037
Reviewed-by: Serge Bazanski <serge@monogon.tech>
Reviewed-by: Tim Windelschmidt <tim@monogon.tech>
Tested-by: Jenkins CI
diff --git a/metropolis/pkg/efivarfs/boot.go b/metropolis/pkg/efivarfs/boot.go
index 71324fe..8fe1a55 100644
--- a/metropolis/pkg/efivarfs/boot.go
+++ b/metropolis/pkg/efivarfs/boot.go
@@ -64,6 +64,9 @@
 	// Path to the UEFI PE executable to execute when this load option is being
 	// loaded.
 	FilePath DevicePath
+	// ExtraPaths contains additional device paths with vendor-specific
+	// behavior. Can generally be left empty.
+	ExtraPaths []DevicePath
 	// OptionalData gets passed as an argument to the executed PE executable.
 	// If zero-length a NULL value is passed to the executable.
 	OptionalData []byte
@@ -85,8 +88,15 @@
 	if err != nil {
 		return nil, fmt.Errorf("failed marshalling FilePath: %w", err)
 	}
+	for _, ep := range e.ExtraPaths {
+		epRaw, err := ep.Marshal()
+		if err != nil {
+			return nil, fmt.Errorf("failed marshalling ExtraPath: %w", err)
+		}
+		filePathRaw = append(filePathRaw, epRaw...)
+	}
 	if len(filePathRaw) > math.MaxUint16 {
-		return nil, fmt.Errorf("failed marshalling FilePath: value too big (%d)", len(filePathRaw))
+		return nil, fmt.Errorf("failed marshalling FilePath/ExtraPath: value too big (%d)", len(filePathRaw))
 	}
 	data = append16(data, uint16(len(filePathRaw)))
 	if strings.IndexByte(e.Description, 0x00) != -1 {
@@ -131,10 +141,19 @@
 		return nil, fmt.Errorf("declared length of FilePath (%d) overruns available data (%d)", lenPath, len(data)-descriptionEnd)
 	}
 	filePathData := data[descriptionEnd : descriptionEnd+int(lenPath)]
-	opt.FilePath, err = UnmarshalDevicePath(filePathData)
+	opt.FilePath, filePathData, err = UnmarshalDevicePath(filePathData)
 	if err != nil {
 		return nil, fmt.Errorf("failed unmarshaling FilePath: %w", err)
 	}
+	for len(filePathData) > 0 {
+		var extraPath DevicePath
+		extraPath, filePathData, err = UnmarshalDevicePath(filePathData)
+		if err != nil {
+			return nil, fmt.Errorf("failed unmarshaling ExtraPath: %w", err)
+		}
+		opt.ExtraPaths = append(opt.ExtraPaths, extraPath)
+	}
+
 	if descriptionEnd+int(lenPath) < len(data) {
 		opt.OptionalData = data[descriptionEnd+int(lenPath):]
 	}
diff --git a/metropolis/pkg/efivarfs/devicepath.go b/metropolis/pkg/efivarfs/devicepath.go
index 1606fd6..8e9dddb 100644
--- a/metropolis/pkg/efivarfs/devicepath.go
+++ b/metropolis/pkg/efivarfs/devicepath.go
@@ -264,14 +264,16 @@
 	return buf, nil
 }
 
-// UnmarshalDevicePath parses a binary device path.
-func UnmarshalDevicePath(data []byte) (DevicePath, error) {
+// UnmarshalDevicePath parses a binary device path until it encounters an end
+// device path structure. It returns that device path (excluding the final end
+// device path marker) as well as all all data following the end marker.
+func UnmarshalDevicePath(data []byte) (DevicePath, []byte, error) {
 	rest := data
 	var p DevicePath
 	for {
 		if len(rest) < 4 {
 			if len(rest) != 0 {
-				return nil, fmt.Errorf("dangling bytes at the end of device path: %x", rest)
+				return nil, nil, fmt.Errorf("dangling bytes at the end of device path: %x", rest)
 			}
 			break
 		}
@@ -279,14 +281,19 @@
 		subT := rest[1]
 		dataLen := binary.LittleEndian.Uint16(rest[2:4])
 		if int(dataLen) > len(rest) {
-			return nil, fmt.Errorf("path element larger than rest of buffer: %d > %d", dataLen, len(rest))
+			return nil, nil, fmt.Errorf("path element larger than rest of buffer: %d > %d", dataLen, len(rest))
 		}
 		if dataLen < 4 {
-			return nil, fmt.Errorf("path element must be at least 4 bytes (header), length indicates %d", dataLen)
+			return nil, nil, fmt.Errorf("path element must be at least 4 bytes (header), length indicates %d", dataLen)
 		}
 		elemData := rest[4:dataLen]
 		rest = rest[dataLen:]
 
+		// End of Device Path
+		if t == 0x7f && subT == 0xff {
+			return p, rest, nil
+		}
+
 		unmarshal, ok := pathElementUnmarshalMap[[2]byte{t, subT}]
 		if !ok {
 			p = append(p, &UnknownPath{
@@ -298,26 +305,12 @@
 		}
 		elem, err := unmarshal(elemData)
 		if err != nil {
-			return nil, fmt.Errorf("failed decoding path element %d: %w", len(p), err)
+			return nil, nil, fmt.Errorf("failed decoding path element %d: %w", len(p), err)
 		}
 		p = append(p, elem)
 	}
-	var endOfPathIdx int
-	for i, e := range p {
-		if e.typ() == 0x7f && e.subType() == 0xff {
-			endOfPathIdx = i
-			break
-		}
+	if len(p) == 0 {
+		return nil, nil, errors.New("empty DevicePath without End Of Path element")
 	}
-	switch {
-	case len(p) == 0:
-		return nil, errors.New("empty DevicePath without End Of Path element")
-	case endOfPathIdx == -1:
-		return nil, fmt.Errorf("got DevicePath with %d elements, but without End Of Path element", len(p))
-	case endOfPathIdx != len(p)-1:
-		return nil, fmt.Errorf("got DevicePath with %d elements with End Of Path element at %d (wanted as last element)", len(p), endOfPathIdx)
-	}
-	p = p[:len(p)-1]
-
-	return p, nil
+	return nil, nil, fmt.Errorf("got DevicePath with %d elements, but without End Of Path element", len(p))
 }
diff --git a/metropolis/pkg/efivarfs/devicepath_test.go b/metropolis/pkg/efivarfs/devicepath_test.go
index b5823ac..ad81279 100644
--- a/metropolis/pkg/efivarfs/devicepath_test.go
+++ b/metropolis/pkg/efivarfs/devicepath_test.go
@@ -81,9 +81,13 @@
 			if !bytes.Equal(got, c.expected) {
 				t.Fatalf("expected %x, got %x", c.expected, got)
 			}
-			if _, err := UnmarshalDevicePath(got); err != nil {
+			_, rest, err := UnmarshalDevicePath(got)
+			if err != nil {
 				t.Errorf("failed to unmarshal value again: %v", err)
 			}
+			if len(rest) != 0 {
+				t.Errorf("rest is non-zero after single valid device path: %x", rest)
+			}
 		})
 	}
 }