Skip to content

Runner crashes at InitializeSecretMasker on machines with Thai (th-TH) culture — culture-sensitive LastIndexOf in PowerShellPreAmpersandEscape #4686

Description

@vitbobhon

Runner crashes at InitializeSecretMasker on machines with Thai (th-TH) culture

Every job fails before the first step runs, on a self-hosted Windows runner whose culture is th-TH.

Environment

Runner version 2.337.0
OS Windows 11 Pro 10.0.22631, Get-WinSystemLocale = th-TH
Install interactive (run.cmd), not a service
Repo secrets none (total_count: 0) — so the masked values are the ones the service sends with the job message

Symptom

Job ends in ~4 seconds with steps = 0. The check-run annotation shows:

System.ArgumentOutOfRangeException: Index and length must refer to a location within the string. (Parameter 'length')
   at System.String.ThrowSubstringArgumentOutOfRange(Int32 startIndex, Int32 length)
   at System.String.Substring(Int32 startIndex, Int32 length)
   at GitHub.DistributedTask.Logging.ValueEncoders.PowerShellPreAmpersandEscape(String value)
   at GitHub.DistributedTask.Logging.SecretMasker.AddValue(String value)
   at GitHub.Runner.Worker.Worker.InitializeSecretMasker(AgentJobRequestMessage message)
   at GitHub.Runner.Worker.Worker.RunAsync(String pipeIn, String pipeOut)

_diag/Worker_*.log confirms the culture:

Worker] Version: 2.337.0
Worker] Culture: th-TH
Worker] UI Culture: en-US
Worker] Message received.
Worker] ERR  System.ArgumentOutOfRangeException ...

Root cause

src/Sdk/DTLogging/Logging/ValueEncoders.cs:

if (!string.IsNullOrEmpty(value) && value.Contains("&"))          // Contains(string) -> ORDINAL
{
    if (value.Contains("&+"))
        secretSection = value.Substring(0, value.IndexOf("&+") + "&+".Length);      // IndexOf(string) -> CULTURE-SENSITIVE
    else
        secretSection = value.Substring(0, value.LastIndexOf("&") + "&".Length);    // LastIndexOf(string) -> CULTURE-SENSITIVE
}

String.Contains(string) is ordinal, but String.IndexOf(string) / String.LastIndexOf(string) are culture-sensitive by default. Under the Thai collation, LastIndexOf("&") returns an index one past the last character, so Substring(0, index + 1) reads past the end.

Minimal repro (.NET 9, ICU)

using System.Globalization;

foreach (var name in new[] { "th-TH", "en-US", "" })
{
    CultureInfo.CurrentCulture = name == "" ? CultureInfo.InvariantCulture : new CultureInfo(name);
    var s = "abcdef&";                       // length 7
    Console.WriteLine($"{name,-6} LastIndexOf(\"&\") = {s.LastIndexOf("&")}");
}

Output:

th-TH  LastIndexOf("&") = 7      <-- == s.Length, so Substring(0, 8) throws
en-US  LastIndexOf("&") = 6
       LastIndexOf("&") = 6      (invariant)

Same result for "abc&def&" (returns 8 for a length-8 string) and for a SAS-style URL "https://x/y?sig=AA&" (returns 19 for a length-19 string).

Any masked value that contains & is enough to kill every job on such a machine — and the values the service sends with a job message routinely contain &.

Suggested fix

Use ordinal comparisons, matching the Contains calls that guard them:

value.IndexOf("&+", StringComparison.Ordinal)
value.LastIndexOf("&", StringComparison.Ordinal)

(Or value.LastIndexOf('&') — the char overloads are already ordinal.)

A defensive clamp on the computed length would also prevent a crash in the masker from taking down the whole job, since a masking failure should never be fatal.

Workaround for affected users

Start the runner with globalization forced to invariant so the culture-sensitive path behaves ordinally:

DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
DOTNET_SYSTEM_GLOBALIZATION_PREDEFINEDCULTURESONLY=false

Note this is inherited by job steps, which changes culture-dependent behaviour inside builds/tests — so it may need to be overridden per job (env: DOTNET_SYSTEM_GLOBALIZATION_INVARIANT: '0').

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions