escape: reject UTF-16 surrogates in \u escapes in cunescape_one() (#43079)

A `\u` escape in `cunescape_one()` was accepted for any 16-bit value
except NUL. That range `0x0000`-`0xFFFF` includes the UTF-16 surrogates
`0xD800`-`0xDFFF`, which are not valid Unicode scalar values and cannot
be encoded as valid UTF-8.
This commit is contained in:
Armaan Sandhu
2026-07-21 20:10:26 +05:30
committed by GitHub
parent cc594a83f4
commit eb03267070
2 changed files with 22 additions and 0 deletions

View File

@@ -202,6 +202,13 @@ int cunescape_one(const char *p, size_t length, char32_t *ret, bool *eight_bit,
if (c == 0 && !accept_nul)
return -EINVAL;
/* Don't allow UTF-16 surrogates, they cannot be encoded as valid UTF-8. Note we
* deliberately do *not* use unichar_is_valid() here (unlike the \U case below):
* it also rejects noncharacters such as U+FFFE, which callers legitimately round-trip
* through \u (e.g. systemd.mount-extra= parsing, see test-fstab-generator). */
if (utf16_is_surrogate(c))
return -EINVAL;
*ret = c;
r = 5;
break;

View File

@@ -111,6 +111,21 @@ TEST(cunescape) {
ASSERT_STREQ(unescaped, "ßßΠA");
unescaped = mfree(unescaped);
/* UTF-16 surrogates cannot be encoded as valid UTF-8 and must be rejected */
ASSERT_ERROR(cunescape("\\ud800", 0, &unescaped), EINVAL);
ASSERT_ERROR(cunescape("\\udfff", 0, &unescaped), EINVAL);
/* The code points immediately outside the surrogate range must still decode */
ASSERT_OK(cunescape("\\ud7ff", 0, &unescaped));
unescaped = mfree(unescaped);
ASSERT_OK(cunescape("\\ue000", 0, &unescaped));
unescaped = mfree(unescaped);
/* Noncharacters (e.g. U+FFFE) are valid scalar values, encode fine as UTF-8, and are
* relied upon by callers (systemd.mount-extra=), so unlike \U they stay accepted here */
ASSERT_OK(cunescape("\\ufffe", 0, &unescaped));
unescaped = mfree(unescaped);
assert_se(cunescape("\\073", 0, &unescaped) >= 0);
ASSERT_STREQ(unescaped, ";");
unescaped = mfree(unescaped);