unescape the value before setting LANG (#41103)

The value in locale files could be shell escaped. The old parsing logic sets the string directly. Which could set LANG to a wrong value like "en_US.UTF-8". This PR un-escapes the value string before setting it to the env LANG.

Feng Wang committed Aug 5, 2026 at 10:43 UTC a8902efb6d4939872d0f7717a615365cc503f0ec
4 files changed +142 -6
src/linux/init/config.cpp
+4 -4
@@ -2616,9 +2616,8 @@ try
2616 // If the current line contains the "LANG=" string, update the
2617 // environment block with the remainder of the line.
2618 //
2619 - // N.B. No validation is done on the contents of the string. If the
2620 - // file contains multiple lines containing "LANG=" the last will
2621 - // be used.
2619 + // N.B. If the file contains multiple lines containing "LANG=" the last
2620 + // will be used.
2621 //
2622
2623 auto Content = strstr(Line, LANG_ENV "=");
@@ -2636,7 +2635,8 @@ try
2635 *SpecialCharacter = '\0';
2636 }
2637
2639 - Environment.AddVariable(LANG_ENV, Content);
2638 + const auto Value = wsl::shared::string::UnescapeShell(wsl::shared::string::Trim(std::string{Content}));
2639 + Environment.AddVariable(LANG_ENV, Value);
2640 }
2641 }
2642
src/shared/inc/stringshared.h
+101
@@ -1036,6 +1036,107 @@ inline std::wstring FormatBytes(uint64_t bytes)
1036 }
1037 }
1038
1039 +template <typename TChar>
1040 +inline std::basic_string<TChar> Trim(const std::basic_string<TChar>& input)
1041 +{
1042 + constexpr TChar whitespace[] = {TChar(' '), TChar('\t'), TChar('\n'), TChar('\r'), TChar('\f'), TChar('\v'), TChar('\0')};
1043 + const auto first = input.find_first_not_of(whitespace);
1044 + if (first == std::basic_string<TChar>::npos)
1045 + {
1046 + return {};
1047 + }
1048 +
1049 + const auto last = input.find_last_not_of(whitespace);
1050 + return input.substr(first, last - first + 1);
1051 +}
1052 +
1053 +template <typename TChar>
1054 +inline std::basic_string<TChar> UnescapeShell(const std::basic_string<TChar>& input)
1055 +{
1056 + enum class Quote
1057 + {
1058 + None,
1059 + Single,
1060 + Double
1061 + };
1062 +
1063 + Quote quote = Quote::None;
1064 + std::basic_string<TChar> output;
1065 + output.reserve(input.size());
1066 +
1067 + for (size_t index = 0; index < input.size(); index += 1)
1068 + {
1069 + const auto current = input[index];
1070 + if (quote == Quote::Single)
1071 + {
1072 + if (current == TChar('\''))
1073 + {
1074 + quote = Quote::None;
1075 + }
1076 + else
1077 + {
1078 + output.push_back(current);
1079 + }
1080 +
1081 + continue;
1082 + }
1083 +
1084 + if (current == TChar('\''))
1085 + {
1086 + if (quote == Quote::Double)
1087 + {
1088 + output.push_back(current);
1089 + }
1090 + else
1091 + {
1092 + quote = Quote::Single;
1093 + }
1094 +
1095 + continue;
1096 + }
1097 +
1098 + if (current == TChar('"'))
1099 + {
1100 + quote = (quote == Quote::Double) ? Quote::None : Quote::Double;
1101 + continue;
1102 + }
1103 +
1104 + if (current != TChar('\\'))
1105 + {
1106 + output.push_back(current);
1107 + continue;
1108 + }
1109 +
1110 + if (++index == input.size())
1111 + {
1112 + // return the original string if the escape is invalid.
1113 + return input;
1114 + }
1115 +
1116 + const auto escaped = input[index];
1117 + if (quote == Quote::None)
1118 + {
1119 + // "\\\n" out of escape means continue the line.
1120 + if (escaped != TChar('\n'))
1121 + {
1122 + output.push_back(escaped);
1123 + }
1124 + }
1125 + else if (escaped == TChar('"') || escaped == TChar('\\') || escaped == TChar('$') || escaped == TChar('`'))
1126 + {
1127 + output.push_back(escaped);
1128 + }
1129 + else if (escaped != TChar('\n'))
1130 + {
1131 + output.push_back(current);
1132 + output.push_back(escaped);
1133 + }
1134 + }
1135 +
1136 + // return the original string if the escape is invalid.
1137 + return (quote == Quote::None) ? output : input;
1138 +}
1139 +
1140 } // namespace wsl::shared::string
1141
1142 template <>
test/windows/SimpleTests.cpp
+35
@@ -289,6 +289,41 @@ class SimpleTests
289 auto upperCaseGuidStringWide = wideGuidStringNoBraces;
290 std::transform(upperCaseGuidStringWide.begin(), upperCaseGuidStringWide.end(), upperCaseGuidStringWide.begin(), toupper);
291 VERIFY_ARE_EQUAL(upperCaseGuidStringWide, wsl::shared::string::GuidToString<wchar_t>(guid, wsl::shared::string::GuidToStringFlags::Uppercase));
292 +
293 + VERIFY_ARE_EQUAL(wsl::shared::string::Trim(std::string{" \tvalue\r\n"}), std::string{"value"});
294 + VERIFY_ARE_EQUAL(wsl::shared::string::Trim(std::string{" \t\r\n"}), std::string{});
295 + VERIFY_ARE_EQUAL(wsl::shared::string::Trim(std::wstring{L" \tvalue\r\n"}), std::wstring{L"value"});
296 +
297 + const std::vector<std::pair<std::string, std::string>> shellStrings{
298 + {"", ""},
299 + {"plain", "plain"},
300 + {"\"\"", ""},
301 + {"''", ""},
302 + {"\"double quoted\"", "double quoted"},
303 + {"'single quoted'", "single quoted"},
304 + {"one' two'\" three\"", "one two three"},
305 + {"escaped\\ value", "escaped value"},
306 + {"escaped\\q", "escapedq"},
307 + {"escaped\\'quote", "escaped'quote"},
308 + {"abc\\\nedf", "abcedf"},
309 + {"\"abc\\\nedf\"", "abcedf"},
310 + {"'abc\\\nedf'", "abc\\\nedf"},
311 + {"\"escaped \\\"quote\\\" and \\\\ slash\"", "escaped \"quote\" and \\ slash"},
312 + {"\"escaped \\$dollar and \\`backtick\"", "escaped $dollar and `backtick"},
313 + {"\"literal \\q\"", "literal \\q"},
314 + {"'literal \\ value'", "literal \\ value"},
315 + {"unterminated'", "unterminated'"},
316 + {"\"unterminated", "\"unterminated"},
317 + {"trailing\\", "trailing\\"},
318 + {"\"trailing\\", "\"trailing\\"}};
319 +
320 + for (const auto& [input, expected] : shellStrings)
321 + {
322 + VERIFY_ARE_EQUAL(wsl::shared::string::UnescapeShell(input), expected);
323 + VERIFY_ARE_EQUAL(
324 + wsl::shared::string::UnescapeShell(wsl::shared::string::MultiByteToWide(input)),
325 + wsl::shared::string::MultiByteToWide(expected));
326 + }
327 }
328
329 TEST_METHOD(WindowsPathWithSpaces)
test/windows/UnitTests.cpp
+2 -2
@@ -571,7 +571,7 @@ class UnitTests
571 DistroFileChange defaultLocale(L"/etc/default/locale", LxsstuLaunchWsl(L"test -f /etc/default/locale") == 0);
572 DistroFileChange localeConf(L"/etc/locale.conf", LxsstuLaunchWsl(L"test -f /etc/locale.conf") == 0);
573
574 - const auto readLang = []() { return LxsstuLaunchWslAndCaptureOutput(L"echo $LANG").first; };
574 + const auto readLang = []() { return LxsstuLaunchWslAndCaptureOutput(L"printenv LANG").first; };
575
576 // Only /etc/default/locale is present (Debian/Ubuntu).
577 {
@@ -586,7 +586,7 @@ class UnitTests
586 {
587 defaultLocale.Delete();
588 localeConf.Delete();
589 - localeConf.SetContent(L"LANG=fr_FR.UTF-8\n");
589 + localeConf.SetContent(L"LANG=\"fr_FR.UTF-8\"\n");
590 TerminateDistribution();
591 VERIFY_ARE_EQUAL(readLang(), L"fr_FR.UTF-8\n");
592 }