diff --git a/src/OpenClaw.SetupEngine/SetupContext.cs b/src/OpenClaw.SetupEngine/SetupContext.cs index 5c5f5ef05..32b1118bc 100644 --- a/src/OpenClaw.SetupEngine/SetupContext.cs +++ b/src/OpenClaw.SetupEngine/SetupContext.cs @@ -173,7 +173,7 @@ public sealed class LocalAiConfig public sealed class WslConfig { private static readonly System.Text.RegularExpressions.Regex s_linuxUserNamePattern = - new("^[a-z_][a-z0-9_-]{0,31}$", System.Text.RegularExpressions.RegexOptions.Compiled); + new(@"\A[a-z_][a-z0-9_-]{0,31}\z", System.Text.RegularExpressions.RegexOptions.Compiled); public string User { get; set; } = "openclaw"; public bool Systemd { get; set; } = true; @@ -185,8 +185,9 @@ public sealed class WslConfig public string? Memory { get; set; } public string? Swap { get; set; } - public static bool IsValidLinuxUserName(string value) - => s_linuxUserNamePattern.IsMatch(value); + public static bool IsValidLinuxUserName(string? value) + => !string.IsNullOrWhiteSpace(value) + && s_linuxUserNamePattern.IsMatch(value); } // ─── Gateway Configuration ─── diff --git a/src/OpenClaw.SetupEngine/SetupWizardRunner.cs b/src/OpenClaw.SetupEngine/SetupWizardRunner.cs index c7ecc7bb8..44fbcdcd9 100644 --- a/src/OpenClaw.SetupEngine/SetupWizardRunner.cs +++ b/src/OpenClaw.SetupEngine/SetupWizardRunner.cs @@ -120,6 +120,10 @@ internal static bool ShouldReplaceOperatorDeviceId( internal async Task SuspendReloadModeAsync() { + var invalidUser = RejectInvalidLinuxUser(); + if (invalidUser is not null) + return invalidUser; + try { // PATH prefix references $PATH. Pipe the script so wsl.exe cannot expand it on argv. @@ -128,6 +132,7 @@ internal async Task SuspendReloadModeAsync() $"{_ctx.WslPathPrefix} && openclaw config set gateway.reload.mode off", TimeSpan.FromSeconds(15), // This bounded handoff must finish so we know whether restoration is required. + // Stdin keeps the username and $PATH out of the wsl.exe argument vector. ct: CancellationToken.None, inputViaStdin: true); if (result.ExitCode != 0) @@ -154,6 +159,10 @@ internal async Task SuspendReloadModeAsync() private async Task RunCoreAsync(CancellationToken ct) { + var invalidUser = RejectInvalidLinuxUser(); + if (invalidUser is not null) + return invalidUser; + var registry = new GatewayRegistry(_ctx.DataDir, logger: new SetupOpenClawLogger(_ctx.Logger)); registry.Load(); @@ -645,6 +654,10 @@ private async Task TryCancelWizardAsync(OpenClawGatewayClient client, string ses internal async Task RestoreReloadModeAsync() { + var invalidUser = RejectInvalidLinuxUser(); + if (invalidUser is not null) + return invalidUser; + var reloadMode = ConfigureGatewayStep.GetEffectiveReloadMode(_ctx.Config.Gateway); try { @@ -823,6 +836,16 @@ private async Task RunReloadModeRestorationCommandAsync(string re } } + private StepResult? RejectInvalidLinuxUser() + { + var user = _ctx.Config.Wsl.User; + if (WslConfig.IsValidLinuxUserName(user)) + return null; + + return StepResult.Terminal( + $"Invalid WSL user '{user}'. Use a Linux username matching [a-z_][a-z0-9_-]{{0,31}}."); + } + internal static bool IsStartupMigrationLeaseContention(CommandResult result) => !result.TimedOut && result.ExitCode != 0 && (result.Stdout.Contains(StartupMigrationLeaseDiagnostic, StringComparison.Ordinal) diff --git a/tests/OpenClaw.SetupEngine.Tests/SetupStepsTests.cs b/tests/OpenClaw.SetupEngine.Tests/SetupStepsTests.cs index 00bfe8659..df32cf4bc 100644 --- a/tests/OpenClaw.SetupEngine.Tests/SetupStepsTests.cs +++ b/tests/OpenClaw.SetupEngine.Tests/SetupStepsTests.cs @@ -4014,6 +4014,7 @@ private static int GetFreeTcpPort() [InlineData("BadUser")] [InlineData("bad user")] [InlineData("bad$user")] + [InlineData("openclaw\n")] public async Task ConfigureWsl_RejectsInvalidLinuxUserName(string user) { var ctx = CreateContext(); @@ -4361,6 +4362,35 @@ var value when value.Contains("curl -s") => Ok("200"), "config set gateway.reload.mode off", commands.WslCalls[0].Command); Assert.Contains("curl -s", commands.WslCalls[1].Command); + Assert.True(commands.WslCalls[0].InputViaStdin); + } + + [Theory] + [InlineData(null)] + [InlineData("bad\"user")] + [InlineData("bad$(id)")] + [InlineData("openclaw\n")] + public async Task SetupWizard_RejectsInvalidLinuxUserBeforeAnyWslCommand(string? user) + { + var commands = new FakeCommandRunner( + _ => Ok(), + (_, _, _) => Fail("WSL must not run for an invalid Linux user")); + var ctx = CreateContext( + new SetupConfig { Wsl = new WslConfig { User = user! } }, + commands); + ctx.DistroName = "test-distro"; + + var suspend = await new SetupWizardRunner(ctx).SuspendReloadModeAsync(); + var restore = await new SetupWizardRunner(ctx).RestoreReloadModeAsync(); + var run = await new SetupWizardRunner(ctx).RunAsync(CancellationToken.None); + + Assert.Equal(StepOutcome.FailedTerminal, suspend.Outcome); + Assert.Equal(StepOutcome.FailedTerminal, restore.Outcome); + Assert.Equal(StepOutcome.FailedTerminal, run.Outcome); + Assert.Contains("Invalid WSL user", run.Message); + Assert.Contains("Invalid WSL user", suspend.Message); + Assert.Contains("Invalid WSL user", restore.Message); + Assert.Empty(commands.WslCalls); } [Fact] @@ -7147,7 +7177,8 @@ private static void AssertReloadRestorationCompleted( { Assert.Contains( commands.WslCalls, - call => call.Command.Contains("config set gateway.reload.mode 'hybrid'")); + call => call.Command.Contains("config set gateway.reload.mode 'hybrid'") + && call.InputViaStdin); Assert.Contains( commands.WslCalls, call => call.Command.Contains("openclaw gateway restart"));