From af4894c74b7e22d6d9d22542711aec44499fa35d Mon Sep 17 00:00:00 2001 From: Kenton Varda Date: Sat, 14 Mar 2026 21:22:14 -0500 Subject: [PATCH] Impose limits on facet name length and nesting depth. Name length limit: 256 chars Nesting depth limit: 4 (including root) We could increase these limits if we needed to but it's hard to imagine anyone needing more than this, in fact I have yet to see a use case for multiple levels of child facets at all. --- src/workerd/api/actor-state.c++ | 12 +++ src/workerd/api/actor-state.h | 7 ++ src/workerd/io/worker.h | 3 + src/workerd/server/server-test.c++ | 140 +++++++++++++++++++++++++++++ src/workerd/server/server.c++ | 7 ++ 5 files changed, 169 insertions(+) diff --git a/src/workerd/api/actor-state.c++ b/src/workerd/api/actor-state.c++ index dbcb1ff3ecd..bc20f4d7fed 100644 --- a/src/workerd/api/actor-state.c++ +++ b/src/workerd/api/actor-state.c++ @@ -974,7 +974,15 @@ class FacetOutgoingFactory final: public Fetcher::OutgoingFactory { jsg::Ref DurableObjectFacets::get(jsg::Lock& js, kj::String name, jsg::Function()> getStartupOptions) { + JSG_REQUIRE(name.size() <= MAX_FACET_NAME_LENGTH, TypeError, "Facet name is too long (max ", + MAX_FACET_NAME_LENGTH, " characters)."); + auto& fm = getFacetManager(); + + JSG_REQUIRE(fm.getDepth() + 1 < MAX_FACET_TREE_DEPTH, Error, + "Facet nesting depth limit exceeded. The maximum depth including the root Durable Object is ", + MAX_FACET_TREE_DEPTH, "."); + auto& ioCtx = IoContext::current(); kj::Function()> getStartInfo = @@ -1031,10 +1039,14 @@ jsg::Ref DurableObjectFacets::get(jsg::Lock& js, } void DurableObjectFacets::abort(jsg::Lock& js, kj::String name, jsg::JsValue reason) { + JSG_REQUIRE(name.size() <= MAX_FACET_NAME_LENGTH, TypeError, "Facet name is too long (max ", + MAX_FACET_NAME_LENGTH, " characters)."); getFacetManager().abortFacet(name, js.exceptionToKj(reason)); } void DurableObjectFacets::delete_(jsg::Lock& js, kj::String name) { + JSG_REQUIRE(name.size() <= MAX_FACET_NAME_LENGTH, TypeError, "Facet name is too long (max ", + MAX_FACET_NAME_LENGTH, " characters)."); getFacetManager().deleteFacet(name); } diff --git a/src/workerd/api/actor-state.h b/src/workerd/api/actor-state.h index 187a1e06bfd..b821339151b 100644 --- a/src/workerd/api/actor-state.h +++ b/src/workerd/api/actor-state.h @@ -412,6 +412,13 @@ class DurableObjectTransaction final: public jsg::Object, public DurableObjectSt class DurableObjectFacets: public jsg::Object { public: + // Maximum length of a facet name, in characters. + static constexpr size_t MAX_FACET_NAME_LENGTH = 256; + + // Maximum depth of the facet tree, including the root Durable Object. Root is at depth 0, so + // the deepest allowed facet is at depth MAX_FACET_TREE_DEPTH - 1. + static constexpr uint MAX_FACET_TREE_DEPTH = 4; + DurableObjectFacets(kj::Maybe> facetManager) : facetManager(kj::mv(facetManager)) {} diff --git a/src/workerd/io/worker.h b/src/workerd/io/worker.h index 05f180b901f..cc338e31a9e 100644 --- a/src/workerd/io/worker.h +++ b/src/workerd/io/worker.h @@ -897,6 +897,9 @@ class Worker::Actor final: public kj::Refcounted { Worker::Actor::Id id; }; + // Returns the nesting depth of this facet. Root = 0, direct child of root = 1, etc. + virtual uint getDepth() const = 0; + // These methods are C++ equivalents of the JavaScript ctx.facets API. virtual kj::Own getFacet( kj::StringPtr name, kj::Function()> getStartInfo) = 0; diff --git a/src/workerd/server/server-test.c++ b/src/workerd/server/server-test.c++ index 81efdd78cc7..d25a05600f8 100644 --- a/src/workerd/server/server-test.c++ +++ b/src/workerd/server/server-test.c++ @@ -5020,6 +5020,146 @@ KJ_TEST("Server: Durable Object facets") { } } +KJ_TEST("Server: Durable Object facet limits") { + kj::StringPtr config = R"(( + services = [ + ( name = "hello", + worker = ( + compatibilityDate = "2025-04-01", + compatibilityFlags = ["experimental"], + modules = [ + ( name = "main.js", + esModule = + `import { DurableObject } from "cloudflare:workers"; + `export default { + ` async fetch(request, env, ctx) { + ` let id = env.MY_ACTOR.idFromName("limits"); + ` let actor = env.MY_ACTOR.get(id); + ` return await actor.fetch(request); + ` } + `} + `export class MyActorClass extends DurableObject { + ` async fetch(request) { + ` let url = new URL(request.url); + ` switch (url.pathname) { + ` case "/name-too-long": { + ` try { + ` this.ctx.facets.get("x".repeat(257), + ` () => ({class: this.env.RECURSIVE})); + ` return new Response("no error"); + ` } catch (e) { + ` return new Response(e.constructor.name + ": " + e.message); + ` } + ` } + ` case "/name-256-ok": { + ` this.ctx.facets.get("x".repeat(256), + ` () => ({class: this.env.RECURSIVE})); + ` return new Response("ok"); + ` } + ` case "/abort-name-too-long": { + ` try { + ` this.ctx.facets.abort("x".repeat(257), new Error("test")); + ` return new Response("no error"); + ` } catch (e) { + ` return new Response(e.constructor.name + ": " + e.message); + ` } + ` } + ` case "/delete-name-too-long": { + ` try { + ` this.ctx.facets.delete("x".repeat(257)); + ` return new Response("no error"); + ` } catch (e) { + ` return new Response(e.constructor.name + ": " + e.message); + ` } + ` } + ` case "/depth-ok": { + ` // Create 3 levels of facets below root = 4 total (the max). + ` let facet = this.ctx.facets.get("a", + ` () => ({class: this.env.RECURSIVE})); + ` return new Response(await facet.nestOk(2)); + ` } + ` case "/depth-exceeded": { + ` // Create 3 levels below root, then try one more. + ` let facet = this.ctx.facets.get("b", + ` () => ({class: this.env.RECURSIVE})); + ` return new Response(await facet.nestDeeper(2)); + ` } + ` } + ` } + `} + `export class RecursiveFacet extends DurableObject { + ` async nestOk(remaining) { + ` if (remaining <= 0) return "ok"; + ` let facet = this.ctx.facets.get("child", + ` () => ({class: this.env.RECURSIVE})); + ` return await facet.nestOk(remaining - 1); + ` } + ` async nestDeeper(remaining) { + ` if (remaining <= 0) { + ` try { + ` this.ctx.facets.get("too-deep", + ` () => ({class: this.env.RECURSIVE})); + ` return "no error, unexpected"; + ` } catch (e) { + ` return e.constructor.name + ": " + e.message; + ` } + ` } + ` let facet = this.ctx.facets.get("child", + ` () => ({class: this.env.RECURSIVE})); + ` return await facet.nestDeeper(remaining - 1); + ` } + `} + ) + ], + bindings = [ + (name = "MY_ACTOR", durableObjectNamespace = "MyActorClass"), + (name = "RECURSIVE", + durableObjectClass = (name = "hello", entrypoint = "RecursiveFacet")) + ], + durableObjectNamespaces = [ + ( className = "MyActorClass", + uniqueKey = "mykey", + ) + ], + durableObjectStorage = (localDisk = "my-disk") + ) + ), + ( name = "my-disk", + disk = ( + path = "../../do-storage", + writable = true, + ) + ), + ], + sockets = [ + ( name = "main", + address = "test-addr", + service = "hello" + ) + ] + ))"_kj; + + TestServer test(config); + test.root->openSubdir(kj::Path({"do-storage"_kj}), kj::WriteMode::CREATE); + test.server.allowExperimental(); + test.start(); + auto conn = test.connect("test-addr"); + + // Name length limit. + conn.httpGet200("/name-too-long", "TypeError: Facet name is too long (max 256 characters)."); + conn.httpGet200("/name-256-ok", "ok"); + conn.httpGet200( + "/abort-name-too-long", "TypeError: Facet name is too long (max 256 characters)."); + conn.httpGet200( + "/delete-name-too-long", "TypeError: Facet name is too long (max 256 characters)."); + + // Depth limit. + conn.httpGet200("/depth-ok", "ok"); + conn.httpGet200("/depth-exceeded", + "Error: Facet nesting depth limit exceeded. " + "The maximum depth including the root Durable Object is 4."); +} + KJ_TEST("Server: Pass service stubs in ctx.props.") { TestServer test(R"(( services = [ diff --git a/src/workerd/server/server.c++ b/src/workerd/server/server.c++ index 43b84c8be80..088a47a0fed 100644 --- a/src/workerd/server/server.c++ +++ b/src/workerd/server/server.c++ @@ -2489,6 +2489,13 @@ class Server::WorkerService final: public Service, return entry.value->addRef(); } + uint getDepth() const override { + KJ_IF_SOME(p, parent) { + return 1 + p.getDepth(); + } + return 0; + } + kj::Own getFacet( kj::StringPtr name, kj::Function()> getStartInfo) override { auto facet = getFacetContainer(kj::str(name), kj::mv(getStartInfo));