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
}