Skip to content
This repository was archived by the owner on Nov 3, 2021. It is now read-only.
This repository was archived by the owner on Nov 3, 2021. It is now read-only.

Inadvertent exporting of Wasm functions #31

Closed

Description

@fgmccabe

The anyref/anyfunc allows a function to exported from a WASM module dynamically - as part of the returned value of another exported function. Similarly for imports.
This effectively blows apart any discipline for what is exported from a WASM module. It also complicates implementation because potentially every WASM function may become accessible from JS.

The table.set operator in conjunction with indirect calling of functions from any table allows monkey-patching of functions.

Activity

  1. lukewagner commented on Feb 14, 2019

    @lukewagner
    Member

    Technically it's not any function; just functions that are statically referrenced by ref.func. Tables are already readable/writable from JS (via Table.prototype.get and set), so once a ref.funcd function is stored in a table, it's no different than if it was added to the table via elem segment.

    Maybe the issue you're getting at, though, which I do think we should consider, is that, in the MVP, we know all the external (exported or stored in an external table) functions before the code section starts, whereas with ref.func, we'll only know this by the end of the code section. This could negatively impact streaming parallel compilation. Given that we explicitly moved the elem section from after to before the code section (way back when) specifically so that it was known before the code section, we might want to consider adding some sort of "aliasable" section which declares those functions (and later, other kinds of definitions too!) which were allowed to be ref.funcd.

  2. titzer commented on Feb 15, 2019

    @titzer

    I agree with @lukewagner here. It'd be nice to know up front which functions are used in a first-class way, before the code section.

  3. rossberg commented on Feb 19, 2019

    @rossberg
    Member

    So, if I was a lazy producer that simply put all their functions into this section, what would be the penalty?

  4. titzer commented on Feb 19, 2019

    @titzer

    On V8, the penalty is that we generate wrapper code per signature (essentially a machine code stub whose complexity is proportional to the number of arguments) plus a JavaScript function per Wasm function (minimum two objects--a JSFunction and a SharedFunctionInfo). We're working to make the generation of those wrappers cheaper (and happen in parallel), but the JSFunctions we'd need to do lazily to avoid the upfront cost.

  5. rossberg commented on Feb 21, 2019

    @rossberg
    Member

    Hm, how difficult would a lazy approach be for an implementation?

    An extra section adds somewhat non-trivial language complexity in this case: we would need to decode that section, validate it, extend static function contexts to track the information whether a func is "first-class", employ that to validate uses of func.ref, etc. All doable, but somewhat ugly and ad-hoc.

    Shame, I suppose a custom section for this sort of (primarily JS-relevant?) optimisation hint would not cut it, since you cannot rely on its being accurate.

  6. lukewagner commented on Mar 12, 2019

    @lukewagner
    Member

    Indirect callability could end up influencing codegen for streaming non-JS hosts as well. I would think, spec-wise, this could be handled as a purely binary encoding detail, similar to how we did with the data segments up-front size declaration.

  7. rossberg commented on Mar 14, 2019

    @rossberg
    Member

    Yeah, I'm not sure. The "binary encoding detail" approach already became slightly hacky for the simple data count section. I suspect it would be borderline for something like this, which requires more involved validation. I'll think about it.

  8. changed the title [-]Inadvertent exporting of WASM functions[/-] [+]Inadvertent exporting of Wasm functions[/+] on Apr 3, 2019
  9. lukewagner commented on Apr 24, 2019

    @lukewagner
    Member

    I'm about to implement this; which means either harvesting ref.funcd func-indices after-the-fact or adding a new declarative section, so it'd be nice to figure this out.

    I still think we should maintain the general property that all aliased functions are known before the code section. I realized that aliased functions are not just externally callable (requiring entry stubs, which should be lazily generated for a couple of reasons) but also indirectly callable (since any funcref can be put into a table). For some impls, indirect calls may have a slightly different ABI with a second prologue that immediately precedes the normal prologue of indirectly-callable functions. Without an up-front declaration, streaming/parallel compilation would need to conservatively give every function an indirect call prologue, increasing codegen size. It's a modestly-compelling example, but given that we've already ensured this property in the MVP, I think it makes sense to continue to ensure it when adding features.

    Any new thoughts @titzer @rossberg ?

  10. rossberg commented on Apr 25, 2019

    @rossberg
    Member

    Ah, thanks for the reminder. I have been throwing this around in my head for a while and so far think that a clean and forward-looking solution involves the following:

    • Adding a "reference section" to the binary format. Its structure follows that of the export section, in that it can (potentially) list entities of different sort. For now we would only allow functions.

    • Extending the abstract syntax with "reference declarations", again similar to export declarations.

    • Validation would check that (a) all reference declarations refer to valid indices, and (b) that every use of ref.func (or similar instructions in the future) only mentions indices declared as references. To that end, validation would require to track the set of declared reference indices as an additional context component.

    • There would probably be a bunch of ad-hoc exceptions for uses of ref.func outside function bodies, i.e., in element segments, which don't require a priori declaration?

    • Since they become part of the AST, reference declarations would also need to occur in the text format, which, among other things, allows writing proper tests for this feature. We could add the same shortcuts as for exports.

    I also have considered the idea of only making it a detail of the binary format. But it seems that would be stretching it too far in this case, since the validation conditions are substantially more extensive than just comparing two numbers.

    WDYT?

  11. rossberg commented on Apr 25, 2019

    @rossberg
    Member

    FWIW, another option might be to piggyback on (and abuse) segments and the validation exception that we'll probably need to introduce for them anyway. That is:

    • Validation requires that ref.func only mentions functions that are also referenced inside an element segment. (For ref.func occurring in segments this condition holds vacuously.)

    • To avoid redundant allocation for auxiliary forward declaration segments, introduce a third kind of segment, unused (or declared), which is like a passive segment in that it does not perform any initialisation, but like an active one in that it is dropped implicitly.

    This solution is a bit more hacky, but would require fewer extensions to AST, binary, and text format (only adding the new segment kind).

  12. lars-t-hansen commented on Apr 25, 2019

    @lars-t-hansen
    Contributor

    I think using element segments came up before but was shot down as too hacky. The declared bit (expressible more to the point?) reduces the hackiness by quite a lot, I think. If other kinds of values require a reference section in the future so be it, but until then we keep the complexity down.

  13. rossberg commented on Apr 25, 2019

    @rossberg
    Member

    Well, I wouldn't want to end up in a future where we have both. If we go for the segment approach, then we should design it such that the new segment kind is extensible to other reference kinds later.

  14. lukewagner commented on Apr 25, 2019

    @lukewagner
    Member

    @rossberg Your "reference section" idea is exactly what I was imagining.

    Regarding binary-encoding vs. validation: I think it'd be kindof a pain to have to forward-declare references in the text format, but if we have them implicitly declared/unified (as we do with function types), then we won't be able to write negative tests. If the distinction is not present in the text format, I'd be inclined to keep it in the binary format as well, but no strong opinion here.

    Regarding the elem section idea: scanning the elem section encoding just now, it's already a bit hairy, so I'd be reticent to add another case. Plus, it raises some questions without very satisfying answers:

    • Is every index used by the elem section implicitly declared referenceable or is it just the indices in the new "declared" segments? If the former, we conflate "stored in table" with "first-class referenceable" (the former can be better-optimized in some cases); if the latter then we still have the special-case exception for indices in the non-"declared" elem segments.
    • Are the referenceable indices stored as constant expressions or just indices? If the former, then the size is ~3x and it also doesn't really feel like a declaration. If the latter, then it's not really symmetric with the other 3 cases of elem segments.
  15. rossberg commented on Apr 25, 2019

    @rossberg
    Member

    @lukewagner, wasn't the text format meant to be a faithful representation of the AST, plus some sugar? That implies that everything expressible in the AST should be expressible in the text format. Making ref decls implicit would violate that (unlike implicit function types, which are just a shorthand).

    Analogous to export, I'd imagine a declaration

    (ref (func $f))
    

    plus the shorthand

    (func $f (ref) (param i32) ...)
    

    Regarding elem sections, I think question (1) may have to be answered either way: does the ref section have to repeat indices already mentioned in a segment? If no then the segment approach would actually make that more uniform. For (2), I was imagining something like externkind vec(idx), closer to the existing format of active segments.

    I don't understand the distinction you are making between "stored in table" and "first-class". Under this proposal, you can always emulate ref.func via elem section + table.get, so how could an engine treat these cases differently? (Without analysing all the code, because then you wouldn't need the forward declaration in the first place.)

    But in general, I agree that a proper reference section would be cleaner.

  16. 20 remaining items

  17. binji commented on May 13, 2019

    @binji
    Member

    If the whole point of elem segments is size...

    I don't think it is -- table.init also simplifies to a single bounds-check without requiring VM optimization.

    It seems like, for there to be a meaningful size downside to having definition-kind-grouped segments, we'd need a situation with a large number of interleaved definition kinds being written into a heterogeneous (anyref) table; I'm having a hard time imagining the use case so it seems premature to optimize for it.

    I can see ref.null being used this way.

    Well, a more important goal is keeping data and elem segments alike, so that would only make sense if we punted on both. ;)

    OK, but we could do that for bulk-memory anyway, since MVP only allows 1 memory, 1 table. TBH, I'd be OK with this change.

  18. lukewagner commented on May 13, 2019

    @lukewagner
    Member

    If the whole point of elem segments is size...

    I don't think it is -- table.init also simplifies to a single bounds-check without requiring VM optimization.

    Yes, but using the same manner of argument as with size: if it's removing branches we're optimizing for, performance would be further improved by removing the branching on the definition kind from the inner loop of table.init by grouping indices by definition kind. Practically speaking though, I assume that, as a run-once, load-time op, table.init isn't being optimized and this distinction won't be meaningful to overall program performance (especially once table.set+ref.fuc are properly inlined, which, as ops intended to be used on hot import/export call paths, they should be).

    It seems like, for there to be a meaningful size downside to having definition-kind-grouped segments, we'd need a situation with a large number of interleaved definition kinds being written into a heterogeneous (anyref) table; I'm having a hard time imagining the use case so it seems premature to optimize for it.

    I can see ref.null being used this way.

    But frequently interleaved throughout an otherwise contiguous segment? Note that this isn't even possible in the MVP's elem segment. If nothing else, this seems like a waste of binary size.

  19. binji commented on May 13, 2019

    @binji
    Member

    Let's discuss this tomorrow in the CG meeting: WebAssembly/meetings#405

  20. lukewagner commented on Aug 6, 2019

    @lukewagner
    Member

    Just to confirm, is the consensus above that for a ref.func $f to be valid, $f must be used or declared in some elem segment (active/passive/declared) and, in particular, this does not include functions used in the exports section or the start section?

  21. rossberg commented on Aug 8, 2019

    @rossberg
    Member

    Yes, $f must occur in an elem segment. It doesn't count to (only) have it occur as an export or start function.

  22. lars-t-hansen commented on Nov 15, 2019

    @lars-t-hansen
    Contributor

    @rossberg, we can close this. Even though @fgmccabe opened it to discuss something else, it is all about the declared element segments, and those have landed.

  23. rossberg commented on Nov 15, 2019

    @rossberg
    Member

    Actually, declared segments haven't landed yet, neither in the interpreter nor in the spec nor are there tests. I have an earlier version of them on a branch, but need to rebase on the bulk proposal, since that significantly changed the segment representation more recently. Discovered various holes in the spec & implementation of that proposal on the way that had to be fixed first.

    I'll try to get to it next week.

  24. rossberg commented on Jan 16, 2020

    @rossberg
    Member

    Fixed via #73.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions