8000 V8 runtime rejects Unicode property escapes (`\p{...}`) because V8 is built without ICU · Issue #1195 · frida/frida-gum · GitHub
[go: up one dir, main page]

Skip to content
10000

V8 runtime rejects Unicode property escapes (\p{...}) because V8 is built without ICU #1195

Open

Description

@leonitousconforti

The V8 runtime fails to compile any script containing a Unicode property escape, while QuickJS accepts the same code:

SyntaxError: Invalid regular expression: /[%[\]/\s\p{Cc}]/u: Invalid property name in character class

This now affects common npm packages bundled into agents:

  • effect 4.0.5 added /[%[\]/\s\p{Cc}]/u in effect/net/NetAddress, so any agent that bundles Effect's RPC or socket modules fails to compile.
  • vitest 5.x uses /\$([$\p{ID_Continue}.]+)/gu in its test-title formatting, which breaks running vitest inside an agent.

Both are ordinary ES2018 syntax, so this will keep turning up as packages adopt it.

Repro (Frida 17.24.3, Linux x86_64, Node 26):

const frida = require("frida");

const device = await frida.getLocalDevice();
const pid = await device.spawn(["/usr/bin/sleep", "infinity"]);
const session = await device.attach(pid);

for (const runtime of ["qjs", "v8"]) {
    try {
        const script = await session.createScript(String.raw`send(/\p{L}/u.test("a"))`, { runtime });
        await script.load();
        console.log(runtime, "ok");
    } catch (e) {
        console.log(runtime, e.message);
    }
}
qjs ok
v8 Script(line 1): SyntaxError: Invalid regular expression: /\p{L}/u: Invalid property name

The result was the same on 17.22.2 (V8 10.x) and 17.24.3 (V8 15.7).

Cause

meson_options.txt in frida/v8 has i18n disabled by default (L79-L83). Without V8_INTL_SUPPORT, ParsePropertyClassName and AddPropertyClassRange are stubs that always fail (regexp-parser.cc#L2572-L2588).

Possible fixes

  1. Build V8 with ICU. The meson build already handles the Intl sources and Torque files under i18n. What's missing is an ICU dependency in releng (WrapDB's icu 78.2 port matches the ICU version V8 15.7 pins), ICU_UTIL_DATA_STATIC, and an ICU init call in gumv8platform.cpp. This also brings Intl, localeCompare and toLocaleString with locale support. The obvious costs are binary size and the chance of ICU symbols clashing with targets that load their own ICU.
  2. Support \p{...} without ICU. Fill in the two non-ICU stubs using compact Unicode property tables, for example the ones QuickJS's libunicode already ships. That fixes regex only, with a much smaller size cost, but it's a patch to carry in the V8 fork.

Is either direction something you'd take? I'm happy to work on it and can bring size numbers before opening PRs. Was disabling i18n mainly about size, or are there other constraints I should design around?

Activity

  1. leonitousconforti commented on Oct 30, 2026

    @leonitousconforti
    Author

    I prototyped both directions to get real numbers. Setup: frida-gum d24632b, frida/v8 f6d7f661 (V8 15.7), Linux x86_64, GCC 15, Frida's own configure flags (-Os, --gc-sections), prebuilt SDK for everything except V8 and ICU. Sizes are of a stripped executable that links frida-gum and GumJS with both runtimes.

    Size

    Variant Stripped size added (baseline 31.4 MB)
    libunicode in the non-ICU stubs +53 KB (+0.17%)
    ICU 78.2, root-only locale data +6.4 MB (+20%)
    ICU 78.2, English-only data +6.5 MB (+21%)
    ICU 78.2, all locales +28.8 MB (+92%)
    ICU 78.2, full WrapDB data +35.8 MB (+114%)

    ICU's code alone is about 2.7 MB (icui18n 1.1 MB, icuuc 0.9 MB, V8's Intl objects 0.3 MB) no matter how much data is kept. The trimmed data sets were made with icupkg, so they are upper bounds.

    Correctness (test262 built-ins/RegExp plus the related String.prototype directories, 2,016 files, run through the GumJS script backend)

    V8 today libunicode ICU QuickJS
    RegExp/property-escapes (613) 163 613 613 613
    RegExp/unicodeSets (114) 48 114 114 114
    All 2,016 1457 1978 2000 1999

    The libunicode build regresses nothing. What it still fails compared to ICU are the other existing non-ICU gaps: /ui folding of ſ, the Kelvin sign and astral letters, normalize() being a no-op, astral identifiers and group names, and astral case mapping.

    libunicode prototype: it only replaces the two #else stubs in regexp-parser.cc (+429/-4) and mirrors the ICU path: General_Category, Script and Script_Extensions values with their aliases, the same binary property allowlist V8 uses with ICU, Any/ASCII/Assigned, the /v properties of strings, and /vi case closure before negation. libunicode (Unicode 17, same as ICU 78) is vendored unmodified and compiled with its symbols renamed, so it cannot clash with QuickJS's copy. Compile and match times are on par with ICU.

    ICU prototype: this took more than the issue suggested:

    • The i18n option also needs the gen-regexp-special-case step (BUILD.gn generates special-case.cc with ICU at build time) and ICU_UTIL_DATA_IMPL=ICU_UTIL_DATA_STATIC.
    • WrapDB's ICU in static mode links icuuc against a stub data library that defines the same icudt78_dat symbol as the real one, so link order decides which wins. I embedded the data from V8's own build instead.
    • Cross builds would need an ICU for the build machine to run the generator. I have not wired that up.
    • It changes runtime behavior in the host process. V8 computes local time through ICU, and Date.prototype.toString() zone names change from (CST) to (Central Standard Time). ICU resolves the zone through /etc/localtime and can fall back to reading all of /usr/share/zoneinfo. Even with UDATA_NO_FILES it still probes $ICU_DATA and checks $ICU_TIMEZONE_FILES_DIR for tz files to load.

    Given that, I'd lean towards the libunicode approach for this issue and treat shipping Intl as a separate decision. Since the patch only touches the non-ICU branch, enabling ICU later would just bypass it. Would you take a PR to frida/v8 along those lines? Sharing QuickJS's libunicode instead of vendoring a second copy would also save 33 KB when both runtimes are built, if you'd prefer that.

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

      0