| 01:01 | <rbuckton> |
As such:
|
| 01:03 | <rbuckton> | However, if you use the same registry RAB with A and B:
|
| 01:06 | <rbuckton> | If such a prototype is initialized lazily in [[GetPrototypeOf]], by the time B can receive a PointA, or that A can receive a PointB, both agents will have completed their handshake with M, so all information is known. This is another reason why my proposal uses a preload script. The preload script performs the worker side of the handshake before any other data can be shared between the worker and M, so you cannot have a stray PointA sent to B, or PointB sent to A, prior to a completed handshake on both sides. |
| 01:13 | <rbuckton> | Now, we could theoretically have a global registry instead, with the structs: {} map only used to correlate PointM and PointA when A is established. Workers will always be part of a tree that points back to some root agent, so there's always a way to collect these things. If the handshake establishes the relationship without the ability to pass messages, would that be sufficient to address concerns about a global registry? |
| 01:16 | <rbuckton> | Especially if the worker can't actually observe the exemplar during handshake, since the handshake process is handled by the runtime. We wouldn't even need communicate the actual exemplars through the handshake process, just the type identities of the exemplars. |
| 01:21 | <rbuckton> | Though there is the caveat that M could try to pass off a PointA as an exemplar to B's Rect, but we could probably just make that an error, i.e. the exemplars M sends during the handshake must have been created by a type created in M's Agent |
| 01:22 | <rbuckton> | and the same thing goes for A (or B) spinning up a Worker (A2) during handshake and passing off one A2's exemplars as one of its own. |
| 03:05 | <Mathieu Hofman> | Right I think an agent based registry can only work if:
that means a worker A connected to a worker B through M but not sharing the same connection registry will not be able share behavior throughout. I'm still wondering about the special parent - child relationship these connection based registries seem to have, and how you can only have one connection registry between 2 agents or things fall apart. I can't explain why exactly right now, but this all feels awkward. |
| 12:38 | <rbuckton> | I'm wondering if we even need a connection-based registry if we can devise a global registry strategy that addresses Agoric's concerns about security. You'd discussed how a mutable global registry is a possible side channel for data exfiltration? I'm curious how serious the concern is and if you have a link to a paper or something else that could provide additional context? Is the concern related to how a Worker could abuse such a global registry, or how a script or module in the same Agent could abuse such a registry? |
| 16:32 | <shu> |
|
| 16:53 | <Mathieu Hofman> | I'm wondering if we even need a connection-based registry if we can devise a global registry strategy that addresses Agoric's concerns about security. You'd discussed how a mutable global registry is a possible side channel for data exfiltration? I'm curious how serious the concern is and if you have a link to a paper or something else that could provide additional context? Is the concern related to how a Worker could abuse such a global registry, or how a script or module in the same Agent could abuse such a registry? |
| 16:55 | <rbuckton> | rbuckton: after chatting with some other V8 engineers i'm coming back to the idea that perhaps (2) is better |
| 16:57 | <shu> | rbuckton: that's not clear to me yet. one challenge here is how to express the thread-localness of a superclass |
| 16:58 | <shu> | we want the fixed layout invariant to hold, so do you say like "shared struct A extends per-agent B", but what is B? it could be itself a shared struct but its layout gets copied into a thread-local version of the struct the first time [[Prototype]] is accessed in a thread |
| 16:58 | <shu> | should it be a non-shared struct declaration? |
| 16:59 | <shu> | (but it gets that layout copy behavior) |
| 17:26 | <rbuckton> | The concern has usually manifested itself in the form of Realm-wide or Agent-wide state, but it's conceivable that the same concern could manifest for Agent cluster-wide state. The problem is that such global mutable state allows 2 parties that do not share any references besides the primordials objects to communicate. In JavaScript today, you can freeze all the intrinsics, and it's not possible for 2 pieces of code to communicate unless they're explicitly provided a reference to each other, or to a shared mutable object. |
| 17:30 | <rbuckton> | Lets assume you can't use the exemplar values themselves to communicate, i.e., the actual exemplars aren't exposed to user code on the other Agent. |
| 17:32 | <rbuckton> | The child thread can't send or receive structs to the parent thread during handshake, and by the time handshake has completed all correlation between the parent and child is frozen. |
| 17:34 | <rbuckton> | By the time A can observe a struct from B, the correlation between M, A, and B has already occurred and is frozen. You cannot dynamically attach new behavior, but we do lazily resolve the prototype based on correlation. |
| 17:40 | <rbuckton> | Maybe there's a small possibility of a timing related exploit if I can somehow spin up multiple additional workers on M and send an existing corelated struct to A to indicate 0 and new correlated struct to A indicating 1 and somehow measure the timing? That might be mitigated if correlation happens before normal communication can occur and prototype lookup always follows the same path, but you could potentially use structs who have narrow and wide correlation sets and measure timing that way, or update an agent-local correlation registry when two agent's communicate for the first time so that you pay that cost once. |
| 17:43 | <rbuckton> | There are possibly other ways to mitigate that as well. |
| 17:46 | <rbuckton> | Within a single Agent, when worker's aren't involved, you wouldn't be able to use this registry for communication because it would be inaccessible. You can also use CSP to lock down Worker to specific scripts, or disable it entirely. |
| 17:49 | <rbuckton> | If Worker is locked down via CSP, the only way you could leverage these for a timing attack would be to be handed a reference to a shared struct, which I would argue qualifies for being provided a reference to a shared mutable object. |
| 18:00 | <rbuckton> | If you have two isolated pieces of code in the same Agent that both have access to an unrestricted Worker, its possible they could already communicate with each other via resource starvation and timing attacks. |
| 19:06 | <Mathieu Hofman> | For same realm/agent, if the registry is string keyed, Alice can register "foo". If Bob can somehow figure out that "foo" is already registered, this is a one bit communication channel. There are likely multiple ways Bob could sense whether "foo" is registered. |
| 19:20 | <rbuckton> | With the global registry concept, all registration within a single Agent would happen via new SharedStructType (or via shared struct Foo {}). No errors would be reported except for running out of heap space (and crashing). When setting up a Worker, there is a correlation step to correlate the registrations within both Agents, but this only occurs at the time of the Worker handshake and should only be observable by interacting with that Worker or a shared struct provided to the worker. |
| 19:22 | <rbuckton> | As far as I can tell, there's no way to observe that within a single Agent/realm. You can't check if something is "registered" because all "registration" happens before the thing you would test exists. |
| 19:24 | <rbuckton> | The only way to observe correlation would be to use a Worker and a shared reference, which still only observes correlation between those two Agents. |
| 19:25 | <rbuckton> | There should be no way to get at the registry itself, and the only way to establish correlation is to already have a reference to the shared struct type. |
| 19:26 | <rbuckton> | You could observe whether A and B share correlation with M, but only if you already have access to shared data from A and B |
| 19:27 | <rbuckton> | There would be no error upon registration, because there is no addressable identity to forge, nor a way to forge it. Every shared struct type would have its own type identity, defined at the time of creation. |
| 19:56 | <Mathieu Hofman> | I think it depends on how the global registry works, how it handles collisions? Any mechanism that uses a forgeable value as key is likely observable, whether it errors, or first / last win. In the latter case, as you mention starting a worker and asking it to send you that type, and seeing what behavior you get, yours or the other one registered in the same realm. I really cannot imagine any way where a registry with forgeable keys can be made unobservable. You do mention "no way to get at the registry itself", which instead sounds like design we were talking about yesterday, not an agent wide string keyed registry, but instead a connection based string-keyed mapper. I agree that it may be possible to make that work, but I think it requires the "correlation registry" between 2 agents to be unique and immutable after start. |
| 19:57 | <shu> | rbuckton: actually how do you think we can syntactically express the shape of a shared struct's prototype, if that prototype is to be fixed layout but per-thread? |
| 19:57 | <shu> | there's not a good precedent to fall back on in class syntax |
| 20:01 | <rbuckton> | rbuckton: actually how do you think we can syntactically express the shape of a shared struct's prototype, if that prototype is to be fixed layout but per-thread? |
| 20:01 | <shu> | it's not as important but i feel it is still important |
| 20:03 | <shu> | part of my mental model of structs (shared or not) over ordinary objects is "the shape doesn't change", and that transitively applies via the prototype chain |
| 20:03 | <rbuckton> | I think it depends on how the global registry works, how it handles collisions? Any mechanism that uses a forgeable value as key is likely observable, whether it errors, or first / last win. In the latter case, as you mention starting a worker and asking it to send you that type, and seeing what behavior you get, yours or the other one registered in the same realm. I really cannot imagine any way where a registry with forgeable keys can be made unobservable. |
| 20:04 | <rbuckton> | part of my mental model of structs (shared or not) over ordinary objects is "the shape doesn't change", and that transitively applies via the prototype chain Number, String, etc. so it had no bearing on the shape of struct's runtime representation. |
| 20:04 | <shu> | yes, that is a competing model |
| 20:05 | <rbuckton> | That's not the case now, but I still don't find see the necessity for a fixed shape for the prototype. |
| 20:05 | <shu> | and i am open to be convinced of that competing model |
| 20:05 | <shu> | it has some attractive properties, like, the dynamism feels more at home with the rest of the language |
| 20:05 | <shu> | it has an exact parallel to primitive prototype wrapping, as you've pointed out |
| 20:06 | <rbuckton> | The caveat is that it doesn't translate well to multiple realms in the same Agent |
| 20:06 | <rbuckton> | Unless you need to somehow define behavior independently per realm. |
| 20:07 | <rbuckton> | Which would be another spanner to throw into the behavior assignment discussion :) |
| 20:07 | <shu> | the downside to the primitive-like wrapping model is i had harbored some hopes "fixed layout" would translate to "easy" static analyzability of static property access on struct instances |
| 20:08 | <shu> | but if for knowing the location s.p requires giving up if p is from the prototype, that's too bad |
| 20:08 | <shu> | it's not the end of the world or anything |
| 20:09 | <shu> | The caveat is that it doesn't translate well to multiple realms in the same Agent |
| 20:09 | <shu> | i'm pretty neutral on whether to choose per-realm or per-agent. agent is not a notion we expose right now, but realms are, so that's more natural |
| 20:10 | <shu> | you end up with weird DX papercuts if you do work with multiple realms in the same agent, but i guess any app that works with multiple realms already must deal with identity discontinuity to some extent |
| 20:11 | <shu> | okay, let's continue the thought experiment down the path of relaxing the fixed layout constraint to not apply to nonshared prototypes |
| 20:11 | <shu> | how would you express that in syntax? |
| 20:12 | <shu> | and how would we take care to not preclude a future with actual shareable functions |
| 20:12 | <Mathieu Hofman> | What collisions? What is forgeable? The only thing user-provided is the correlation token used to explain what prototype to choose for a foreign struct within an Agent, and that only affects that Agent's view of the struct, not any other agent. |
| 20:13 | <Mathieu Hofman> | Which would be another spanner to throw into the behavior assignment discussion :) |
| 20:13 | <shu> | Mathieu Hofman: i take it you'd prefer per-realm over per-agent? |
| 20:15 | <rbuckton> | I'm not sure any handshake mechanism will work per-realm unless you have to establish the handshake when the realm is created, and you can't do that in the browser on the main thread with frames. |
| 20:15 | <shu> | ah i hadn't thought that far, that's what you meant by spanner |
| 20:16 | <Mathieu Hofman> | Well let's say I don't want this to enable a realm to discover the object graph of another realm, if they were previously isolated. I think that's my constraint |
| 20:17 | <shu> | to answer my own syntax question earlier, this could work:
|
| 20:17 | <shu> | since currently, having static prototype is an early error |
| 20:18 | <rbuckton> | That's a bit strange, and it doesnt seem like it would work well with method declarations. |
| 20:19 | <shu> | why wouldn't it work well with method declarations? |
| 20:19 | <rbuckton> | It looks a bit like a field declaration. |
| 20:20 | <shu> | (and to clarify, are you thinking of method declarations in the possible future where they are specially-packaged-and-cloned, or the possible future where we have some exotic new callable that's truly shared) |
| 20:20 | <rbuckton> | I'm considering both |
| 20:21 | <shu> | It looks a bit like a field declaration. |
| 20:21 | <shu> | well, the internal slot |
| 20:22 | <rbuckton> | I need to think on that a bit. |
| 20:22 | <shu> | it's by all means just an incantation |
| 20:22 | <shu> | not a composable bit of syntax |
| 20:22 | <shu> | ideas welcome, certainly, most things i've thought of are even uglier |
| 20:25 | <rbuckton> | You want a syntax that:
Does that cover everything? |
| 20:28 | <shu> | that seems comprehensive
|
| 20:29 | <rbuckton> | If possible I'd like to not have to go indirectly through a WeakMap. |
| 20:29 | <shu> | for arbitrary fields? |
| 20:29 | <rbuckton> | I'd also like to find a way to allow shared private fields, even if that privacy is only agent-local. |
| 20:30 | <shu> | let's punt on private fields for now :) |
| 20:30 | <rbuckton> | I'm not sure if its feasible, but I'd like to find a way to consider it. |
| 20:31 | <shu> | a big part of the reason i've moved back to thinking thread-local prototype being the superior solution is the performance footgun aspect of heavy thread-local field usage |
| 20:32 | <rbuckton> | I'm writing a lot of shared structs using TypeScript's soft private currently. |
| 20:32 | <shu> | the performance will be so extremely different, yet looks the same |
| 20:32 | <shu> | if we bottleneck that thread-local lookup to be just on [[Prototype]], then we ease the performance footgun concerns |
| 20:33 | <shu> | the expressivity still exists with WeakMaps |
| 20:34 | <shu> | private names should just work, with the big exception of the lexical scoping of #-names |
| 20:34 | <shu> | so the per-agent privacy "just works" but that feels weird |
| 20:36 | <shu> | well no, "just works" is too strong. there will need to syntax changes to allow #-names to be scoped in such a way that allows it to even be expressed with struct declarations |
| 20:37 | <rbuckton> | The issue with private names is whether #foo is accessible inside of a nonshared method in two different threads. |
| 20:37 | <shu> | right |
| 20:38 | <rbuckton> | for it to be useful, it has to be. But that weakens privacy. |
| 20:39 | <rbuckton> | So you either need to:
|
| 20:41 | <rbuckton> | I think (2) is unusable, I'm sure someone won't be happy with (3), and I don't have a solution for (4) yet. |
| 20:41 | <rbuckton> | nonshared private fields are definitely doable. |
| 20:42 | <rbuckton> | the performance will be so extremely different, yet looks the same |
| 20:43 | <shu> | yeah perhaps |
| 20:43 | <shu> | i agree (2) will be unusable |
| 20:44 | <rbuckton> | For private names, we could make you explicitly annotate them as shared to get the point across that their privacy is weaker. |
| 20:44 | <shu> | the only solution that composes i can think of is some kind of new exotic callable that's threadsafe |
| 20:44 | <shu> | and that this new exotic callable can close over # names |
| 20:45 | <shu> | but it can't close over normal bindings |
| 20:45 | <shu> | nobody liked the exotic callable idea the first time i brought it up though |
| 20:45 | <rbuckton> | why would we need that? |
| 20:45 | <rbuckton> | And I'm not even sure how you'd use that |
| 20:46 | <shu> | the private names thing can fall out of that, what i had in mind:
|
| 20:47 | <rbuckton> | In the "weaker privacy" model I was thinking about, private names are part of the type identity associated with a shared struct, and the handshake process that provides correlation between an exemplar and a prototype could be smart enough to correlate the private name as well. |
| 20:48 | <shu> | in the evaluation of S above, #x gets evaluated once and is closed over by this new shareable exotic callable, and you use those methods on instances and things just work |
| 20:48 | <shu> | we can't do this with normal functions obvoiusly because they're not shared things |
| 20:49 | <rbuckton> | So you do:
|
| 20:49 | <rbuckton> | You just have the private name itself be correlated. |
| 20:50 | <shu> | not sure i grasp how the correlation works |
| 20:50 | <rbuckton> | though that is a step towards always using shared struct declarations for handshaking rather than just an exemplar and a prototype. |
| 20:50 | <rbuckton> | I'll see if I can summarize? |
| 20:56 | <shu> | i have a harebrained worse-is-better idea |
| 21:00 | <rbuckton> |
|
| 21:02 | <shu> | i'm not clear on the second-to-last bullet point |
| 21:02 | <shu> | how does "look up the prototype of a non-local shared struct" differ from [[GetPrototypeOf]]? |
| 21:04 | <rbuckton> | The last two bullet points are mostly part of the same thing. |
| 21:05 | <shu> | are you saying every [[GetPrototypeOf]] of an instance has a pre-hook that does correlation in the registry? |
| 21:05 | <shu> | i was hoping you'd set up the correlation once per type and not incur a check on every [[GetPrototypeOf]] |
| 21:05 | <rbuckton> | There are two options. One is "when we create the thread local prototype Object for the foreign shared struct type we just received, we can correlate it in the registry" |
| 21:08 | <rbuckton> | The other is lazily on [[GetPrototypeOf]], but we don't need the laziness if the runtime can do all of this work for you. |
| 21:10 | <shu> | so here's my harebrained worse-is-better idea: what if
|
| 21:12 | <shu> | the return value of packageForClone() would be special cased in the structured clone algorithm to be cloneable |
| 21:15 | <rbuckton> | I still don't think this works because it makes assumptions about what is reachable in the child thread. |
| 21:16 | <shu> | how so? |
| 21:16 | <rbuckton> | The child thread might be running from a bundle that doesn't include some module names, because the methods that use them were removed from the child thread bundle due to tree shaking. |
| 21:17 | <shu> | it's like re-evaling a function's toString(), no assumptions are made per se, but if things get DCE'd because the tool wasn't aware it's implicitly being used somehow, then the tool needs those exceptions annotated, yeah |
| 21:17 | <rbuckton> | And its likely that the child thread already has a copy of all of the necessary behavior in memory, so now we're taking up even more memory in the worker thread for duplicate code. |
| 21:18 | <shu> | how did it get the necessary behavior in memory, import? this idea means you never import the right structs, you gotta always postMessage them |
| 21:18 | <shu> | but agreed that doesn't feel great |
| 21:19 | <rbuckton> | I'm not a fan of that design, tbh. Its too easy for code to become entangled. |
| 21:19 | <rbuckton> | I'll be back in a bit, in a meeting for the next hour. |
| 21:19 | <shu> | the minimal version of this idea is that shared struct constructors ought to be made structured cloneable in such a way that the VM can keep the type identity correlation across agents |
| 21:20 | <shu> | they can be safely cloned because these constructors don't call user code |
| 21:21 | <shu> | i guess that minimal version can be combined with your registry handshake. it makes the correlation of type identities automatic |
| 22:06 | <rbuckton> | I'm not sure I agree with that approach? I might need my shared struct constructor to access some per-thread configured object that may not be trivially serializable, such as accessing threadId in import { threadId } from "node:worker_threads" or, read from an environment variable via sys.getEnvironmentVariable(name), where sys must be correctly initialized for within that thread. |
| 22:06 | <rbuckton> | And both of these cases are present in the work I'm doing right now. |
| 22:07 | <rbuckton> | and I definitely want to be able to run user code so that I can appropriately set up shared struct instances, including providing suitable defaults to match the types I've defined. |
| 22:08 | <shu> | i think we're talking about 2 constructors |
| 22:08 | <rbuckton> | Yes and no. |
| 22:08 | <shu> | shared structs don't have user code constructors (i now also see that the README.md is incorrect) |
| 22:08 | <rbuckton> | They don't currently, correct. |
| 22:09 | <shu> | they just have a way to objects of the correct layout, let's call this constructor the "minter" |
| 22:09 | <shu> | you can wrap this in a per-thread constructor that does thread-local things |
| 22:09 | <rbuckton> | Sure, but you're talking about packing in the prototype members along with that, which we don't do anywhere else in JS. |
| 22:09 | <shu> | sorry i switched gears |
| 22:09 | <shu> | scratch the prototype members idea |
| 22:10 | <shu> | the minimal version is: the minter, and the minter alone, with no transitive properties, is a cloneable function that can be cloned across worker boundaries |
| 22:10 | <rbuckton> | Ok, but then I don't see why serializing the constructor is useful. |
| 22:10 | <shu> | ah, because the VM can keep tabs under the hood that it's correlated with shared structs of a particular type |
| 22:11 | <shu> | but i guess this doesn't work for your approach because you want to be able to
|
| 22:11 | <shu> | instead of
|
| 22:11 | <rbuckton> | Those are the same thing to me |
| 22:12 | <rbuckton> | Just different levels of abstraction |
| 22:12 | <shu> | they aren't to me, because "import { S } from some place" already evaluates and binds an S that we'd need to correlate after-the-fact |
| 22:12 | <shu> | where as "receive S via message" gets the right S beforehand with no addition coordination needed |
| 22:13 | <shu> | it's the difference between 2 copies that are correlated and 1 copy |
| 22:13 | <rbuckton> | Ok, fair. Then the issue I have with the 2nd approach is one of timing. |
| 22:13 | <shu> | right, there is a conceptual startup barrier for all workers |
| 22:14 | <rbuckton> | And some workers might want to be able to create instances of a shared struct type ahead of the handshake process or message or whatever |
| 22:14 | <rbuckton> | Because sometimes I need to set up singleton values or run code against objects that also happen to be shared. |
| 22:14 | <shu> | yeah, that style is explicitly unsupported, or at least will always need to be reordered after the handshake barrier |
| 22:15 | <rbuckton> | With the approach I've been suggesting, it doesn't. |
| 22:15 | <rbuckton> | I'm already doing that, sans behavior, currently. |
| 22:15 | <shu> | i still don't understand how the correlation works |
| 22:15 | <shu> | are you free? maybe we can hop on a 30 minute call and talk it through |
| 22:15 | <rbuckton> | Sure |
| 22:15 | <shu> | i'll DM |