Skip to content

Copy the $<name> group name before interning it in getSubstitution - #2255

Open
Medios-lgtm wants to merge 1 commit into
meta:static_hfrom
Medios-lgtm:regexp-substitution-group-name-copy
Open

Medios-lgtm wants to merge 1 commit into
meta:static_hfrom
Medios-lgtm:regexp-substitution-group-name-copy

Conversation

@Medios-lgtm

Copy link
Copy Markdown
Contributor

Summary

getSubstitution resolves a $<name> reference by slicing the replacement string and calling StringView::getUTF16Ref. For a UTF-16 replacement that call does not copy, it returns a raw pointer straight into the replacement StringPrimitive on the GC heap. That pointer is handed to IdentifierTable::getSymbolHandle, which, when the name is not already interned, allocates a long-lived DynamicStringPrimitive and only afterwards copies the characters from the caller's pointer. allocateDynamicString re-reads its input through primHandle after that allocation for exactly this reason, but a bare UTF16Ref gives it no handle to re-read, so a GC in the window moves the replacement string and the copy reads freed memory into the new identifier.

Any script reaches it through String.prototype.replace / replaceAll with a named-group RegExp, a UTF-16 replacement string, and a group name not yet interned, e.g. 'abc'.replace(new RegExp('(?<' + n + '>b)'), '[$<' + n + '>]'). In the default Hades config a long-lived allocation does not start a young-gen collection so the stale read usually survives; under HERMESVM_SANITIZE_HANDLES a collection runs on every allocation and it faults every time.

The fix copies the name into the SmallVector that is already declared, with appendUTF16String, before the lookup, the same shape Callable.cpp uses to build a bound function's name. getUTF16Ref has no other caller that feeds the result into an allocating function.

Test Plan

Added test/hermes/regexp-replace-named-group-gc.js, which runs under -gc-sanitize-handles=1.

Before the fix, built with -DHERMESVM_SANITIZE_HANDLES=ON -DHERMESVM_GCKIND=MALLOC -DHERMES_ENABLE_ADDRESS_SANITIZER=ON, the reproducer aborts:

==ERROR: AddressSanitizer: heap-use-after-free
READ of size 38 ... in __asan_memmove
  #1 IdentifierTable::allocateDynamicString<char16_t, true>(...)
  #3 IdentifierTable::getSymbolHandle(...)
  #4 hermes::vm::getSubstitution(...)
  #5 hermes::vm::regExpPrototypeSymbolReplace(...)
freed by thread T0 here:
  #1 MallocGC::collect(...)
  #3 GCBase::makeA<DynamicStringPrimitive<char16_t, true>, ... LongLived ...>
  #4 IdentifierTable::allocateDynamicString<char16_t, true>(...)

After the fix the same sanitize build prints the expected output and ASan is quiet:

named group replacement
a[b]c
<1>-<2>-<3>
xz

In the default ASan+Debug (Hades) build the new test passes and the full regexp lit subset is green (21 passed, 1 unsupported). clang-format --dry-run --Werror is clean on the changed file.

getSubstitution passed the result of StringView::getUTF16Ref, a raw
pointer into the replacement StringPrimitive, to
IdentifierTable::getSymbolHandle. Creating a new identifier allocates
before copying the name, so a GC in that window moved the replacement
string and the copy read freed memory. Copy the name into the local
SmallVector first.
@meta-cla meta-cla Bot added the CLA Signed Do not delete this pull request or issue due to inactivity. label Oct 18, 2026

This branch has not been deployed

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

Labels

CLA Signed Do not delete this pull request or issue due to inactivity.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant