Skip to content

Conversation

@erights
Copy link
Contributor

@erights erights commented Sep 12, 2025

Closes: #2951
Refs: #1798 , https://github.com/tc39/proposal-error-capturestacktrace , https://github.com/tc39/proposal-error-stacks , https://github.com/tc39/proposal-error-stack-accessor

Description

(Description mostly from Copilot's overview below, which is quite good!)

This PR's error taming implements stronger platform detection for v8, addressing issue #2951. The change improves the reliability of v8 detection by testing the prepareStackTrace mechanism, in addition to checking for the presence of captureStackTrace.

This is needed because some non-v8 platforms have implemented captureStackTrace without prepareStackTrace. The https://github.com/tc39/proposal-error-capturestacktrace proposal would make it standard, eventually causing all conformant JS engines to implement it.

A verbally reported bug motivated this, even though that bug itself may be a false alarm. The report raised the possibility that our error-taming of treating the Hermes platform as if it was v8, and then misbehaving because it does not act like v8. Whether or not this actually happens on Hermes, it is plausible and would happen elsewhere with ses prior to this PR.

Security Considerations

If non-v8 engines start implementing the prepareStackTrace mechanism, we may need to tighten the sniff test once again. Such is the nature of sniff tests. Since we do not yet know the specifics of such possible additions, it does not yet make sense to tighten the sniff test, unless I'm missing some we should sniff instead.

During such a transition period, it is possible that the falsly-triggered v8 error taming could misbehave, including in ways that may be exploitable.

Scaling Considerations

none

Documentation Considerations

This is a bug fix and does not need documentation beyond the issue, this PR, and the inline comments added by this PR.

Testing Considerations

  • I did not add automated tests, because I am unclear on how automated tests would test this. Reviewers: suggestions welcome.

  • What I did do is manually step through the logic in the vscode debugger while running on Node. The sniff test code worked as expected there. If we do rely on such manual testing, I should at least test on some non-v8 platform that does implement captureStackTrace.

Compatibility Considerations

See "Security Considerations" above.

Upgrade Considerations

none

@erights erights self-assigned this Sep 12, 2025
@erights erights requested a review from Copilot September 12, 2025 00:25
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR implements stronger platform detection specifically for v8 JavaScript engine in the SES error constructor taming functionality, addressing issue #2951. The change improves the reliability of v8 detection by testing the prepareStackTrace mechanism rather than just checking for the presence of captureStackTrace.

Key changes:

  • Enhanced v8 detection logic using prepareStackTrace functionality testing
  • Added import for isArray utility function
  • Replaced simple function existence check with comprehensive platform sniffing

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

@erights erights force-pushed the markm-zb-tighter-v8-error-sniff branch 2 times, most recently from 6c7e4b6 to 3630c30 Compare September 12, 2025 00:34
@erights erights marked this pull request as ready for review September 12, 2025 00:57
@erights erights added debugging support taming compat tc39 tooling devex developer experience fidelity Pertaining to the fidelity of emulation of native Hardened JavaScript. metamask labels Sep 12, 2025
@erights erights requested a review from Copilot September 12, 2025 01:04
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated no new comments.


Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

@naugtur
Copy link
Member

naugtur commented Sep 15, 2025

Get ready, this will hurt.

Hermes does implement Error.prepareStackTrace and calls it with error and CallSite array

CallSite implementation is incomplete, mainly because Hermes doesn't have a concept of source files.

(...) have been added, full paths are present generally

Result of calling all V8 CallSite methods:

method return type return value
getColumnNumber number 3
getEnclosingColumnNumber number 1
getEnclosingLineNumber number 41
getEvalOrigin undefined
getFileName string file:///(...)endo/packages/ses/test/hermes/prepare-stack.js
getFunction undefined
getFunctionName string myfunction
getLineNumber number 42
getMethodName object
getPosition number 965
getPromiseIndex object
getScriptNameOrSourceURL string file:///(...)endo/packages/ses/test/hermes/prepare-stack.js
getScriptHash string 8ce5e9bc697fd7bb9acae06a235f27efab97fdb82378433b06a63ffe38c8303b
getThis undefined
getTypeName object
isAsync boolean false
isConstructor boolean false
isEval boolean false
isNative boolean false
isPromiseAll boolean false
isToplevel boolean true
toString string myfunction (file:///(...)endo/packages/ses/test/hermes/prepare-stack.js:42:3)

Result of calling all the same methods in Hermes

method return type return value
getColumnNumber number 8
getEnclosingColumnNumber Error undefined is not a function
getEnclosingLineNumber Error undefined is not a function
getEvalOrigin object
getFileName string /(...)endo/packages/ses/test/hermes/prepare-stack.js
getFunction undefined
getFunctionName string myfunction
getLineNumber number 42
getMethodName object
getPosition Error undefined is not a function
getPromiseIndex object
getScriptNameOrSourceURL Error undefined is not a function
getScriptHash Error undefined is not a function
getThis undefined
getTypeName object
isAsync boolean false
isConstructor object
isEval object
isNative boolean false
isPromiseAll boolean false
isToplevel object
toString string [object CallSite]

We could either make sniffing even stronger by testing for usefulness of toString
or we could keep the V8 detection in a state that also covers hermes (might need to rename some things?) and add a fallback for broken toString

@naugtur
Copy link
Member

naugtur commented Sep 15, 2025

I've pushed two commits:

  1. test runner for hermes with some tests covering the smoke test and known issues
  2. a potential fallback for repareStackTrace CallSite toString method being broken

@naugtur naugtur force-pushed the markm-zb-tighter-v8-error-sniff branch 3 times, most recently from 41009a8 to 0fca5fc Compare September 15, 2025 14:09
;;;;
var TEST;
function test(name, fn) {
Copy link
Member

@naugtur naugtur Sep 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks to the test going in a function I can start the concatenation with it, so if errors are thrown, the line numbers will be correct. SES gets concatenated after but the test runs synchronously after SES still.

// like v8.
platform = 'v8';

if (`${sst[0]}` === '[object CallSite]') {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here we detect Hermes providing a broken toString

uncurryThis(csProto.getColumnNumber),
];

callSiteToStringFallback = callSite =>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of creating it and passing around, we could instead repair the original, but I'm afraid someone else might be doing similar feature detection

// Error.captureStackTrace = null

/* global test */
test('error taming unsafe', () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the one that fails without my toStringFallback

@@ -0,0 +1,92 @@
/* global test */
test('known issue: CallSite implementation', () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

full test documenting broken callsite implementation

@@ -0,0 +1,36 @@
/* global test */
/* global repairIntrinsics */
test('knonw issue: Hermes Promise is non-standard', () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test documents the broken promise implementation and verifies that the fix we're applying in lavamoat still works.

Are we interested in adding the fix to ses-hermes? I think it'd make sense - Promise constructors will throw often if not fixed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where can I find this fix?

Copy link
Member

@naugtur naugtur Sep 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fix is demonstrated here (capturing all truthy underscore-prefixed fields from Promise and preserving them till after repair to bring them back)
It's used in @lavamoat/react-native-lockdown but I think if we claim SES works under Hermes, we should consider adding this exception to the hermes build by default.

Another awful detail - the field that is exposed and must exist or the Promie implementation falls apart is containing a no-op function that the implementation reuses in a few places, which could have been a local scope function. Other fields, null by default, allow instrumenting all promises, so good riddance.

@erights
Copy link
Contributor Author

erights commented Sep 15, 2025

Get ready, this will hurt.

Thanks for the warning. It did indeed hurt :/

Comment on lines +51 to +56
if (typeof originalPrepareStackTrace === 'function') {
// This case should not occur on v8 or any other platform.
// But if it does, we assume we're on v8, or a platform whose
// error stack logic is close enough that we can treat it
// like v8.
platform = 'v8';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe it does happen in Node.js according to the realm init logic I'm seeing: https://github.com/nodejs/node/blob/b8870c4a61f9c4c4490e43472e98655b00055359/lib/internal/bootstrap/realm.js#L444-L470

FYI that logic shows that it's not v8 that calls Error.prepareStackTrace but the embedder that sets a callback (in this case setPrepareStackTraceCallback), which itself does the forwarding to Error.prepareStackTrace (or some other fallback)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Huh. Testing just now, it was added sometime between Node v18.19.0 exclusive and Node v20.18.3 inclusive.

@erights
Copy link
Contributor Author

erights commented Sep 15, 2025

potential fallback for repareStackTrace CallSite toString method being broken

Btw, given the topic, I first read this as "repairStackTrace" and became unreasonably hopeful for a moment ;) .

Copy link
Contributor

@mhofman mhofman left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given the new modular design of prepareStackTrace in Node.js introduced in nodejs/node#50827, I think we should at least restore (or not touch?) the original prepareStackTrace if the errorTaming mode is 'unsafe'.

Also, since this is so related, maybe we could attempt to leverage the original prepareStackTrace to build the stack string (see #1798), but happy to have this as a follow up PR.

(@mhofman , restoring your original. Somehow when I thought I was replying to it, I accidentally revised it instead. In any case, I found a better place for the reply, above.)

@erights erights force-pushed the markm-zb-tighter-v8-error-sniff branch from 0fca5fc to 6fb1c1c Compare September 16, 2025 06:56
@erights erights requested a review from gibson042 September 16, 2025 08:16
@erights erights force-pushed the markm-zb-tighter-v8-error-sniff branch 2 times, most recently from fb86608 to bd4fcbc Compare September 19, 2025 00:43
@erights erights force-pushed the markm-zb-tighter-v8-error-sniff branch from bd4fcbc to bad2617 Compare October 7, 2025 23:41
@erights
Copy link
Contributor Author

erights commented Oct 7, 2025

@naugtur , since the problematic case is Hermes, and since you've contributed so much of this PR anyway, should you take it from here?

Github will allow me to assign you. But due to the fact that I started the PR, Github won't let me review. So if you do take it from here, just informally consider me a reviewer. Thanks!

@erights erights requested a review from naugtur October 7, 2025 23:44
@naugtur
Copy link
Member

naugtur commented Oct 8, 2025

Happy to take over, but may need some help with specifying the desired scope / what's left to do.

@mhofman, care to elaborate on the comment about who calls prepareStackTrace? How does it affect what we should do here?

If Node is doing things, should I expect the behavior to differ between Node and Chrome?

@naugtur naugtur self-assigned this Oct 8, 2025
@mhofman
Copy link
Contributor

mhofman commented Oct 9, 2025

@mhofman, care to elaborate on the comment about who calls prepareStackTrace? How does it affect what we should do here?

If Node is doing things, should I expect the behavior to differ between Node and Chrome?

My understanding is that v8 itself only implements a mechanism so that the host can specify the callback to use to prepare stack traces, and Chrome and v8 both register and internal callback (setPrepareStackTraceCallback(prepareStackTraceCallback) in the case of Node.js) that ends up calling whatever function is at Error.prepareStackTrace if it exists.

Node.js further has an internal default behavior for prepareStackTrace that depends on whether source maps are enabled or not. That internal behavior is now exposed as a "proxy" function set as the initial value for Error.prepareStackTrace. That means that the comment currently saying "This case should not occur on v8 or any other platform" is in fact inaccurate.

Instead of bailing out and assume v8, we may want to still test behavior when Error.prepareStackTrace is called to check whether it's compliant, even if we find an existing Error.prepareStackTrace. That means we need to restore whatever we found there in the first place after the test if we don't end up applying our own (e.g. unsafe errors).

Furthermore I recommend that we go the extra mile, save the original Error.prepareStackTrace if it exists, and rely on it to generate the stack string (from the filtered stack) in our replacement, so that any source mapping that Node.js does is still applied.

@naugtur naugtur added lavamoat and removed metamask labels Oct 23, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

debugging support devex developer experience fidelity Pertaining to the fidelity of emulation of native Hardened JavaScript. lavamoat taming compat tc39 tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ses v8 sniff test is too weak to exclude very-non-v8 platforms

4 participants