diff --git a/pkg/unikontainers/unikernels/linux.go b/pkg/unikontainers/unikernels/linux.go index bca93e5a9..0a2e3be1e 100644 --- a/pkg/unikontainers/unikernels/linux.go +++ b/pkg/unikontainers/unikernels/linux.go @@ -250,10 +250,16 @@ func (l *Linux) parseCmdLine(cmdLine []string) error { return fmt.Errorf("no init was specified") } - // Wrap multi-word arguments in quotes for urunit + // Wrap multi-word arguments in quotes for urunit. Arguments that already + // contain a single quote cannot be safely wrapped this way, since naively + // concatenating quotes produces an unbalanced quote sequence that + // corrupts urunit's token-boundary parsing of the boot cmdline. normalizedArgs := make([]string, len(cmdLine)) for i, arg := range cmdLine { arg = strings.TrimSpace(arg) + if strings.Contains(arg, "'") { + return fmt.Errorf("argument %q contains an unsupported single quote character", arg) + } if strings.Contains(arg, " ") { normalizedArgs[i] = "'" + arg + "'" } else { diff --git a/pkg/unikontainers/unikernels/linux_test.go b/pkg/unikontainers/unikernels/linux_test.go new file mode 100644 index 000000000..0db2501cd --- /dev/null +++ b/pkg/unikontainers/unikernels/linux_test.go @@ -0,0 +1,80 @@ +// Copyright (c) 2023-2026, Nubificus LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package unikernels + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestLinuxParseCmdLine(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + cmdLine []string + expectedApp string + expectedCmd string + expectErr bool + }{ + { + name: "empty cmdline", + cmdLine: []string{}, + expectErr: true, + }, + { + name: "single word app only", + cmdLine: []string{"/urunit"}, + expectedApp: "/urunit", + expectedCmd: "", + }, + { + name: "multi-word argument gets quoted", + cmdLine: []string{"/urunit", "sh -c ls"}, + expectedApp: "/urunit", + expectedCmd: "'sh -c ls'", + }, + { + name: "argument with space and single quote is rejected", + cmdLine: []string{"/urunit", "sh -c \"echo it's broken\""}, + expectErr: true, + }, + { + name: "argument with single quote and no space is rejected", + cmdLine: []string{"/urunit", "it's"}, + expectErr: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + l := &Linux{} + err := l.parseCmdLine(tc.cmdLine) + + if tc.expectErr { + require.Error(t, err) + return + } + + require.NoError(t, err) + assert.Equal(t, tc.expectedApp, l.App) + assert.Equal(t, tc.expectedCmd, l.Command) + }) + } +}