Repository: containers/podman (Go)

Issue:
Confirm only using UTF8 when interacting with wsl.exe
### Feature request description

This is a copy of this issue: https://github.com/podman-desktop/podman-desktop/issues/12998

Currently, the wsl.exe CLI uses UTF16 by default https://github.com/microsoft/WSL/issues/13093. We are investigating whether we can change wsl.exe to be UTF8 by default, and want to make sure this will not break folks interacting with it.

So our recommendation is that you set the environment variable WSL_UTF8=1 when interacting with the WSL CLI to get that UTF8 behaviour now, so that you aren't broken in the future.

### Suggest potential solution

I looked through the repo and didn't see any cases of WSL_UTF8 being set. I also searched for `wsl.exe` but didn't see any instance besides a test (wutil_test.go) referencing it. So I wanted to open this issue to make sure it was tracked and known, thank you!

### Have you considered any alternatives?

A clear and concise description of any alternative solutions or features you've considered.

### Additional context

Add any other context or screenshots about the feature request here.


Discussion:
https://github.com/containers/podman/blob/1a3b35673d7661fc80b7fbff74d93bac9d53e083/pkg/machine/wsl/machine.go#L97-L114

That seems to be parsing UTF16 so this needs to be changed, I see no reason we we couldn't do that.
 matchOutputLine() in pkg/machine/wsl/wutil/wutil.go as well

cc @l0rd 

--- 

if you want to search for wsl calls look for `"wsl"` because we don't add the .exe suffix when executing it.
This was indeed a problem as the command `podman machine start` fails if `WSL_UTF8=1`. I have opened a PR to enforce UTF-8 encoded output and fix the `podman machine init` problem.



Patch:
diff --git a/cmd/podman-wslkerninst/main.go b/cmd/podman-wslkerninst/main.go
index 9a7d36a28d2..c2ebc602405 100644
--- a/cmd/podman-wslkerninst/main.go
+++ b/cmd/podman-wslkerninst/main.go
@@ -48,7 +48,7 @@ func installWslKernel() error {
 	)
 	backoff := 500 * time.Millisecond
 	for i := 1; i < 6; i++ {
-		err = wutil.SilentExec("wsl", "--update")
+		err = wutil.SilentExec("--update")
 		if err == nil {
 			break
 		}
diff --git a/go.mod b/go.mod
index d9f407109fd..8482a540e03 100644
--- a/go.mod
+++ b/go.mod
@@ -73,7 +73,6 @@ require (
 	golang.org/x/sync v0.15.0
 	golang.org/x/sys v0.33.0
 	golang.org/x/term v0.32.0
-	golang.org/x/text v0.26.0
 	google.golang.org/protobuf v1.36.6
 	gopkg.in/inf.v0 v0.9.1
 	gopkg.in/yaml.v3 v3.0.1
@@ -185,6 +184,7 @@ require (
 	go.yaml.in/yaml/v2 v2.4.2 // indirect
 	golang.org/x/mod v0.25.0 // indirect
 	golang.org/x/oauth2 v0.30.0 // indirect
+	golang.org/x/text v0.26.0 // indirect
 	golang.org/x/time v0.11.0 // indirect
 	golang.org/x/tools v0.33.0 // indirect
 	google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb // indirect
diff --git a/pkg/machine/wsl/machine.go b/pkg/machine/wsl/machine.go
index 9808f38d4ca..4390a8f4388 100644
--- a/pkg/machine/wsl/machine.go
+++ b/pkg/machine/wsl/machine.go
@@ -20,11 +20,10 @@ import (
 	"github.com/containers/podman/v5/pkg/machine/env"
 	"github.com/containers/podman/v5/pkg/machine/ignition"
 	"github.com/containers/podman/v5/pkg/machine/vmconfigs"
+	"github.com/containers/podman/v5/pkg/machine/wsl/wutil"
 	"github.com/containers/podman/v5/utils"
 	"github.com/containers/storage/pkg/homedir"
 	"github.com/sirupsen/logrus"
-	"golang.org/x/text/encoding/unicode"
-	"golang.org/x/text/transform"
 )
 
 var (
@@ -100,13 +99,9 @@ func provisionWSLDist(name string, imagePath string, prompt string) (string, err
 	// 1. Wsl/Service/RegisterDistro/CreateVm/HCS/ERROR_NOT_SUPPORTED
 	// 2. Wsl/Service/RegisterDistro/CreateVm/HCS/HCS_E_SERVICE_NOT_AVAILABLE
 	cmdOutput := &bytes.Buffer{}
-	err = runCmdPassThroughTee(cmdOutput, "wsl", "--import", dist, distTarget, imagePath, "--version", "2")
-	decoder := unicode.UTF16(unicode.LittleEndian, unicode.UseBOM).NewDecoder()
-	decoded, _, decodeErr := transform.Bytes(decoder, cmdOutput.Bytes())
-	if decodeErr != nil {
-		return "", fmt.Errorf("failed to decode WSL output: %w", decodeErr)
-	}
-	decodedStr := strings.ToLower(string(decoded))
+	cmd := wutil.NewWSLCommand("--import", dist, distTarget, imagePath, "--version", "2")
+	err = runCmdPassThroughTee(cmdOutput, cmd)
+	decodedStr := strings.ToLower(cmdOutput.String())
 	for _, substr := range []string{"hcs/error_not_supported", "hcs/hcs_e_service_not_available"} {
 		if strings.Contains(decodedStr, substr) {
 			return "", ErrWslNotSupported
@@ -373,13 +368,15 @@ func installWsl() error {
 		return err
 	}
 	defer log.Close()
-	if err := runCmdPassThroughTee(log, "dism", "/online", "/enable-feature",
-		"/featurename:Microsoft-Windows-Subsystem-Linux", "/all", "/norestart"); isMsiError(err) {
+	cmd := exec.Command("dism", "/online", "/enable-feature",
+		"/featurename:Microsoft-Windows-Subsystem-Linux", "/all", "/norestart")
+	if err := runCmdPassThroughTee(log, cmd); isMsiError(err) {
 		return fmt.Errorf("could not enable WSL Feature: %w", err)
 	}
 
-	if err = runCmdPassThroughTee(log, "dism", "/online", "/enable-feature",
-		"/featurename:VirtualMachinePlatform", "/all", "/norestart"); isMsiError(err) {
+	cmd = exec.Command("dism", "/online", "/enable-feature",
+		"/featurename:VirtualMachinePlatform", "/all", "/norestart")
+	if err = runCmdPassThroughTee(log, cmd); isMsiError(err) {
 		return fmt.Errorf("could not enable Virtual Machine Feature: %w", err)
 	}
 
@@ -466,50 +463,49 @@ func withUser(s string, user string) string {
 func wslInvoke(dist string, arg ...string) error {
 	newArgs := []string{"-u", "root", "-d", dist}
 	newArgs = append(newArgs, arg...)
-	return runCmdPassThrough("wsl", newArgs...)
+	cmd := wutil.NewWSLCommand(newArgs...)
+	return runCmdPassThrough(cmd)
 }
 
 func wslPipe(input string, dist string, arg ...string) error {
 	newArgs := []string{"-u", "root", "-d", dist}
 	newArgs = append(newArgs, arg...)
-	return pipeCmdPassThrough("wsl", input, newArgs...)
+	cmd := wutil.NewWSLCommand(newArgs...)
+	return pipeCmdPassThrough(cmd, input)
 }
 
-func runCmdPassThrough(name string, arg ...string) error {
-	logrus.Debugf("Running command: %s %v", name, arg)
-	cmd := exec.Command(name, arg...)
+func runCmdPassThrough(cmd *exec.Cmd) error {
+	logrus.Debugf("Running command: %s %v", cmd.Path, cmd.Args)
 	cmd.Stdin = os.Stdin
 	cmd.Stdout = os.Stdout
 	cmd.Stderr = os.Stderr
 	if err := cmd.Run(); err != nil {
-		return fmt.Errorf("command %s %v failed: %w", name, arg, err)
+		return fmt.Errorf("command %s %v failed: %w", cmd.Path, cmd.Args, err)
 	}
 	return nil
 }
 
-func runCmdPassThroughTee(out io.Writer, name string, arg ...string) error {
-	logrus.Debugf("Running command: %s %v", name, arg)
+func runCmdPassThroughTee(out io.Writer, cmd *exec.Cmd) error {
+	logrus.Debugf("Running command: %s %v", cmd.Path, cmd.Args)
 
 	// TODO - Perhaps improve this with a conpty pseudo console so that
 	//        dism installer text bars mirror console behavior (redraw)
-	cmd := exec.Command(name, arg...)
 	cmd.Stdin = os.Stdin
 	cmd.Stdout = io.MultiWriter(os.Stdout, out)
 	cmd.Stderr = io.MultiWriter(os.Stderr, out)
 	if err := cmd.Run(); isMsiError(err) {
-		return fmt.Errorf("command %s %v failed: %w", name, arg, err)
+		return fmt.Errorf("command %s %v failed: %w", cmd.Path, cmd.Args, err)
 	}
 	return nil
 }
 
-func pipeCmdPassThrough(name string, input string, arg ...string) error {
-	logrus.Debugf("Running command: %s %v", name, arg)
-	cmd := exec.Command(name, arg...)
+func pipeCmdPassThrough(cmd *exec.Cmd, input string) error {
+	logrus.Debugf("Running command: %s %v", cmd.Path, cmd.Args)
 	cmd.Stdin = strings.NewReader(input)
 	cmd.Stdout = os.Stdout
 	cmd.Stderr = os.Stderr
 	if err := cmd.Run(); err != nil {
-		return fmt.Errorf("command %s %v failed: %w", name, arg, err)
+		return fmt.Errorf("command %s %v failed: %w", cmd.Path, cmd.Args, err)
 	}
 	return nil
 }
@@ -569,7 +565,7 @@ func getAllWSLDistros(running bool) (map[string]struct{}, error) {
 	if running {
 		args = append(args, "--running")
 	}
-	cmd := exec.Command("wsl", args...)
+	cmd := wutil.NewWSLCommand(args...)
 	out, err := cmd.StdoutPipe()
 	if err != nil {
 		return nil, err
@@ -581,7 +577,7 @@ func getAllWSLDistros(running bool) (map[string]struct{}, error) {
 	}
 
 	all := make(map[string]struct{})
-	scanner := bufio.NewScanner(transform.NewReader(out, unicode.UTF16(unicode.LittleEndian, unicode.UseBOM).NewDecoder()))
+	scanner := bufio.NewScanner(out)
 	for scanner.Scan() {
 		fields := strings.Fields(scanner.Text())
 		if len(fields) > 0 {
@@ -598,7 +594,7 @@ func getAllWSLDistros(running bool) (map[string]struct{}, error) {
 }
 
 func isSystemdRunning(dist string) (bool, error) {
-	cmd := exec.Command("wsl", "-u", "root", "-d", dist, "sh")
+	cmd := wutil.NewWSLCommand("-u", "root", "-d", dist, "sh")
 	cmd.Stdin = strings.NewReader(sysdpid + "\necho $SYSDPID\n")
 	out, err := cmd.StdoutPipe()
 	if err != nil {
@@ -621,26 +617,26 @@ func isSystemdRunning(dist string) (bool, error) {
 
 	err = cmd.Wait()
 	if err != nil {
-		return false, fmt.Errorf("command %s %v failed: %w (%s)", cmd.Path, cmd.Args, err, strings.TrimSpace(stderr.String()))
+		return false, fmt.Errorf("command %s %v failed: %w (%s)", cmd.Path, cmd.Args[1:], err, strings.TrimSpace(stderr.String()))
 	}
 
 	return result, nil
 }
 
 func terminateDist(dist string) error {
-	cmd := exec.Command("wsl", "--terminate", dist)
+	cmd := wutil.NewWSLCommand("--terminate", dist)
 	out, err := cmd.CombinedOutput()
 	if err != nil {
-		return fmt.Errorf("command %s %v failed: %w (%s)", cmd.Path, cmd.Args, err, strings.TrimSpace(string(out)))
+		return fmt.Errorf("command %s %v failed: %w (%s)", cmd.Path, cmd.Args[1:], err, strings.TrimSpace(string(out)))
 	}
 	return nil
 }
 
 func unregisterDist(dist string) error {
-	cmd := exec.Command("wsl", "--unregister", dist)
+	cmd := wutil.NewWSLCommand("--unregister", dist)
 	out, err := cmd.CombinedOutput()
 	if err != nil {
-		return fmt.Errorf("command %s %v failed: %w (%s)", cmd.Path, cmd.Args, err, strings.TrimSpace(string(out)))
+		return fmt.Errorf("command %s %v failed: %w (%s)", cmd.Path, cmd.Args[1:], err, strings.TrimSpace(string(out)))
 	}
 	return nil
 }
@@ -685,13 +681,14 @@ func getCPUs(name string) (uint64, error) {
 	if run, _ := isWSLRunning(dist); !run {
 		return 0, nil
 	}
-	cmd := exec.Command("wsl", "-u", "root", "-d", dist, "nproc")
+	cmd := wutil.NewWSLCommand("-u", "root", "-d", dist, "nproc")
 	out, err := cmd.StdoutPipe()
 	if err != nil {
 		return 0, err
 	}
 	stderr := &bytes.Buffer{}
 	cmd.Stderr = stderr
+	cmd.Env = []string{"WSL_UTF8=1"}
 	if err = cmd.Start(); err != nil {
 		return 0, err
 	}
@@ -702,7 +699,7 @@ func getCPUs(name string) (uint64, error) {
 	}
 	err = cmd.Wait()
 	if err != nil {
-		return 0, fmt.Errorf("command %s %v failed: %w (%s)", cmd.Path, cmd.Args, err, strings.TrimSpace(strings.TrimSpace(stderr.String())))
+		return 0, fmt.Errorf("command %s %v failed: %w (%s)", cmd.Path, cmd.Args[1:], err, strings.TrimSpace(strings.TrimSpace(stderr.String())))
 	}
 
 	ret, err := strconv.Atoi(result)
@@ -715,7 +712,7 @@ func getMem(name string) (strongunits.MiB, error) {
 	if run, _ := isWSLRunning(dist); !run {
 		return 0, nil
 	}
-	cmd := exec.Command("wsl", "-u", "root", "-d", dist, "cat", "/proc/meminfo")
+	cmd := wutil.NewWSLCommand("-u", "root", "-d", dist, "cat", "/proc/meminfo")
 	out, err := cmd.StdoutPipe()
 	if err != nil {
 		return 0, err
@@ -746,7 +743,7 @@ func getMem(name string) (strongunits.MiB, error) {
 	}
 	err = cmd.Wait()
 	if err != nil {
-		return 0, fmt.Errorf("command %s %v failed: %w (%s)", cmd.Path, cmd.Args, err, strings.TrimSpace(stderr.String()))
+		return 0, fmt.Errorf("command %s %v failed: %w (%s)", cmd.Path, cmd.Args[1:], err, strings.TrimSpace(stderr.String()))
 	}
 
 	return strongunits.MiB(total - available), err
diff --git a/pkg/machine/wsl/stubber.go b/pkg/machine/wsl/stubber.go
index 6f6720c5dca..edf7e084f43 100644
--- a/pkg/machine/wsl/stubber.go
+++ b/pkg/machine/wsl/stubber.go
@@ -7,10 +7,10 @@ import (
 	"errors"
 	"fmt"
 	"os"
-	"os/exec"
 	"strings"
 
 	"github.com/containers/podman/v5/pkg/machine/env"
+	"github.com/containers/podman/v5/pkg/machine/wsl/wutil"
 
 	gvproxy "github.com/containers/gvisor-tap-vsock/pkg/types"
 	"github.com/containers/podman/v5/pkg/machine"
@@ -108,7 +108,8 @@ func (w WSLStubber) Remove(mc *vmconfigs.MachineConfig) ([]string, func() error,
 	// below if we wanted to hard error on the wsl unregister
 	// of the vm
 	wslRemoveFunc := func() error {
-		if err := runCmdPassThrough("wsl", "--unregister", env.WithPodmanPrefix(mc.Name)); err != nil {
+		cmd := wutil.NewWSLCommand("--unregister", env.WithPodmanPrefix(mc.Name))
+		if err := runCmdPassThrough(cmd); err != nil {
 			return err
 		}
 		return nil
@@ -253,7 +254,7 @@ func (w WSLStubber) StopVM(mc *vmconfigs.MachineConfig, hardStop bool) error {
 		fmt.Fprintf(os.Stderr, "Could not stop API forwarding service (win-sshproxy.exe): %s\n", err.Error())
 	}
 
-	cmd := exec.Command("wsl", "-u", "root", "-d", dist, "sh")
+	cmd := wutil.NewWSLCommand("-u", "root", "-d", dist, "sh")
 	cmd.Stdin = strings.NewReader(waitTerm)
 	out := &bytes.Buffer{}
 	cmd.Stderr = out
@@ -263,7 +264,7 @@ func (w WSLStubber) StopVM(mc *vmconfigs.MachineConfig, hardStop bool) error {
 		return fmt.Errorf("executing wait command: %w", err)
 	}
 
-	exitCmd := exec.Command("wsl", "-u", "root", "-d", dist, "/usr/local/bin/enterns", "systemctl", "exit", "0")
+	exitCmd := wutil.NewWSLCommand("-u", "root", "-d", dist, "/usr/local/bin/enterns", "systemctl", "exit", "0")
 	if err = exitCmd.Run(); err != nil {
 		return fmt.Errorf("stopping systemd: %w", err)
 	}
diff --git a/pkg/machine/wsl/wutil/wutil.go b/pkg/machine/wsl/wutil/wutil.go
index b65ab7bcb2b..69ec64ccccd 100644
--- a/pkg/machine/wsl/wutil/wutil.go
+++ b/pkg/machine/wsl/wutil/wutil.go
@@ -6,13 +6,11 @@ import (
 	"bufio"
 	"fmt"
 	"io"
+	"os"
 	"os/exec"
 	"strings"
 	"sync"
 	"syscall"
-
-	"golang.org/x/text/encoding/unicode"
-	"golang.org/x/text/transform"
 )
 
 var (
@@ -29,8 +27,14 @@ type wslStatus struct {
 	wslFeatureEnabled bool
 }
 
+func NewWSLCommand(arg ...string) *exec.Cmd {
+	cmd := exec.Command("wsl", arg...)
+	cmd.Env = append(os.Environ(), "WSL_UTF8=1")
+	return cmd
+}
+
 func SilentExec(command string, args ...string) error {
-	cmd := exec.Command(command, args...)
+	cmd := NewWSLCommand(args...)
 	cmd.SysProcAttr = &syscall.SysProcAttr{CreationFlags: 0x08000000}
 	cmd.Stdout = nil
 	cmd.Stderr = nil
@@ -40,8 +44,8 @@ func SilentExec(command string, args ...string) error {
 	return nil
 }
 
-func SilentExecCmd(command string, args ...string) *exec.Cmd {
-	cmd := exec.Command(command, args...)
+func SilentExecCmd(args ...string) *exec.Cmd {
+	cmd := NewWSLCommand(args...)
 	cmd.SysProcAttr = &syscall.SysProcAttr{CreationFlags: 0x08000000}
 	return cmd
 }
@@ -53,7 +57,7 @@ func parseWSLStatus() wslStatus {
 			vmpFeatureEnabled: false,
 			wslFeatureEnabled: false,
 		}
-		cmd := SilentExecCmd("wsl", "--status")
+		cmd := SilentExecCmd("--status")
 		out, err := cmd.StdoutPipe()
 		cmd.Stderr = nil
 		if err != nil {
@@ -79,7 +83,7 @@ func IsWSLInstalled() bool {
 }
 
 func IsWSLStoreVersionInstalled() bool {
-	cmd := SilentExecCmd("wsl", "--version")
+	cmd := SilentExecCmd("--version")
 	cmd.Stdout = nil
 	cmd.Stderr = nil
 	if err := cmd.Run(); err != nil {
@@ -95,7 +99,7 @@ func matchOutputLine(output io.ReadCloser) wslStatus {
 		vmpFeatureEnabled: true,
 		wslFeatureEnabled: true,
 	}
-	scanner := bufio.NewScanner(transform.NewReader(output, unicode.UTF16(unicode.LittleEndian, unicode.UseBOM).NewDecoder()))
+	scanner := bufio.NewScanner(output)
 	for scanner.Scan() {
 		line := scanner.Text()
 		for _, match := range wslNotInstalledMessages {


Test patch:
diff --git a/pkg/machine/wsl/wutil/wutil_test.go b/pkg/machine/wsl/wutil/wutil_test.go
index be27edc1eeb..fe015a9d6e6 100644
--- a/pkg/machine/wsl/wutil/wutil_test.go
+++ b/pkg/machine/wsl/wutil/wutil_test.go
@@ -8,7 +8,6 @@ import (
 	"testing"
 
 	"github.com/stretchr/testify/assert"
-	"golang.org/x/text/encoding/unicode"
 )
 
 const (
@@ -136,11 +135,15 @@ func TestMatchOutputLine(t *testing.T) {
 	}
 	for _, tt := range tests {
 		t.Run(tt.winVariant, func(t *testing.T) {
-			encoder := unicode.UTF16(unicode.LittleEndian, unicode.UseBOM).NewEncoder()
-			encodedOutput, err := encoder.String(tt.statusOutput)
-			assert.Nil(t, err)
-			reader := io.NopCloser(strings.NewReader(encodedOutput))
+			reader := io.NopCloser(strings.NewReader(tt.statusOutput))
 			assert.Equal(t, tt.want, matchOutputLine(reader))
 		})
 	}
 }
+
+func TestNewWSLCommand(t *testing.T) {
+	cmd := NewWSLCommand("--status")
+	assert.Contains(t, cmd.Path, "wsl")
+	assert.Equal(t, []string{"--status"}, cmd.Args[1:])
+	assert.Contains(t, cmd.Env, "WSL_UTF8=1")
+}