m/p/devicemapper: make parameter encoding part of package

The DM kernel interface gets a single parameter string for each DM
target in a table but internally the kernel immediately decodes it into
an argv-style list of string arguments. Because everything needs to do
it and it can be quite hard to get right, let's make it part of the
devicemapper package. Properly encoding this also means you get
actionable errors when you pass invalid data instead of weird kernel
errors or misbehavior.

Change-Id: I8060871a7459183c0395e5e4e8aac517544b2e87
Reviewed-on: https://review.monogon.dev/c/monogon/+/309
Reviewed-by: Sergiusz Bazanski <serge@monogon.tech>
diff --git a/metropolis/node/core/localstorage/crypt/crypt.go b/metropolis/node/core/localstorage/crypt/crypt.go
index d6ed541..613ebea 100644
--- a/metropolis/node/core/localstorage/crypt/crypt.go
+++ b/metropolis/node/core/localstorage/crypt/crypt.go
@@ -60,7 +60,7 @@
 		devicemapper.Target{
 			Length:     integritySectors,
 			Type:       "integrity",
-			Parameters: fmt.Sprintf("%v 0 28 J 1 journal_sectors:1024", baseName),
+			Parameters: []string{baseName, "0", "28", "J", "1", "journal_sectors:1024"},
 		},
 	})
 	if err != nil {
@@ -77,7 +77,7 @@
 		devicemapper.Target{
 			Length:     integritySectors,
 			Type:       "crypt",
-			Parameters: fmt.Sprintf("capi:gcm(aes)-random %v 0 %v 0 1 integrity:28:aead", hex.EncodeToString(encryptionKey), integrityDevName),
+			Parameters: []string{"capi:gcm(aes)-random", hex.EncodeToString(encryptionKey), "0", integrityDevName, "0", "1", "integrity:28:aead"},
 		},
 	})
 	if err != nil {
@@ -115,7 +115,7 @@
 		{
 			Length:     1,
 			Type:       "integrity",
-			Parameters: fmt.Sprintf("%v 0 28 J 1 journal_sectors:1024", baseName),
+			Parameters: []string{baseName, "0", "28", "J", "1", "journal_sectors:1024"},
 		},
 	})
 	if err != nil {
diff --git a/metropolis/pkg/devicemapper/BUILD.bazel b/metropolis/pkg/devicemapper/BUILD.bazel
index 9e66a7e..e0dca24 100644
--- a/metropolis/pkg/devicemapper/BUILD.bazel
+++ b/metropolis/pkg/devicemapper/BUILD.bazel
@@ -2,7 +2,10 @@
 
 go_library(
     name = "go_default_library",
-    srcs = ["devicemapper.go"],
+    srcs = [
+        "ctype.go",
+        "devicemapper.go",
+    ],
     importpath = "source.monogon.dev/metropolis/pkg/devicemapper",
     visibility = ["//metropolis:__subpackages__"],
     deps = [
diff --git a/metropolis/pkg/devicemapper/ctype.go b/metropolis/pkg/devicemapper/ctype.go
new file mode 100644
index 0000000..05e6934
--- /dev/null
+++ b/metropolis/pkg/devicemapper/ctype.go
@@ -0,0 +1,40 @@
+package devicemapper
+
+// Linux kernel ctype data from @linux//include/linux:ctype.h
+
+const (
+	_U  = 0x01 /* upper */
+	_L  = 0x02 /* lower */
+	_D  = 0x04 /* digit */
+	_C  = 0x08 /* cntrl */
+	_P  = 0x10 /* punct */
+	_S  = 0x20 /* white space (space/lf/tab) */
+	_X  = 0x40 /* hex digit */
+	_SP = 0x80 /* hard space (0x20) */
+)
+
+var ctypeLookup = [256]byte{
+	_C, _C, _C, _C, _C, _C, _C, _C, /* 0-7 */
+	_C, _C | _S, _C | _S, _C | _S, _C | _S, _C | _S, _C, _C, /* 8-15 */
+	_C, _C, _C, _C, _C, _C, _C, _C, /* 16-23 */
+	_C, _C, _C, _C, _C, _C, _C, _C, /* 24-31 */
+	_S | _SP, _P, _P, _P, _P, _P, _P, _P, /* 32-39 */
+	_P, _P, _P, _P, _P, _P, _P, _P, /* 40-47 */
+	_D, _D, _D, _D, _D, _D, _D, _D, /* 48-55 */
+	_D, _D, _P, _P, _P, _P, _P, _P, /* 56-63 */
+	_P, _U | _X, _U | _X, _U | _X, _U | _X, _U | _X, _U | _X, _U, /* 64-71 */
+	_U, _U, _U, _U, _U, _U, _U, _U, /* 72-79 */
+	_U, _U, _U, _U, _U, _U, _U, _U, /* 80-87 */
+	_U, _U, _U, _P, _P, _P, _P, _P, /* 88-95 */
+	_P, _L | _X, _L | _X, _L | _X, _L | _X, _L | _X, _L | _X, _L, /* 96-103 */
+	_L, _L, _L, _L, _L, _L, _L, _L, /* 104-111 */
+	_L, _L, _L, _L, _L, _L, _L, _L, /* 112-119 */
+	_L, _L, _L, _P, _P, _P, _P, _C, /* 120-127 */
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 128-143 */
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 144-159 */
+	_S | _SP, _P, _P, _P, _P, _P, _P, _P, _P, _P, _P, _P, _P, _P, _P, _P, /* 160-175 */
+	_P, _P, _P, _P, _P, _P, _P, _P, _P, _P, _P, _P, _P, _P, _P, _P, /* 176-191 */
+	_U, _U, _U, _U, _U, _U, _U, _U, _U, _U, _U, _U, _U, _U, _U, _U, /* 192-207 */
+	_U, _U, _U, _U, _U, _U, _U, _P, _U, _U, _U, _U, _U, _U, _U, _L, /* 208-223 */
+	_L, _L, _L, _L, _L, _L, _L, _L, _L, _L, _L, _L, _L, _L, _L, _L, /* 224-239 */
+	_L, _L, _L, _L, _L, _L, _L, _P, _L, _L, _L, _L, _L, _L, _L, _L} /* 240-255 */
diff --git a/metropolis/pkg/devicemapper/devicemapper.go b/metropolis/pkg/devicemapper/devicemapper.go
index 5807676..9b560e6 100644
--- a/metropolis/pkg/devicemapper/devicemapper.go
+++ b/metropolis/pkg/devicemapper/devicemapper.go
@@ -24,6 +24,7 @@
 	"fmt"
 	"os"
 	"runtime"
+	"strings"
 	"unsafe"
 
 	"github.com/pkg/errors"
@@ -137,6 +138,35 @@
 	return nil
 }
 
+// marshalParams marshals a list of strings into a single string according to
+// the rules in the kernel-side decoder. Strings with null bytes or only
+// whitespace characters cannot be encoded and will return an errors.
+func marshalParams(params []string) (string, error) {
+	var strb strings.Builder
+	for _, param := range params {
+		var hasNonWhitespace bool
+		for i := 0; i < len(param); i++ {
+			b := param[i]
+			if b == 0x00 {
+				return "", errors.New("parameter with null bytes cannot be encoded")
+			}
+			isWhitespace := ctypeLookup[b]&_S != 0
+			if !isWhitespace {
+				hasNonWhitespace = true
+			}
+			if isWhitespace || b == '\\' {
+				strb.WriteByte('\\')
+			}
+			strb.WriteByte(b)
+		}
+		if !hasNonWhitespace {
+			return "", errors.New("parameter with only whitespace cannot be encoded")
+		}
+		strb.WriteByte(' ')
+	}
+	return strb.String(), nil
+}
+
 var fd uintptr
 
 func getFd() (uintptr, error) {
@@ -217,7 +247,9 @@
 	// @linux//drivers/md/... by looking for dm_register_target() calls.
 	Type string
 	// Parameters are additional parameters specific to the target type.
-	Parameters string
+	// Note that null bytes and parameters consisting only of whitespace
+	// characters cannot be encoded and will return an error.
+	Parameters []string
 }
 
 func LoadTable(name string, readOnly bool, targets []Target) error {
@@ -227,9 +259,13 @@
 	}
 	var data bytes.Buffer
 	for _, target := range targets {
+		encodedParams, err := marshalParams(target.Parameters)
+		if err != nil {
+			return fmt.Errorf("cannot encode parameters: %w", err)
+		}
 		// Gives the size of the spec and the null-terminated params aligned to 8 bytes
-		padding := len(target.Parameters) % 8
-		targetSize := uint32(int(unsafe.Sizeof(DMTargetSpec{})) + (len(target.Parameters) + 1) + padding)
+		padding := len(encodedParams) % 8
+		targetSize := uint32(int(unsafe.Sizeof(DMTargetSpec{})) + (len(encodedParams) + 1) + padding)
 
 		targetSpec := DMTargetSpec{
 			SectorStart: target.StartSector,
@@ -242,7 +278,7 @@
 		if err := binary.Write(&data, native_endian.NativeEndian(), &targetSpec); err != nil {
 			panic(err)
 		}
-		data.WriteString(target.Parameters)
+		data.WriteString(encodedParams)
 		data.WriteByte(0x00)
 		for i := 0; i < padding; i++ {
 			data.WriteByte(0x00)