Skip to content

Commit

Permalink
fix: correctly quote and unquote strings in GRUB config
Browse files Browse the repository at this point in the history
One of the fields in the GRUB config - boot arguments - contains
user-controlled input. Talos supports variable expansion in
`talos.config` parameter, and uses `${var}` syntax.

In GRUB config, `}` is a special character, and introduction of `}`
breaks config parsing both for GRUB and Talos.

Correctly escape and unescape special characters.

Signed-off-by: Andrey Smirnov <andrey.smirnov@talos-systems.com>
  • Loading branch information
smira committed Feb 2, 2023
1 parent 54cf067 commit 54f7d4c
Show file tree
Hide file tree
Showing 6 changed files with 172 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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+(.+)$`)
)
Expand Down Expand Up @@ -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]))
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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])
Expand All @@ -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])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 -}}
Expand Down Expand Up @@ -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)
}
Original file line number Diff line number Diff line change
@@ -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)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
}

0 comments on commit 54f7d4c

Please sign in to comment.