Skip to content

Commit 9fe0282

Browse files
authored
Add compat flag to mark jsg::Object prototypes as immutable (#5522)
1 parent 7a125e8 commit 9fe0282

File tree

12 files changed

+66
-1
lines changed

12 files changed

+66
-1
lines changed

src/workerd/api/BUILD.bazel

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1170,3 +1170,9 @@ wd_test(
11701170
"tests/instrumentation-tail-worker.js",
11711171
],
11721172
)
1173+
1174+
wd_test(
1175+
src = "tests/headers-immutable-prototype-test.wd-test",
1176+
args = ["--experimental"],
1177+
data = ["tests/headers-immutable-prototype-test.js"],
1178+
)
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { strictEqual } from 'node:assert';
2+
3+
export const test = {
4+
test() {
5+
strictEqual(
6+
Reflect.getOwnPropertyDescriptor(Headers, 'prototype').writable,
7+
false
8+
);
9+
},
10+
};
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
using Workerd = import "/workerd/workerd.capnp";
2+
3+
const unitTests :Workerd.Config = (
4+
services = [
5+
( name = "headers-immutable-prototype-test",
6+
worker = (
7+
modules = [
8+
(name = "worker", esModule = embed "headers-immutable-prototype-test.js")
9+
],
10+
compatibilityDate = "2025-11-01",
11+
compatibilityFlags = ["nodejs_compat", "immutable_api_prototypes"],
12+
)
13+
),
14+
],
15+
);

src/workerd/io/compatibility-date.capnp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1221,4 +1221,10 @@ struct CompatibilityFlags @0x8f8c1b68151b6cef {
12211221
$compatDisableFlag("disable_python_check_rng_state")
12221222
$experimental;
12231223

1224+
shouldSetImmutablePrototype @145 :Bool
1225+
$compatEnableFlag("immutable_api_prototypes")
1226+
$compatDisableFlag("mutable_api_prototypes");
1227+
# When set, tells JSG to make the prototype of all jsg::Objects immutable.
1228+
# TODO(soon): Add the default on date once the flag is verified to be
1229+
# generally safe.
12241230
}

src/workerd/io/worker.c++

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1052,9 +1052,14 @@ Worker::Isolate::Isolate(kj::Own<Api> apiParam,
10521052
lock->v8Isolate->SetData(jsg::SET_DATA_ISOLATE, this);
10531053

10541054
lock->setCaptureThrowsAsRejections(features.getCaptureThrowsAsRejections());
1055+
// TODO(cleanup): Now that this list has grown significantly, we should probably
1056+
// refactor to pass all of the options in a single call instead of one by one.
10551057
if (features.getSetToStringTag()) {
10561058
lock->setToStringTag();
10571059
}
1060+
if (features.getShouldSetImmutablePrototype() || features.getPythonWorkers()) {
1061+
lock->setImmutablePrototype();
1062+
}
10581063
if (features.getNodeJsCompatV2()) {
10591064
lock->setNodeJsCompatEnabled();
10601065
}

src/workerd/jsg/jsg-test.h

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,6 @@ struct NumberBox: public Object {
217217
}
218218

219219
JSG_RESOURCE_TYPE(NumberBox) {
220-
221220
JSG_METHOD(increment);
222221
JSG_METHOD(incrementBy);
223222
JSG_METHOD(incrementByBox);

src/workerd/jsg/jsg.c++

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,10 @@ void Lock::setToStringTag() {
231231
IsolateBase::from(v8Isolate).enableSetToStringTag();
232232
}
233233

234+
void Lock::setImmutablePrototype() {
235+
IsolateBase::from(v8Isolate).enableSetImmutablePrototype();
236+
}
237+
234238
void Lock::setLoggerCallback(kj::Function<Logger>&& logger) {
235239
IsolateBase::from(v8Isolate).setLoggerCallback({}, kj::mv(logger));
236240
}

src/workerd/jsg/jsg.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2660,6 +2660,7 @@ class Lock {
26602660
void setThrowOnUnrecognizedImportAssertion();
26612661
bool getThrowOnUnrecognizedImportAssertion() const;
26622662
void setToStringTag();
2663+
void setImmutablePrototype();
26632664
void disableTopLevelAwait();
26642665

26652666
using Logger = void(Lock&, kj::StringPtr);

src/workerd/jsg/resource.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1896,6 +1896,10 @@ class ResourceWrapper {
18961896
static_cast<v8::PropertyAttribute>(v8::PropertyAttribute::DontEnum |
18971897
v8::PropertyAttribute::DontDelete | v8::PropertyAttribute::ReadOnly));
18981898

1899+
if (getShouldSetImmutablePrototype(isolate)) {
1900+
constructor->ReadOnlyPrototype();
1901+
}
1902+
18991903
constructor->SetClassName(classname);
19001904

19011905
static_assert(kj::isSameType<typename T::jsgThis, T>(),

src/workerd/jsg/setup.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,14 @@ class IsolateBase {
178178
setToStringTag = true;
179179
}
180180

181+
inline bool shouldSetImmutablePrototype() const {
182+
return shouldSetImmutablePrototypeFlag;
183+
}
184+
185+
void enableSetImmutablePrototype() {
186+
shouldSetImmutablePrototypeFlag = true;
187+
}
188+
181189
inline void disableTopLevelAwait() {
182190
allowTopLevelAwait = false;
183191
}
@@ -324,6 +332,7 @@ class IsolateBase {
324332
bool nodeJsCompatEnabled = false;
325333
bool nodeJsProcessV2Enabled = false;
326334
bool setToStringTag = false;
335+
bool shouldSetImmutablePrototypeFlag = false;
327336
bool allowTopLevelAwait = true;
328337
bool usingNewModuleRegistry = false;
329338
bool usingEnhancedErrorSerialization = false;

0 commit comments

Comments
 (0)