strip control characters from guest cloud-init log lines

The hostagent forwards cloud-init log lines read from the guest into
CloudInitProgress events, which limactl start and limactl watch print to
the operator terminal with a bare %s. A guest controls those bytes, so
it can emit ANSI/OSC escape sequences to spoof the terminal. Strip
non-printable runes at the single emit choke point emitCloudInitProgressEvent.

The control-character stripper is moved from pkg/cidata to strutil.RemoveControlChars
so both cidata and the hostagent path share it.

Signed-off-by: nashit hayyat <nashit@bugqore.com>
This commit is contained in:
nashit hayyat committed 2026-08-24 12:48:00 +05:30
1 parent 1281ca54bc
commit d9238b3e17
4 files changed
+63 -12

No files matched your search

+2 -12
View File
@@ -19,7 +19,6 @@ import (
"strconv"
"strings"
"time"
"unicode"
"github.com/docker/go-units"
"github.com/sirupsen/logrus"
@@ -36,6 +35,7 @@ import (
"github.com/lima-vm/lima/v2/pkg/networks/usernet"
"github.com/lima-vm/lima/v2/pkg/osutil"
"github.com/lima-vm/lima/v2/pkg/sshutil"
"github.com/lima-vm/lima/v2/pkg/strutil"
)
var netLookupIP = func(host string) []net.IP {
@@ -132,7 +132,7 @@ func templateArgs(ctx context.Context, bootScripts bool, instDir, name string, i
Name: name,
Hostname: hostname.FromInstName(name), // TODO: support customization
User: *instConfig.User.Name,
Comment: removeControlChars(*instConfig.User.Comment),
Comment: strutil.RemoveControlChars(*instConfig.User.Comment),
Home: *instConfig.User.Home,
Shell: *instConfig.User.Shell,
UID: *instConfig.User.UID,
@@ -496,16 +496,6 @@ func GenerateISO9660(ctx context.Context, drv driver.Driver, instDir, name strin
return args.IID, iso9660util.Write(filepath.Join(instDir, filenames.CIDataISO), "cidata", layout, iso9660Options...)
}
func removeControlChars(s string) string {
out := make([]rune, 0, len(s))
for _, r := range s {
if unicode.IsPrint(r) {
out = append(out, r)
}
}
return string(out)
}
func getCert(content string) Cert {
lines := []string{}
for line := range strings.SplitSeq(content, "\n") {
+36
View File
@@ -0,0 +1,36 @@
// SPDX-FileCopyrightText: Copyright The Lima Authors
// SPDX-License-Identifier: Apache-2.0
package hostagent
import (
"bytes"
"encoding/json"
"strings"
"testing"
"unicode"
"gotest.tools/v3/assert"
"github.com/lima-vm/lima/v2/pkg/hostagent/events"
)
func TestEmitCloudInitProgressEventStripsControlChars(t *testing.T) {
var buf bytes.Buffer
a := &HostAgent{eventEnc: json.NewEncoder(&buf)}
// A compromised guest can put arbitrary bytes in its cloud-init log, including
// an ANSI escape sequence that would rewrite the operator's terminal.
const payload = "starting\x1b[2Kspoofed\x07 line"
a.emitCloudInitProgressEvent(t.Context(), &events.CloudInitProgress{LogLine: payload})
var ev events.Event
assert.NilError(t, json.Unmarshal(buf.Bytes(), &ev))
assert.Assert(t, ev.Status.CloudInitProgress != nil)
got := ev.Status.CloudInitProgress.LogLine
for _, r := range got {
assert.Assert(t, unicode.IsPrint(r), "control char %#q survived in %#q", r, got)
}
assert.Equal(t, got, "starting[2Kspoofed line")
assert.Assert(t, !strings.ContainsRune(got, '\x1b'))
}
+7
View File
@@ -48,6 +48,7 @@ import (
"github.com/lima-vm/lima/v2/pkg/portfwd"
"github.com/lima-vm/lima/v2/pkg/sshutil"
"github.com/lima-vm/lima/v2/pkg/store"
"github.com/lima-vm/lima/v2/pkg/strutil"
"github.com/lima-vm/lima/v2/pkg/version/versionutil"
)
@@ -353,6 +354,12 @@ func (a *HostAgent) emitEvent(_ context.Context, ev events.Event) {
}
func (a *HostAgent) emitCloudInitProgressEvent(ctx context.Context, progress *events.CloudInitProgress) {
// LogLine is read verbatim from the guest's cloud-init output over SSH, so an
// untrusted guest controls its bytes. limactl start/watch print it to the
// operator's terminal, so strip control characters here to keep the guest
// from injecting ANSI/OSC escape sequences.
progress.LogLine = strutil.RemoveControlChars(progress.LogLine)
a.statusMu.RLock()
currentStatus := a.currentStatus
a.statusMu.RUnlock()
+18
View File
@@ -0,0 +1,18 @@
// SPDX-FileCopyrightText: Copyright The Lima Authors
// SPDX-License-Identifier: Apache-2.0
package strutil
import "unicode"
// RemoveControlChars drops non-printable runes so guest-controlled text is safe
// to print to the operator's terminal.
func RemoveControlChars(s string) string {
out := make([]rune, 0, len(s))
for _, r := range s {
if unicode.IsPrint(r) {
out = append(out, r)
}
}
return string(out)
}