Skip to content

SECURITY: fix side-channel in V8 discovered by @yayuwang0624 - #9

Merged
soatok merged 3 commits into
masterfrom
security-fox
Sep 10, 2026
Merged

soatok merged 3 commits into
masterfrom
security-fox

Conversation

@soatok

@soatok soatok commented Sep 10, 2026 •

Copy link
Copy Markdown
Owner

Vulnerability discovered and reported by Yayu Wang. Research done by Yayu Wang, Kjell Dankert, Duy Kha Dinh, and Aastha Mehta, University of British Columbia.

From the initial disclosure email (converted to Markdown):

I hope you are doing well. My name is Yayu Wang, a PhD student at the University of British Columbia. I am reaching out, together with my colleague Kjell Dankert, our postdoctoral researcher Kha Dinh, and our supervisor Aastha Mehta (all CCed), to disclose a cache side-channel issue we observed in constant-time-js.

We acknowledge that the project is a proof of concept and explicitly advises against production use. Even with these limitations in mind, we believe that reporting this issue is still responsible, particularly because the project is intended to demonstrate constant-time programming techniques in JavaScript. We provide a description of the issue and its potential impact below.

1. Summary

The conditional-selection functions in constant-time-js use branchless JavaScript expressions to construct selection masks. We found that select, select_alt, and select_ints nevertheless produce secret-dependent instruction- and data-cache behavior in V8. The leakage arises from V8's ToBoolean mechanism, its different handling of -0 and -1, and direct accesses to the raw true and false Oddball objects on different cache lines. The resulting cache-access patterns can reveal the value of the selection condition.

2. Details

We verified the issue in constant-time-js 0.4.0 at commit bfbbc6f153fe3ceec09a7ece8a90003f091ac662, using V8 13.1.201.22 on x86-64.

The affected mask constructions appear in select, select_alt, and select_ints:

// select(returnLeft, left, right)  
const mask: number = (-!!returnLeft) & 0xff;

// select_alt(choice, m, n)  
const mask = (-choice) & 0xff;

// select_ints(returnLeft, left, right)  
const mask: number = (-(returnLeft & 1)) & 0xfffffffff;

For select, we observed that the !!returnLeft conversion reaches V8's Builtins_ToBoolean. The true and false cases execute instructions at different addresses within the builtin.

We separately used hardware read watchpoints to measure the data addresses accessed while V8 converts the boolean condition. This confirmed direct reads from the raw true and false Oddball objects in V8's read-only heap:

condition object base to_number field containing 64-byte line
true 0x...0c8 0x...0cc 0x...0c0
false 0x...0ac 0x...0b0 0x...080

The conversion executes loads of the following form, with rdi pointing to the condition's Oddball object:

movl   rdx, [rdi - 0x1]  // load the Oddball's map  
vmovsd xmm0, [rdi + 0x3] // load the Oddball's to_number field

Thus, the same load instruction accesses 0x...0cc for true and 0x...0b0 for false. The instruction address can be identical while its data operand reveals the condition through two different cache lines. This means that an executed-instruction trace alone can incorrectly classify the operation as constant-time; both instruction and data addresses must be considered.

For select_alt and select_ints, unary negation creates -0 when the choice is zero and -1 when it is one. -1 can be stored as a tagged small integer, while -0 must be materialized as a heap-allocated number. Consequently, the two choices take different paths through V8's negation and bitwise-operation handlers.

We compared calls using the two possible condition values by recording the executed instruction addresses and their corresponding 64-byte cache lines. The principal results were:

mask construction Ignition JIT code (TurboFan) only JIT code + builtins
(-!!cond) & 0xff differs identical differs
-(cond & 1) differs identical identical
0 - (cond & 1), numeric 0=/=1 identical identical identical

A mitigation that produced identical traces in our tests, but that we have not verified more broadly, is to accept a numeric 0 or 1 choice and construct the mask by subtracting from positive zero:

const mask = (0 - (choice & 1)) & 0xff;

3. Impact

This is a cache-based side-channel issue that primarily affects shared hardware environments. An attacker running on the same physical machine as the victim, such as in a co-located cloud VM or container, may be able to distinguish the instruction- or data-cache activity associated with the two selection values.

We also have Flush+Reload and Prime+Probe attack PoCs against a Curve25519 ECDH implementation that uses the constant-time-js selection pattern.

4. Further Discussion

We would be happy to provide additional technical details, PoC code, or raw execution traces, and to discuss potential mitigations if needed. Please let us know how you would like to proceed.

Thank you very much for your time and consideration.

Best regards,

Yayu

I'll have more to say about this shortly, but I want to get the fix in and tagged first.

@soatok

soatok commented Sep 10, 2026

Copy link
Copy Markdown
Owner Author

Ah, of course the CI is broken. I should fix that too!

@soatok
soatok merged commit a7a7476 into master Sep 10, 2026
3 checks passed
@soatok
soatok deleted the security-fox branch September 10, 2026 19:05
@soatok

soatok commented Sep 10, 2026

Copy link
Copy Markdown
Owner Author

@soatok

soatok commented Sep 12, 2026

Copy link
Copy Markdown
Owner Author

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant