diff --git a/internal/app/machined/pkg/runtime/v1alpha1/bootloader/grub/decode.go b/internal/app/machined/pkg/runtime/v1alpha1/bootloader/grub/decode.go index 2655f3288f..bdc66804d3 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/bootloader/grub/decode.go +++ b/internal/app/machined/pkg/runtime/v1alpha1/bootloader/grub/decode.go @@ -14,7 +14,7 @@ import ( var ( defaultEntryRegex = regexp.MustCompile(`(?m)^\s*set default="(.*)"\s*$`) fallbackEntryRegex = regexp.MustCompile(`(?m)^\s*set fallback="(.*)"\s*$`) - menuEntryRegex = regexp.MustCompile(`(?m)^menuentry "(.+)" {([^}]+)}`) + menuEntryRegex = regexp.MustCompile(`(?ms)^menuentry\s+"(.+?)" {(.+?)[^\\]}`) linuxRegex = regexp.MustCompile(`(?m)^\s*linux\s+(.+?)\s+(.*)$`) initrdRegex = regexp.MustCompile(`(?m)^\s*initrd\s+(.+)$`) ) @@ -61,7 +61,7 @@ func Decode(c []byte) (*Config, error) { } if len(defaultEntryMatches[0]) != 2 { - return nil, fmt.Errorf("expected 2 matches, got %d", len(defaultEntryMatches[0])) + return nil, fmt.Errorf("default entry: expected 2 matches, got %d", len(defaultEntryMatches[0])) } defaultEntry, err := ParseBootLabel(string(defaultEntryMatches[0][1])) @@ -89,7 +89,7 @@ func parseEntries(conf []byte) (map[BootLabel]MenuEntry, error) { matches := menuEntryRegex.FindAllSubmatch(conf, -1) for _, m := range matches { if len(m) != 3 { - return nil, fmt.Errorf("expected 3 matches, got %d", len(m)) + return nil, fmt.Errorf("conf block: expected 3 matches, got %d", len(m)) } confBlock := m[2] @@ -118,15 +118,17 @@ func parseEntries(conf []byte) (map[BootLabel]MenuEntry, error) { } func parseConfBlock(block []byte) (linux, cmdline, initrd string, err error) { + block = []byte(unquote(string(block))) + linuxMatches := linuxRegex.FindAllSubmatch(block, -1) if len(linuxMatches) != 1 { return "", "", "", - fmt.Errorf("expected 1 match, got %d", len(linuxMatches)) + fmt.Errorf("linux: expected 1 match, got %d", len(linuxMatches)) } if len(linuxMatches[0]) != 3 { return "", "", "", - fmt.Errorf("expected 3 matches, got %d", len(linuxMatches[0])) + fmt.Errorf("linux: expected 3 matches, got %d", len(linuxMatches[0])) } linux = string(linuxMatches[0][1]) @@ -135,12 +137,12 @@ func parseConfBlock(block []byte) (linux, cmdline, initrd string, err error) { initrdMatches := initrdRegex.FindAllSubmatch(block, -1) if len(initrdMatches) != 1 { return "", "", "", - fmt.Errorf("expected 1 match, got %d", len(initrdMatches)) + fmt.Errorf("initrd: expected 1 match, got %d: %s", len(initrdMatches), string(block)) } if len(initrdMatches[0]) != 2 { return "", "", "", - fmt.Errorf("expected 2 matches, got %d", len(initrdMatches[0])) + fmt.Errorf("initrd: expected 2 matches, got %d", len(initrdMatches[0])) } initrd = string(initrdMatches[0][1]) diff --git a/internal/app/machined/pkg/runtime/v1alpha1/bootloader/grub/encode.go b/internal/app/machined/pkg/runtime/v1alpha1/bootloader/grub/encode.go index a6b32e5ce2..0be457924b 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/bootloader/grub/encode.go +++ b/internal/app/machined/pkg/runtime/v1alpha1/bootloader/grub/encode.go @@ -28,7 +28,7 @@ terminal_output console menuentry "{{ $entry.Name }}" { set gfxmode=auto set gfxpayload=text - linux {{ $entry.Linux }} {{ $entry.Cmdline }} + linux {{ $entry.Linux }} {{ quote $entry.Cmdline }} initrd {{ $entry.Initrd }} } {{ end -}} @@ -59,7 +59,9 @@ func (c *Config) Encode(wr io.Writer) error { return err } - t := template.Must(template.New("grub").Parse(confTemplate)) + t := template.Must(template.New("grub").Funcs(template.FuncMap{ + "quote": quote, + }).Parse(confTemplate)) return t.Execute(wr, c) } diff --git a/internal/app/machined/pkg/runtime/v1alpha1/bootloader/grub/export_test.go b/internal/app/machined/pkg/runtime/v1alpha1/bootloader/grub/export_test.go new file mode 100644 index 0000000000..bc10dbd9fc --- /dev/null +++ b/internal/app/machined/pkg/runtime/v1alpha1/bootloader/grub/export_test.go @@ -0,0 +1,15 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package grub + +// Quote exported for testing. +func Quote(s string) string { + return quote(s) +} + +// Unquote exported for testing. +func Unquote(s string) string { + return unquote(s) +} diff --git a/internal/app/machined/pkg/runtime/v1alpha1/bootloader/grub/grub_test.go b/internal/app/machined/pkg/runtime/v1alpha1/bootloader/grub/grub_test.go index 7b5ad179d6..5733ad7038 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/bootloader/grub/grub_test.go +++ b/internal/app/machined/pkg/runtime/v1alpha1/bootloader/grub/grub_test.go @@ -52,6 +52,22 @@ func TestDecode(t *testing.T) { assert.True(t, strings.HasPrefix(b.Initrd, "/B/")) } +func TestEncodeDecode(t *testing.T) { + config := grub.NewConfig("talos.platform=metal talos.config=https://my-metadata.server/talos/config?hostname=${hostname}&mac=${mac}") + require.NoError(t, config.Put(grub.BootB, "talos.platform=metal talos.config=https://my-metadata.server/talos/config?uuid=${uuid}")) + + var b bytes.Buffer + + require.NoError(t, config.Encode(&b)) + + t.Logf("config encoded to:\n%s", b.String()) + + config2, err := grub.Decode(b.Bytes()) + require.NoError(t, err) + + assert.Equal(t, config, config2) +} + func TestParseBootLabel(t *testing.T) { label, err := grub.ParseBootLabel("A - v1") assert.NoError(t, err) diff --git a/internal/app/machined/pkg/runtime/v1alpha1/bootloader/grub/quote.go b/internal/app/machined/pkg/runtime/v1alpha1/bootloader/grub/quote.go new file mode 100644 index 0000000000..8b91882172 --- /dev/null +++ b/internal/app/machined/pkg/runtime/v1alpha1/bootloader/grub/quote.go @@ -0,0 +1,29 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package grub + +import ( + "strings" +) + +// quote according to (incomplete) GRUB quoting rules. +// +// See https://www.gnu.org/software/grub/manual/grub/html_node/Shell_002dlike-scripting.html +func quote(s string) string { + for _, c := range `\{}$|;<>"` { + s = strings.ReplaceAll(s, string(c), `\`+string(c)) + } + + return s +} + +// unquote according to (incomplete) GRUB quoting rules. +func unquote(s string) string { + for _, c := range `{}$|;<>\"` { + s = strings.ReplaceAll(s, `\`+string(c), string(c)) + } + + return s +} diff --git a/internal/app/machined/pkg/runtime/v1alpha1/bootloader/grub/quote_test.go b/internal/app/machined/pkg/runtime/v1alpha1/bootloader/grub/quote_test.go new file mode 100644 index 0000000000..e042209e1e --- /dev/null +++ b/internal/app/machined/pkg/runtime/v1alpha1/bootloader/grub/quote_test.go @@ -0,0 +1,99 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package grub_test + +import ( + "testing" + + "github.com/siderolabs/talos/internal/app/machined/pkg/runtime/v1alpha1/bootloader/grub" +) + +//nolint:dupl +func TestQuote(t *testing.T) { + t.Parallel() + + for _, test := range []struct { + name string + input string + expected string + }{ + { + name: "empty", + input: "", + expected: "", + }, + { + name: "no special characters", + input: "foo", + expected: "foo", + }, + { + name: "backslash", + input: `foo\`, + expected: `foo\\`, + }, + { + name: "escaped backslash", + input: `foo\$`, + expected: `foo\\\$`, + }, + } { + test := test + + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + actual := grub.Quote(test.input) + + if actual != test.expected { + t.Fatalf("expected %q, got %q", test.expected, actual) + } + }) + } +} + +//nolint:dupl +func TestUnquote(t *testing.T) { + t.Parallel() + + for _, test := range []struct { + name string + input string + expected string + }{ + { + name: "empty", + input: "", + expected: "", + }, + { + name: "no special characters", + input: "foo", + expected: "foo", + }, + { + name: "backslash", + input: `foo\\`, + expected: `foo\`, + }, + { + name: "escaped backslash", + input: `foo\\\$`, + expected: `foo\$`, + }, + } { + test := test + + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + actual := grub.Unquote(test.input) + + if actual != test.expected { + t.Fatalf("expected %q, got %q", test.expected, actual) + } + }) + } +}