Skip to content

Conversation

@kriskowal
Copy link
Member

@kriskowal kriskowal commented Oct 10, 2025

This change increases test coverage over lockdown with hardenTaming unsafe. The increase in test coverage revealed the need to relax certain cases, but moreof adjust passStyleOf to repair unfrozen V8 error objects in unsafe hardenTaming mode.

@kriskowal kriskowal requested a review from erights October 10, 2025 23:35
Comment on lines 111 to 114
// Error.prototype.stack is own accessor in V8, but nowhere else to our
// knowledge (2025).
// We relax validation for "stack" only when harden.isFake.
if (!(harden.isFake && propName === 'stack')) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I fail to see how unsafe harden is related to error stacks. I think what we possibly need to do is allow known "platform own stack accessors" in pass-style. It would be nice to only allow these if there was no lockdown, or if lockdown didn't tame errors.

Copy link
Member Author

Choose a reason for hiding this comment

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

I’ll think more next week about sensing the effects of error taming variants. I’ll probe your mind next week about “platform own stack accessors” because my understanding is that expectations about the identity or uniqueness of error own get and set may be evolving in a way that will make it harder to sense whether they are the platform’s intrinsic get and set. I fear we may truly need to leave a pinhole for stack in pass-style, generally, and simply be careful to discriminate them with isError and time we destructure all properties of an object.

@kriskowal kriskowal force-pushed the kriskowal-fix-harden-taming-unsafe branch from 8f28d6a to cd71d7d Compare October 11, 2025 03:10
@kriskowal kriskowal force-pushed the kriskowal-ses-ava-multi-config branch from 87d00cd to 342ac49 Compare October 11, 2025 03:10
@kriskowal kriskowal marked this pull request as draft October 21, 2025 18:00
@kriskowal
Copy link
Member Author

We’ve resolved to update this change such that passStyleOf will repair v8 error stack properties iff harden does not. This is a departure from the absolute purity of passStyleOf, but one necessary to sensibly continue supporting the noop mode of harden.

@kriskowal kriskowal force-pushed the kriskowal-ses-ava-multi-config branch 3 times, most recently from df3ba90 to 2b0119f Compare October 28, 2025 01:43
Base automatically changed from kriskowal-ses-ava-multi-config to master October 28, 2025 01:50
@kriskowal kriskowal force-pushed the kriskowal-fix-harden-taming-unsafe branch from cd71d7d to afa9989 Compare October 30, 2025 22:06
@kriskowal kriskowal requested a review from mhofman October 30, 2025 22:08
@kriskowal kriskowal marked this pull request as ready for review October 30, 2025 22:08
@kriskowal kriskowal force-pushed the kriskowal-fix-harden-taming-unsafe branch from afa9989 to 0fd4738 Compare October 30, 2025 22:15

const { getOwnPropertyDescriptors, getPrototypeOf } = Object;

// @ts-expect-error isFake is a secret.
Copy link
Contributor

Choose a reason for hiding this comment

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

WAT? If it is a secret, it is a very poorly kept secret!

(Please avoid ever seeming to make a security claim that we're not actually making.)

Copy link
Member Author

Choose a reason for hiding this comment

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

This is a pattern copied from elsewhere. It is not a secret in this sense. It is a secret in the sense that we do not make its existence evident in the type, in order to avoid burdening common users with undue knowledge of its existence. Recommendation welcome.

Copy link
Member Author

Choose a reason for hiding this comment

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

How about, “Existence of harden.isFake is not communicated by the types.”?

makeExo,
} from '../index.js';

// @ts-expect-error isFake is a secret property.
Copy link
Contributor

Choose a reason for hiding this comment

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

again

return obj;
},
tortuous(hardA, softB, hardC, optHardD, optSoftE = {}) {
// Recall that isFrozen lies with hardenTaming: unsafe
Copy link
Contributor

Choose a reason for hiding this comment

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

Aren't you fixing that? We should at least vehemently deprecate the existing unsafe-fast.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, I expect this to change to accommodate the new mode. We may need to expose the real isFrozen somewhere to make this test work in every mode.

Comment on lines +24 to +25
// We do not trust isFrozen because lockdown with unsafe hardenTaming replaces
// isFrozen with a version that is in cahoots with fake harden.
Copy link
Contributor

Choose a reason for hiding this comment

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

This is the unsafe-fast case that we're at least deprecating and hopefully removing soon, yes?

}

const er1StackDesc = getOwnPropertyDescriptor(Error('er1'), 'stack');
const er2StackDesc = getOwnPropertyDescriptor(TypeError('er2'), 'stack');
Copy link
Contributor

Choose a reason for hiding this comment

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

Just curious: Do you have a particular case in mind that you're defending against with the "redundant" check?

Copy link
Member Author

Choose a reason for hiding this comment

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

I don’t. This is copied from SES.

Comment on lines 39 to 96
if (
er1StackDesc === undefined ||
er2StackDesc === undefined ||
er1StackDesc.get === undefined
) {
Copy link
Contributor

Choose a reason for hiding this comment

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

The fact that you wrote || means I'm confused what the purpose of the "redundant" check is. I expected something more like

Suggested change
if (
er1StackDesc === undefined ||
er2StackDesc === undefined ||
er1StackDesc.get === undefined
) {
if (
(er1StackDesc === undefined &&
er2StackDesc === undefined) ||
er1StackDesc.get === undefined
) {

return undefined;
}

// We should only encounter this case on v8 because of its problematic
Copy link
Contributor

Choose a reason for hiding this comment

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

And the JS engine MetaMask is using?

Comment on lines 53 to 56
// In the v8 case as we understand it, all errors have an own stack
// accessor property, but within the same realm, all these accessor
// properties have the same getter and have the same setter.
// This is therefore the case that we repair.
Copy link
Contributor

@erights erights Oct 31, 2025

Choose a reason for hiding this comment

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

Should also mention somewhere that the upcoming Error.captureStackTrace proposal may result in more own stack accessors, on more platforms, that may or may not use the same getter and setter. If we don't think those are a problem, we should explain somewhere why not. But not necessarily here.

// properties have the same getter and have the same setter.
// This is therefore the case that we repair.
typeof er1StackDesc.get !== 'function' ||
er1StackDesc.get !== er2StackDesc.get ||
Copy link
Contributor

Choose a reason for hiding this comment

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

Aha! Now I understand

Comment on lines 68 to 70
// Otherwise, we have own stack accessor properties that are outside
// our expectations, that therefore need to be understood better
// before we know how to repair them.
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this comment be moved to immediately above the above throw?

Copy link
Contributor

@erights erights left a comment

Choose a reason for hiding this comment

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

I did not review most of the tests. But I reviewed error.js carefully which LGTM.

Thanks!

Comment on lines 270 to 342
if (repairError !== undefined) {
repairError(candidate);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Comment here would be good about why we're ok with this special case side effect in what should be a pure query function.

@kriskowal kriskowal force-pushed the kriskowal-fix-harden-taming-unsafe branch from 0fd4738 to 9cbb853 Compare October 31, 2025 21:30
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.

Looks good, but would love to see an explicit test for the can't patch error case (either by harden or by passStyleOf).

Also wondering about how lazy the makeRepairError should be.

},
rawOut(obj) {
t.is(Object.isFrozen(obj), true);
t.true(hardenIsFake || Object.isFrozen(obj));
Copy link
Contributor

Choose a reason for hiding this comment

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

Curious why this is the only Object.isFrozen case where we have to condition on hardenIsFake

Copy link
Member Author

Choose a reason for hiding this comment

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

I count 9 total. 5 affirmative. 4 negative. Perhaps you mean something else?

Copy link
Contributor

Choose a reason for hiding this comment

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

I see some Object.freeze calls above in the same test that don't get the same treatment, and arguably I didn't look closely at what the test does, I just saw those lines modified, but not get an extra hardenIsFake condition, and was just wondering why.

Copy link
Member Author

Choose a reason for hiding this comment

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

Your intuition was onto something, anyway. I went through each of these cases again and found 4 that did not require the relaxation for harden.isFake, left over from when I relaxed these tests to accommodate the unsafe mode of @endo/harden where isFrozen is not rigged. Will have to revisit if we go so far.

);
}
if (repairError !== undefined) {
// This point is unreachable unless the candidate is mutable and the
Copy link
Contributor

Choose a reason for hiding this comment

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

How do we know the object is mutable here? I mean it's possible that fake harden didn't freeze but that someone actually explicitly froze it.

@kriskowal kriskowal force-pushed the kriskowal-fix-harden-taming-unsafe branch from 9cbb853 to 392e030 Compare November 3, 2025 05:20
Copy link
Member Author

@kriskowal kriskowal left a comment

Choose a reason for hiding this comment

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

Pushed a commit to explicitly test frozen-not-hardened errors are unfixable on pathological-V8 (but fine on prior V8 versions we still test).

},
rawOut(obj) {
t.is(Object.isFrozen(obj), true);
t.true(hardenIsFake || Object.isFrozen(obj));
Copy link
Member Author

Choose a reason for hiding this comment

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

I count 9 total. 5 affirmative. 4 negative. Perhaps you mean something else?

@kriskowal kriskowal force-pushed the kriskowal-fix-harden-taming-unsafe branch from 946b89b to 5221365 Compare November 3, 2025 22:58
@kriskowal
Copy link
Member Author

I believe I’ve addressed all feedback. Commits added, none changed, for incremental review.

@kriskowal kriskowal force-pushed the kriskowal-fix-harden-taming-unsafe branch from 5221365 to de4795c Compare November 3, 2025 23:19
const { ownKeys } = Reflect;
const { isFrozen, getOwnPropertyDescriptors, values } = Object;

const repairError = makeRepairError();
Copy link
Contributor

Choose a reason for hiding this comment

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

So this still causes makeRepairError to still run at import of @endo/pass-style, but from what I gather, for the use of the toPassableError export, which unlike passStyleOf, is always exported anew.

I think the pass-style package has grown into a weird monster of instantiable and instantiated exports, both exposed on the same entry-point. Because of this I'm tempted to ask to revert my previous request for a lazy repairError, as it's pointless with the current structure of this package. Sorry about the back and forth, I forgot how unwieldy this whole thing is.

@kriskowal kriskowal force-pushed the kriskowal-fix-harden-taming-unsafe branch 5 times, most recently from 71c7d0f to 6feeca5 Compare November 4, 2025 01:50
@kriskowal kriskowal force-pushed the kriskowal-fix-harden-taming-unsafe branch from 6feeca5 to 95c8156 Compare November 4, 2025 02:05
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.

LGTM

Feel free to further revert the repairError change so that it's fully contained within error.js and no changes are required of passStyleOf.js.

@kriskowal kriskowal force-pushed the kriskowal-fix-harden-taming-unsafe branch from 95c8156 to 8c55a42 Compare November 4, 2025 05:22
@kriskowal kriskowal force-pushed the kriskowal-fix-harden-taming-unsafe branch from 8c55a42 to 39f8386 Compare November 4, 2025 06:33
const mutable = {};
t.is(greeter.sayHello(mutable), 'hello', `passableGreeter can sayHello`);
t.is(Object.isFrozen(mutable), true, `mutable is frozen`);
t.true(Object.isFrozen(mutable), `mutable is frozen`);
Copy link
Member Author

Choose a reason for hiding this comment

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

This case relies on harden and isFrozen to be in cahoots. This passes with unsafe harden taming because isFrozen is lying. The safe harden taming passes because harden, called as a side-effect of passing mutable to an Exo, is effective. This will continue to work without modification when we introduce surface hardening. If we obviate unsafe harden taming by making an unsafe mode of @endo/harden which doesn’t collude with isFrozen, this test will need to be adjusted to take that into account.

},
rawIn(obj) {
t.is(Object.isFrozen(obj), false);
t.false(Object.isFrozen(obj));
Copy link
Member Author

Choose a reason for hiding this comment

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

This test passes because the M.raw guard is bypassing the harden otherwise implied. It’s always passed an unfrozen/unhardened object in the tests.

},
rawOut(obj) {
t.is(Object.isFrozen(obj), true);
t.true(hardenIsFake || Object.isFrozen(obj));
Copy link
Member Author

Choose a reason for hiding this comment

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

Your intuition was onto something, anyway. I went through each of these cases again and found 4 that did not require the relaxation for harden.isFake, left over from when I relaxed these tests to accommodate the unsafe mode of @endo/harden where isFrozen is not rigged. Will have to revisit if we go so far.

t.false(Object.isFrozen(obj));
return obj;
},
rawOut(obj) {
Copy link
Member Author

Choose a reason for hiding this comment

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

rawOut has an M.any() guard, so the argument is implicitly hardened, albeit fake hardened. The relaxation is unnecessary in this case and I will restore the case.

t.is(Object.isFrozen(optSoftE), false);
t.true(Object.isFrozen(hardC));
t.true(Object.isFrozen(optHardD));
t.false(!hardenIsFake && Object.isFrozen(optSoftE));
Copy link
Member Author

Choose a reason for hiding this comment

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

This case requires a relaxation because isFrozen is misreporting a soft object has having been frozen. It’s either a soft object that was passed through a raw optional arg, or defaulted to a soft object.

@kriskowal kriskowal merged commit a5a0f5c into master Nov 4, 2025
19 checks passed
@kriskowal kriskowal deleted the kriskowal-fix-harden-taming-unsafe branch November 4, 2025 06:39
kriskowal added a commit that referenced this pull request Nov 13, 2025
Refs: #2990

## Description

This change synchronizes the error repair mechanism in SES with changes
made in #2990 to pass-style to gratutiously improve the resilience of
that mechanism in the face of scripts that ran before SES.

### Security Considerations

This reduces SES vulnerability to corruption from code that runs before
SES. Replacing the TypeError constructor cannot confuse SES with regard
to whether the platform produces type errors with own stack properties.

### Scaling Considerations

None.

### Documentation Considerations

None.

### Testing Considerations

We cover versions of Node.js with and without own stack properties in CI
and existing tests should be sufficient.

### Compatibility Considerations

Programs that previously deceived SES may now fail when initializing
SES. We consider such programs compromised and that incompatibility is a
feature.

### Upgrade Considerations

None.
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.

4 participants