From 54a292a86bb8d3fef0aba5c03fb1d209c6c642f5 Mon Sep 17 00:00:00 2001 From: rahul05ranjan <11793455+rahul05ranjan@users.noreply.github.com> Date: Wed, 28 Oct 2026 10:15:28 +0530 Subject: [PATCH] fix(rumprun): keep argument boundaries in the guest config The rumprun configuration carried the application command as a single 'cmdline' string, which the guest splits on whitespace (config.c: makeargv/rumprun_parseargs). An argument that contains a space therefore reached the application as two arguments and an empty argument vanished, so 'redis-server --requirepass "a b"' failed with a bare 'a b' split into 'a' and 'b', and '--logfile ""' lost its value entirely. The rumprun configuration format has a second form for exactly this: 'rc' takes bin plus an argv array, and the guest copies every array element as its own string (config.c: addbin), with no whitespace processing. Emit the command in the rc form, keeping argv[0] as bin and the rest as the argv array. The JSON encoder keeps HTML escaping off, because the guest's jsmn parser keeps string bytes as written and does not decode \uXXXX escapes; the same helper shape as the open #1101, which this change does not depend on but will textually overlap - happy to rebase once one lands. Fixes #1158 Co-authored-by: rahul05ranjan <11793455+rahul05ranjan@users.noreply.github.com> Signed-off-by: rahul05ranjan <11793455+rahul05ranjan@users.noreply.github.com> --- pkg/unikontainers/unikernels/rumprun.go | 54 ++++++++-- pkg/unikontainers/unikernels/rumprun_test.go | 101 +++++++++++++++++++ 2 files changed, 147 insertions(+), 8 deletions(-) create mode 100644 pkg/unikontainers/unikernels/rumprun_test.go diff --git a/pkg/unikontainers/unikernels/rumprun.go b/pkg/unikontainers/unikernels/rumprun.go index 403fdb501..07c4801a4 100644 --- a/pkg/unikontainers/unikernels/rumprun.go +++ b/pkg/unikontainers/unikernels/rumprun.go @@ -15,6 +15,7 @@ package unikernels import ( + "bytes" "encoding/json" "fmt" "strings" @@ -26,7 +27,7 @@ const RumprunUnikernel string = "rumprun" const SubnetMask125 = "128.0.0.0" type Rumprun struct { - Command string + CmdLine []string Monitor string Envs []string Net RumprunNet @@ -37,6 +38,27 @@ type RumprunCmd struct { CmdLine string `json:"cmdline"` } +// rumprunRc is one entry of the rumprun configuration's "rc" list: bin is +// passed to the program as argv[0] and every element of argv as its own +// argument, so argument boundaries survive (doc/config.md). +type rumprunRc struct { + Bin string `json:"bin"` + Argv []string `json:"argv"` +} + +// marshalRumprunJSON marshals without HTML escaping. The guest parses the +// configuration with jsmn and keeps string bytes as they appear, without +// decoding \uXXXX escapes, so &, < and > must stay raw. +func marshalRumprunJSON(v any) ([]byte, error) { + var buf bytes.Buffer + enc := json.NewEncoder(&buf) + enc.SetEscapeHTML(false) + if err := enc.Encode(v); err != nil { + return nil, err + } + return bytes.TrimSuffix(buf.Bytes(), []byte("\n")), nil +} + type RumprunEnv struct { Env string `json:"env"` } @@ -68,12 +90,28 @@ func (r *Rumprun) CommandString() (string, error) { envJSONString := "" netJSONString := "" blkJSONString := "" - cmd := RumprunCmd{ - CmdLine: r.Command, - } - cmdJSON, err := json.Marshal(cmd) - if err != nil { - return "", fmt.Errorf("could not marshal cmdline: %v", err) + var cmdJSON []byte + var err error + if len(r.CmdLine) == 0 { + // Without a command there is nothing to describe in rc. Keep the + // empty cmdline of the old format, which lets the guest run its + // first baked program without arguments. + cmdJSON, err = marshalRumprunJSON(RumprunCmd{CmdLine: ""}) + if err != nil { + return "", fmt.Errorf("could not marshal cmdline: %v", err) + } + } else { + // The old "cmdline" format is one whitespace-delimited string, which + // cannot tell an argument that contains a space from two arguments + // and drops empty arguments. "rc" keeps every argv element intact. + cmdJSON, err = marshalRumprunJSON(struct { + Rc []rumprunRc `json:"rc"` + }{ + Rc: []rumprunRc{{Bin: r.CmdLine[0], Argv: r.CmdLine[1:]}}, + }) + if err != nil { + return "", fmt.Errorf("could not marshal rc: %v", err) + } } cmdJSONString = string(cmdJSON) for i, eVar := range r.Envs { @@ -221,7 +259,7 @@ func (r *Rumprun) Init(data types.UnikernelParams) error { r.Blk.Source = "" } - r.Command = strings.Join(data.CmdLine, " ") + r.CmdLine = data.CmdLine r.Monitor = data.Monitor r.Envs = data.EnvVars diff --git a/pkg/unikontainers/unikernels/rumprun_test.go b/pkg/unikontainers/unikernels/rumprun_test.go new file mode 100644 index 000000000..d80fce4b9 --- /dev/null +++ b/pkg/unikontainers/unikernels/rumprun_test.go @@ -0,0 +1,101 @@ +// 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 ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/urunc-dev/urunc/pkg/unikontainers/types" +) + +// The guest splits a "cmdline" value on whitespace (config.c's makeargv and +// rumprun_parseargs), so arguments that contain spaces and empty arguments +// cannot be expressed in that form. The "rc" form keeps every argv element +// intact: config.c's addbin copies each array element as its own string. +func TestRumprunCommandStringPreservesArgumentBoundaries(t *testing.T) { + t.Parallel() + + rumprun := &Rumprun{ + CmdLine: []string{"redis-server", "--requirepass", "a b", "--logfile", ""}, + Envs: []string{"REDIS_CONF=/etc/redis.conf"}, + } + + commandString, err := rumprun.CommandString() + require.NoError(t, err) + + // The result is not valid JSON as a whole (the multiple "env" keys), so + // check the rc member against its exact serialization. + assert.Contains(t, commandString, + `"rc":[{"bin":"redis-server","argv":["--requirepass","a b","--logfile",""]}]`, + "every argument must stay one element, including the spaced value and the empty one") + + var parsed struct { + Rc []struct { + Bin string `json:"bin"` + Argv []string `json:"argv"` + } `json:"rc"` + } + require.NoError(t, json.Unmarshal([]byte(commandString), &parsed)) + require.Len(t, parsed.Rc, 1) + assert.Equal(t, "redis-server", parsed.Rc[0].Bin) + assert.Equal(t, []string{"--requirepass", "a b", "--logfile", ""}, parsed.Rc[0].Argv) +} + +// The guest's token2cstr keeps string bytes as they appear, without decoding +// \uXXXX escapes, so the configuration must not escape &, < or >. +func TestRumprunCommandStringKeepsHTMLEScapeCharactersRaw(t *testing.T) { + t.Parallel() + + rumprun := &Rumprun{ + CmdLine: []string{"sh", "-c", "echo ad"}, + } + + commandString, err := rumprun.CommandString() + require.NoError(t, err) + + assert.Contains(t, commandString, `"argv":["-c","echo ad"]`) + assert.NotContains(t, commandString, `u003`, "the guest cannot decode \\uXXXX escapes") +} + +// An empty command line has no rc entry to describe; it keeps the old empty +// cmdline, which lets the guest run its first baked program without arguments. +func TestRumprunCommandStringEmptyCmdLineKeepsEmptyCmdline(t *testing.T) { + t.Parallel() + + rumprun := &Rumprun{CmdLine: nil} + + commandString, err := rumprun.CommandString() + require.NoError(t, err) + + assert.Contains(t, commandString, `"cmdline":""`) + assert.NotContains(t, commandString, `"rc"`) +} + +// Init must hand the whole argument list to CommandString instead of +// collapsing it into one string first. +func TestRumprunInitKeepsCmdLineUnjoined(t *testing.T) { + t.Parallel() + + rumprun := newRumprun() + err := rumprun.Init(types.UnikernelParams{ + CmdLine: []string{"app", "one arg", "another"}, + Monitor: "hvt", + }) + require.NoError(t, err) + assert.Equal(t, []string{"app", "one arg", "another"}, rumprun.CmdLine) +}