11:43
<Luca Casonato>
My major open question in response to bakkot's points at plenary is how this qualifies as a new path to eval - we discussed this at the last loader meeting and came to the conclusion that a ModuleSource constructor was in fact not a new path to eval, because the "evalness" of the ModuleSource is done through the existing dynamic import. The constructor itself does not eval - eval only happens at import - which is already possible right now.
11:44
<Luca Casonato>
I would still like to invite bakkot to a loader meeting so we can discuss this in detail - I am likely missing something, or I am not understanding your concern correctly.
15:30
<littledan>
I think this is discussed more concretely as we discuss mitigations. Clearly the whole thing (ModuleSource + import()) put together was a path to eval. I guess the question was whether bakkot would accept a mitigation which was, "ModuleSource produces marked things which cannot be passed to import() but otherwise work" (but I'm not so convinced the feature is very useful, and we were discussing this above)
16:04
<bakkot>

Luca Casonato: as littledan says, ModuleSource + dynamic import is eval, to my view. yes, in some (but not all) hosts it possible to use data URIs with dynamic import already, but a.) that is not widely supported especially given CSP and so is not a reliable way to eval and b.) data URIs are very obviously a kludge, whereas a ModuleSource constructor would be a first-class part of the language, and we should be concerned about making paths to eval more usable, not just whether they exist at all.

also it is not at all clear to me what the use case for ModuleSource is other than eval; the cases discussed about were all either eval-like or a desire from a single library to have a built-in way of parsing out bindings. I think an affirmative case needs to be made for this feature (as with any feature), and to convince me personally that case needs to be something other than "we would like it to be easier to do eval" (or there needs to be an extremely strong reason to make it easier to do eval).

17:39
<littledan>
<weak-argument>well, it's eval for a module, which is otherwise more or less a missing capability</weak-argument>
17:39
<Kris Kowal>
I would ideally like to find a motivation for this path to eval that does not require others to buy into the SES group’s long-term motivation to make a safe path to eval, that does not rest on CSP, since I know that is contentious. But, that is personally my motivation. Making an easy path to lift text makes it easier to build a sandbox, and making that part of the language makes it easier to write portable sandboxes. One of the drawbacks of the compartment shim is that we have to censor the words eval and import from source text in order to enforce confinement. There are false positives that a native implementation wouldn’t suffer from.
17:40
<Kris Kowal>
Making it eaiser to make sandboxes has the emergent effect of making sandboxes safer.
17:41
<Kris Kowal>
And given that the design is compatible with CSP, I feel like this is a win-win.
17:41
<littledan>
Kris, I'm glad you're describing your actual use case, as this makes it a lot easier to follow
17:41
<littledan>
It's quite hard for me to follow a use case argument which centers on the censorship mechanism, since I really don't understand what sorts of restrictions you have in how you evolve that.
17:41
<littledan>
maybe you could give some more details there?
17:43
<Kris Kowal>
Yeah, in the SES shim, confinement is enforced at runtime and it’s a production performance and developer-experience requirement that we not entrain rigorous static analysis or on-the-fly transformations.
17:44
<Kris Kowal>
The heart of the confinement mechanism points four of JavaScript’s sharpest edges at each other: We use use direct eval in sloppy mode inside a with block that puts an opaque proxy on the stack.
17:44
<Kris Kowal>
That’s…a summary.
17:45
<Kris Kowal>
In any case, there are a number of obvious ways to escape lexical containment.
17:46
<Kris Kowal>
Collectively “undeniable intrinsics”. So, for example, (async()=>{}) gives you access to the async function prototype. That’s pretty straightforward to confine with the hardening of shared intrinsics.
17:47
<Kris Kowal>
In the most basic case, {} and [] give you access to shared intrinsics in ways that can’t be denied with the lexical environment.
17:47
<Kris Kowal>
Likewise, import escapes confinement. So, we use a careful regex to identify any pattern that might be an invocation of import and refuse to evaluate such programs.
17:48
<Kris Kowal>
The Evaluators proposal would allow us to hook the behavior of import for confined programs.
17:49
<Kris Kowal>
We also ban direct eval similarly, but not so much because of confinement concerns, but because it is impossible to faithfully emulate the intended behavior in the shim.
17:52
<Kris Kowal>
In order to support modules, we have no choice but to convert modules into programs at compile time, which is a source-to-source transformation that reduces debuggability. We also eschew source maps because they can be used to confuse auditors.
17:53
<Kris Kowal>
So we have a module-to-program transform, which necessarily generates both the module functor source code and the bindings in a JSON envelope, and a runtime that handles these “module sources” reconstructs the linkage from the declared bindings and a sort of calling convention.
17:55
<Kris Kowal>
The net result is a Zip file that contains JSON enveloped sources which is not as debuggable or reviewable as we’d like. This is the artifact that must be trusted when you grant capabilities to the guest program, so this is the artifact that gets fingerprinted for integrity checks. It’s also, notably, not hosted on the web, and modules not hosted on the web are not currently possible to evaluate across all JavaScript hosts.
17:56
<Kris Kowal>
There are hacks, but they are hacks, and hacks and confinement are somewhat inimical.
17:58
<Kris Kowal>
And again, our position is not that we hope to capture all JavaScript in hardened JavaScript. Our position is that it should be possible to get to hardened JavaScript from ordinary mutable implicitly permissive one-big-sandbox JavaScript. We in fact require that as a starting point since anything else would preclude evolution of the host environment over time.
18:29
<Luca Casonato>

bakkot: so your concern is not that it is a new path to eval, but rather that it may make an existing path to eval more ergonomic?

As for the usecases that can justify an unconstructable ModuleSource:

  • multiple instantiation modules (including module expressions & declarations)
  • low level script-src: no-eval module passing between workers
  • manual instantiation and module instrospection (we've seen this in WASM using WebAssembly.Module, for which ModuleSource would be the JS equivalent)
18:34
<bakkot>
Luca Casonato: it is a new path to eval in many contexts (e.g. anywhere with CSP, and therefore in any library), and also it makes an existing path to eval more ergonomic; both are concerns are important to me
18:35
<Kris Kowal>
I do not think that bakkot objects to the concept of an object that represents a module source, which has intersection semantics relevant to other proposals and does not imply a path to eval. Correct me if I’m wrong.
18:36
<bakkot>
right - none of my eval concerns apply to ModuleSource which does not take a string argument; I am fine with a static import syntax which gives you an object you can clone between workers and so on
18:36
<Kris Kowal>
Oh, bakkot our intention is not to be a new path to eval with CSP. no-unsafe-eval would render a ModuleSource instance lifted from text inert.
18:37
<Kris Kowal>
right - none of my eval concerns apply to ModuleSource which does not take a string argument; I am fine with a static import syntax which gives you an object you can clone between workers and so on
This is to say we don’t need to convince bakkot of importing module sources or module blocks with sources, again correct me if I’m wrong.
18:38
<bakkot>
there are at least some CSPs which allow no-unsafe-eval but do not allow data URIs (primarily because there are libraries with this requirement), so constructible ModuleSource + dynamic import would still constitute a new way to eval in such contexts
18:38
<Kris Kowal>
By inert I mean that import(new Module(new ModuleSource(''))) would unconditionally return a rejected promise under a no-unsafe-eval CSP, and would poison any module that takes it as a transitive dependency as well.
18:39
<Kris Kowal>
Otherwise I would agree that would constitute a new path to eval in situations that should have none.
18:40
<Kris Kowal>
there are at least some CSPs which allow no-unsafe-eval but do not allow data URIs (primarily because there are libraries with this requirement), so constructible ModuleSource + dynamic import would still constitute a new way to eval in such contexts
Is there a way to better communicate an intention that constructible module sources must not create a new path to eval under any existing CSP that should disallow it?
18:41
<bakkot>
well, if your CSP allows no-unsafe-eval, then it should allow eval, but I still don't think it is a good idea for the language to provide a new way to eval in that context
18:42
<bakkot>
and import is not a way to eval in that context, which is good
18:45
<Kris Kowal>
I mean a CSP that disallows use of eval, to be clear.
18:45
<bakkot>
I agree that if we do end up with a constructible ModuleSource it must not be importable in a no-unsafe-eval context; I don't think there was any confusion about that
18:45
<Kris Kowal>
There’s a strong possibility I don’t know CSP well enough to have this conversation. I’m hoping for instruction.
18:46
<bakkot>
the relevant fact is that no-unsafe-eval does not restricted use of data: URIs; those are controlled by the use of a data: scheme expression in the CSP
18:46
<bakkot>
so it is possible to have CSP in which eval is allowed and data: URIs are not, in which context ModuleSource + dynamic import is unambiguously a new path to eval
18:47
<Kris Kowal>
Is there a way we can frame it such that it would not be?
18:47
<bakkot>
not that I can think of?
18:48
<bakkot>
though all this discussion about CSP is all a little bit outside my main objection. I agree that it is technically true that in at least some contexts import can be used for eval because of the existence of data URIs, though it is also even more technically true that this does not not apply in all contexts in which ModuleSource would work (if I understand correctly). however, I don't think this technical point is all that important; even granting it, it seems to me that import + constructible ModuleSource would a new first-class path to eval in a way that import('data:') is not , the latter being kludgey and not universally acceptable or available.
18:49
<Kris Kowal>
That is, how does one describe all CSPs in which import(new Module(new ModuleSource(text))) must fail?
18:50
<Kris Kowal>
Yeah, I’m not interested in any contexts where import('data:') is a workaround for eval CSP restrictions except insofar as that the proposal should not introduce loopholes in CSP.
18:50
<bakkot>
and while I am not dead-set against having a new or new-ish first-class path to eval I would want it to have a strong justification, which I have not heard yet. whether this is actually new or merely new-ish is not all that important to me - either way I still would want there to be a strong justification.
18:50
<Kris Kowal>
I am interested in environment in which there are no such workarounds, ones where import(doesNotEvenTakeURL).
18:50
<bakkot>
That is, how does one describe all CSPs in which import(new Module(new ModuleSource(text))) must fail?
those which forbid unsafe-eval, I think is an accurate description
18:51
<Kris Kowal>
So it is possible that the CSP tangent was entirely us talking past each other. Thanks.
18:52
<Kris Kowal>
If I read correctly, we’re generally in agreement that ModuleSource(text) would not introduce a CSP loophole.
18:52
<bakkot>
right
18:53
<Kris Kowal>
and while I am not dead-set against having a new or new-ish first-class path to eval I would want it to have a strong justification, which I have not heard yet. whether this is actually new or merely new-ish is not all that important to me - either way I still would want there to be a strong justification.
This is my prior understanding and I suggest we focus on articulating a strong justification.
18:54
<Kris Kowal>
@bakkot You would not happen to have seen the bits about confining JavaScript guests without CSP above before they scrolled away?
18:54
<bakkot>
(the bit about CSP was just to say that import + data: URIs does not already constitute a path to eval in all contexts - there are CSPs which allow eval but do not allow importing data: URIs, so adding constructible ModuleSource would be in fact granting a path to eval via import which did not previously exist in that context. but as I say this is all kind of a technical point rather than my main objection.)
18:57
<bakkot>
@bakkot You would not happen to have seen the bits about confining JavaScript guests without CSP above before they scrolled away?
I did read it but am lacking context to understand why your design requires the ability to eval modules from within a module, as opposed to having a compartment or something which would allow you to hook import within that context and provide a module that way
18:58
<Kris Kowal>
We certainly need both. The key is that the platform is backed by zipped applications (no URLs and no fetch capability)
18:59
<bakkot>
Why does the import hook on a compartment (or whatever compartments are called now) not suffice here?
19:00
<Kris Kowal>
The import hook returns Module instances with ModuleSources, from text dug out of Zip files.
19:01
<bakkot>
That's one possible design; certainly there are other designs for import which don't require a constructible ModuleSource, no?
19:01
<bakkot>
you could have a defineImport(specifier: string, source: string) capability available to the thing which created the compartment, or something, no?
19:02
<bakkot>
that would still be eval-like, but I am a lot less concerned about eval which is specifically defined in terms of compartments and which is defined from the outer compartment for use by the inner compartment, as opposed to one which is ambiently available
19:02
<bakkot>
(forgive me if I'm using the wrong terminology here)
19:17
<Kris Kowal>
That’s an interesting clarification.
19:29
<Luca Casonato>
there are at least some CSPs which allow no-unsafe-eval but do not allow data URIs (primarily because there are libraries with this requirement), so constructible ModuleSource + dynamic import would still constitute a new way to eval in such contexts
What CSP actually prevents import("data:...")? A no eval script-src doesn't in Chrome 👀 (example: https://dash.deno.com/playground/csptest)
19:33
<Luca Casonato>
also if i specify default-src 'none' - and same also in FF
19:34
<Kris Kowal>
that would still be eval-like, but I am a lot less concerned about eval which is specifically defined in terms of compartments and which is defined from the outer compartment for use by the inner compartment, as opposed to one which is ambiently available
Yeah, re terminology, by ambiently available, do you mean available by reference? An ocap interpretation of the term ambient would mean that it is registered to the parent host and available to all peers by a forgeable name.
19:48
<Luca Casonato>
What CSP actually prevents import("data:...")? A no eval script-src doesn't in Chrome 👀 (example: https://dash.deno.com/playground/csptest)
Oh apparently nonce'd script tags have an implicit `script-src 'strict-dynamic'' behavior for imports - TIL!
19:48
<bakkot>
What CSP actually prevents import("data:...")? A no eval script-src doesn't in Chrome 👀 (example: https://dash.deno.com/playground/csptest)
nonces are automatically inherited by imports, I think? try unsafe-inline instead of nonces in the CSP
19:50
<Luca Casonato>
yup, ok with that I can make a CSP that can do eval, but no import("data:..."): script-src 'unsafe-inline' 'unsafe-eval'
19:50
<Luca Casonato>
So your concern is that module source would allow import() to eval with just unsafe-eval specified, whereas right now you need script-src data: or script-src blob:?
19:50
<bakkot>
Yeah, re terminology, by ambiently available, do you mean available by reference? An ocap interpretation of the term ambient would mean that it is registered to the parent host and available to all peers by a forgeable name.
I mean like any library can use this capability to eval in the current context, rather than only being able to do eval inside of a specially-constructed box
19:52
<bakkot>
So your concern is that module source would allow import() to eval with just unsafe-eval specified, whereas right now you need script-src data: or script-src blob:?
well, again, that is a technical point. yes, I am concerned a little bit about that, but the larger concern is that I do not regard data: URIs as being a first-class path to eval built in to the language, for this and other reasons, while ModuleSource would be.
19:52
<Luca Casonato>
i see - thank you for the clarification.
19:56
<Luca Casonato>
For all intents and purposes contexts with dynamic import and data URLs would have a constructable (all be it async) ModuleSource in the form of (await import("data:application/javascript,abc", { reflect: "module" }).source anyway, so for me the lack of an explicit ModuleSource constructor is not the end of the world
19:57
<Luca Casonato>
And in these contexts this factory would be subject to CSP - so no new path to eval here (the exact same CSP a direct import("data:...") is subject to today)
19:57
<Kris Kowal>
So, Module in this proposal is providing exactly as limiting a scope as Compartments, even if Compartments are framed as a registry of source strings. If you’ll forgive a reductio absurdam, const c = new Compartment({ importHook(specifier) { return import(specifier, {reflect: true}) } ); c.registerSource('name', text); c.import('name') is equivalent. The key here is that Module is a box.
19:58
<Kris Kowal>
My hope is that Module is in fact as much a box as the box you like in Compartments! :-)
20:00
<Kris Kowal>
And Evaluators give us another box, where we can capture the dynamic import behavior for non-modules. Out of these primitives, Compartment can be implemented in user code.
20:00
<Kris Kowal>
And hardened JavaScript can be faithfully implemented in user code, as it cannot with a shim.
20:00
<bakkot>
So, Module in this proposal is providing exactly as limiting a scope as Compartments, even if Compartments are framed as a registry of source strings. If you’ll forgive a reductio absurdam, const c = new Compartment({ importHook(specifier) { return import(specifier, {reflect: true}) } ); c.registerSource('name', text); c.import('name') is equivalent. The key here is that Module is a box.
again, I'm concerned not just with whether the thing is possible, but how accessible it is. I agree that some designs for compartments can be bludgeoned into being a current-context eval but that doesn't mean all possible designs are equivalent.
20:02
<bakkot>
eval already exists, but we've largely been able to convince people not to use it. I don't want to make a new eval and leave it lying around in another easily accessible form.
20:02
<Kris Kowal>
Well, if you’re satisfied by some API impedance, that’s overall good news since we care more about what’s possible than how coherent it is.
20:03
<Kris Kowal>
And Compartments are not all that cumbersome.
20:03
<bakkot>
I am certainly more comfortable with less accessible new ways to eval but not automatically entirely comfortable with them
20:04
<Kris Kowal>
Would this be a sufficiently inaccessible affordance: new Compartment({ importHook(specifier) { return { source }; } }).import('specifier')?
20:04
<bakkot>
though in addition I think if Compartments the use case then a design which exposes the capability only to compartments is more palatable for that reason
20:05
<bakkot>
That would make me feel a lot better than import(new Module(new ModuleSource(source))), certainly
20:09
<bakkot>
Though I would still be hesitant to add that if the only use case is your source-from-zip-files thing
20:10
<Kris Kowal>

So we would recover the ergonomics in user code like:

const loadModuleSource = async source => new Compartment({ importHook: () => ({ source }) }).import('.', { reflect: true });
20:11
<bakkot>
Why do you want to recover the ergonomics?
20:11
<Kris Kowal>
zip-files in this case represent a class including databases, local storage, &c, if that makes any difference in substance.
20:11
<bakkot>
Not really.
20:12
<Kris Kowal>
And in all these cases, the point is that it’s possible to draw a circle around a working set as a reviewable, confineable artifact, which would not otherwise be possible to isolate in this way.
20:12
<bakkot>
that is, those things are not a use case on their own - if you want to eval code from local storage, my question will be why do you want that
20:13
<bakkot>
I think I understand your particular use case, which makes cloning an opaque object to a Worker not viable, but am not convinced by that use case alone
20:13
<Kris Kowal>
In our case, the artifacts are smart contracts, responsible for handling money on behalf of multiple parties.
20:14
<bakkot>
Right, which is to say, an extremely unusual case, unlikely to generalize to something needed by other applications
20:15
<Kris Kowal>
Doug Crockford used to refer to the generalization of the case as mash-ups.
20:16
<bakkot>
Doug Crockford talked about wanting to be able to shell out to a separate process and communicate code to that process in a transparent representation?
20:18
<Kris Kowal>
Hardly so specifically, but Doug built AdSafe at Yahoo, which does isolate and evaluate arbitrary guest programs.
20:20
<bakkot>
Program isolation is mostly orthogonal from eval - generally one can serve the guest programs from a server, as programs, and not need to eval them (assuming the language has isolation abilities at all, which would need to be built out anyway, as we are doing with realms and so on)
20:21
<bakkot>
Your particular use case prevents this but, again, your particular use case seems to me to be extremely unusual
20:22
<bakkot>
So I would still want to hear other use cases before adding this feature, as I would for any feature, although they can be somewhat less compelling than I would want for a new easily-accessible path to eval