Design: Proposta: Sintaxe de resolução de importação para hosts wasm nativos

Criado em 19 jun. 2019  ·  25Comentários  ·  Fonte: WebAssembly/design

Eu gostaria de propor um mecanismo de sintaxe e resolução para instruções de importação em hosts wasm nativos (então, por enquanto, principalmente WASI e talvez node.js).

Justificativa

O modelo ao qual estou aspirando é o que @lukewagner chamou de link compartilhado sem nada ; que chamarei de composição sem confiança nesta proposta.

Nesse modelo, os desenvolvedores wasm podem extrair de um ecossistema de módulos semelhante ao npm; cada módulo é autocontido com suas próprias dependências e geralmente não em pacotes instalados globalmente. Este também é o modelo escolhido pelo snapcraft e flatpack; seu principal benefício é que cada instalação de um programa neste modelo é garantidamente igual, o que é imensamente útil para reportar bugs e evitar problemas de dependência.

(sério, o inferno da dependência é horrível; a conveniência de um sistema de importação que pode mitigar esses problemas não pode ser subestimada)

Uma versão wasm desse modelo também deve ser segura, mesmo diante de código malicioso, daí o nome "composição sem confiança". Um sistema de resolução de importação nativo deve respeitar o princípio básico dos recursos de objeto: código não confiável só pode acessar dados que foram explicitamente passados ​​para esse código. Em outras palavras, sub-sub-sub-subdependencies não devem ser capazes de importar /usr/bin/someprogram.wasm mesmo que o host tenha permissões de sistema de arquivos para esse arquivo.

Agora, importar um módulo wasm diretamente de outro módulo wasm atualmente é praticamente impossível . O uso de módulos wasm juntos requer que algum código de cola seja escrito em um incorporador JS. Há uma futura proposta de integração do esm , mas:

  • A proposta é voltada para embeddings JS, e faz suposições baseadas no ambiente ES.

  • A proposta não especifica um mecanismo de resolução de nome de caminho; supondo que o mecanismo final seja dependente da implementação, a resolução do nome do caminho do node.js não passa o requisito "sem confiança" do wasm.

Resumindo, o wasm precisa de um mecanismo padrão, seguro e não JS para importar módulos de outros módulos. Embora esse padrão possa não ser usado exclusivamente pelo WASI, o WASI cobre a maioria dos casos de uso.

Proposta

Durante a fase de instanciação (por exemplo WebAssembly.instantiate , wasm_instance_new e wasm::Instance::make ), o compilador WebAssembly recebe um objeto raiz opcional do sistema de arquivos e um objeto opcional de mapa de importação.

A raiz do sistema de arquivos geralmente será o diretório no qual o arquivo .wasm está. O objeto de mapa de importação depende da plataforma.

Ao analisar uma string de importação, o compilador aplica o seguinte algoritmo:

  • Verifique se o caminho é um caminho de URI válido (ou algum outro esquema).
  • Verifique se o caminho é relativo e não inclui segmentos . ou .. .
  • Se o primeiro segmento tiver o prefixo @ , mapeie-o para o caminho definido no mapa de importação do host.
  • Se o primeiro segmento tiver o prefixo $ , verifique a pasta atual e todas as pastas pai até a raiz de importação, para arquivos wasi_import_map.json ; retorna na primeira vez que um mapa tem uma entrada correspondente ao segmento.

Importações de host ( @ )

Esta sintaxe é usada para importar a funcionalidade do host, por exemplo, @wasi/fs ou @stdjs/global/Array .

Ele tem a vantagem de não colidir com as convenções de importação JS existentes (desde que "wasi" e "stdjs" sejam reservados como organizações NPM).

Importações mapeadas ( $ )

Essa sintaxe é usada para importar dados do diretório atual, de maneira segura para OCAP.

O princípio fundamental é que, por padrão, um arquivo .wasm só tem acesso aos arquivos em seu diretório atual e subdiretórios; entretanto, um diretório pode ter um arquivo wasi_import_map.json , que declara ganchos que os arquivos .wasm em subdiretórios podem acessar.

Por exemplo:

myWasmModule/
    index.wasm
    foo/
        wasi_import_map.json
        foo.wasm
        foobar/
            foobar.wasm
    bar/
        bar.wasm

No exemplo acima:

  • index.wasm pode importar foo.wasm , foobar.wasm e bar.wasm .
  • foo.wasm e bar.wasm não podem importar um ao outro ou index.wasm .
  • foobar.wasm pode importar foo.wasm se foo/wasi_import_map.json tiver uma chave para esse caminho.
  • foo/wasi_import_map.json não pode dar foo.wasm ou foobar.wasm acesso a index.wasm ou bar.wasm .

O mapeamento de importação tem alguns casos de uso:

  • Permitir que um gerenciador de pacotes aplique a desduplicação de dependência para reduzir os tempos de compilação e melhorar o tamanho do cache de código.

  • Fornecer calços e polyfills.

  • Use bibliotecas diferentes para compilações de depuração versus versões.

Por que não usar os mapas de importação do WICG?

Há um argumento a ser feito de que seria mais simples reutilizar a proposta de mapas de importação do WICG (de agora em diante WIM), e que ter casos especiais para $ e @ é uma complexidade estranha.

Alguns contrapontos:

  • O WIM resolve um problema diferente dos mapas de importação WASI. O WIM é baseado no princípio de que fazer uma chamada de busca em um endereço estático geralmente é seguro e não pode vazar dados. Esta proposta, em vez disso, baseia-se no modelo de segurança OCAP.

  • Ter uma sintaxe separada para importações normais, de host e mapeadas permite que o desenvolvedor e as ferramentas comuniquem sua intenção especificamente; também encoraja mensagens de erro mais legíveis.

Caso de uso da Web

Observe que esta proposta é destinada a intérpretes wasm nativos. A forma como o navegador resolve as instruções de importação é completamente independente desta proposta.

No entanto, se um ecossistema de gerenciador de pacotes fosse desenvolvido em torno dessa estrutura de importação, bundlers como Webpack e Rollup poderiam começar a fornecer uma versão amigável para navegador dessa estrutura, como fazem atualmente com o npm.


De qualquer forma, essa é a minha proposta. Qualquer feedback é bem-vindo :)

Comentários muito úteis

Eu também tenho sentido a necessidade de algo assim. No entanto, sinto que ainda não estamos equipados para especificar nada sem introduzir algum conceito fundamentalmente novo e topologicamente "maior" (que contém vários módulos), como "pacote" (unidade de código em um gerenciador de pacotes como npm) ou "contêiner " (unidade de código que pode ser executada por um container runtime) porque, como apontado acima, não podemos assumir que existe algum sistema de arquivos ambiente cujos caminhos podemos falar. Uma vez que você tenha um conceito de "pacote" ou "contêiner" (ambos acho que fazem sentido, para diferentes fases de implantação), então é muito mais fácil ser concreto sobre como a resolução funciona.

Também compartilho o objetivo do @littledan de que, tudo o que for definido, deve ter a mesma interpretação dentro e fora de um ambiente JS/ESM. Dessa forma, você pode ter um único pacote de módulos wasm que podem ser usados ​​tão bem no Node quanto fora dele.

Todos 25 comentários

e se, em vez de ter uma sintaxe específica, apenas recomendássemos que as implementações wasi apliquem as regras de capacidade do estilo libpreload para importar a resolução? se você tiver a.wasm e b.wasm você pode declarar . ou ./b.wasm no seu manifesto wasi para que você possa importá-lo de a.wasm

isso tornaria o wasi muito mais compatível com outros sistemas aleatórios aos quais ele pode se unir, como navegadores e node.

Gosto da orientação geral desta proposta. Impedir que libs capturem importações arbitrariamente parece importante. Também gosto que esta proposta seja bastante leve. Alguns pensamentos, no entanto.

  • Sua descrição dos mapas de importação está um pouco incompleta. A ideia é que uma entrada de mapa a: b/c permitiria importar b/c com o nome $a ? Se sim, não tenho certeza de como isso permite shimming ou remapeamento de libs, pois o módulo de importação precisa estar ciente de que o nome é (re)mapeado. Por que o prefixo $ é necessário?

  • Seria bom tratar os diretórios de uma maneira mais parecida com o módulo, onde cada diretório também pode restringir os arquivos que "exporta" para o exterior . Esses "mapas de exportação" seriam adicionais aos mapas de importação que definem os ambientes voltados para dentro . Se um mapa de exportação for fornecido, apenas os arquivos/caminhos listados nele podem ser acessados ​​a partir de módulos ou mapas de importação/exportação em diretórios vizinhos ou irmãos.

  • Concordo plenamente com o uso de referências de URI padrão como mecanismo de nomenclatura. No entanto, os prefixos @ ou $ estão abusando um pouco da noção de referência relativa ( @a/b é uma referência relativa em termos de URI, enquanto a semântica atribuída aqui é muito uma forma absoluta de ref).

  • Bikeshedding: Há muito benefício em usar JSON para importar mapas? Conforme você os descreve, eles consistem apenas em uma lista de pares de caminhos, portanto, um formato complexo e confuso como JSON pode ser um exagero para isso.

Se sim, não tenho certeza de como isso permite shimming ou remapeamento de libs

Eu estava pensando em injeção de dependência, onde (por exemplo) dois módulos precisam usar a mesma instância do react podem importar $react e contar com a raiz do projeto para fornecer o mapeamento.

No entanto, agora que penso nisso, isso levanta a questão do que fazer quando dois arquivos/módulos importam o mesmo arquivo/módulo. Eles devem criar duas instâncias separadas ou uma instância compartilhada? Ambas as respostas têm seus casos de uso.

Eu também estava pensando que poderia haver um mecanismo para ter vários arquivos wasm_import_map para depuração, lançamento, testes de unidade, etc, mas isso parece um recurso prematuro. Wasm realmente não precisa se tornar Cmake.

Por que o prefixo $ é necessário?

Ele permite mensagens de erro mais claras e diagnóstico mais rápido de problemas.

Cannot find host module "@foobar"

vs

Cannot find file "some/path/foobar.wasm"

vs

Key "$foobar" not found in imports maps:
- "some/path/wasm_import_map.json"
- "some/wasm_import_map.json"

Não ter prefixos significa que você tem mensagens de erro longas listando todas as possibilidades ou, mais provavelmente, você obtém os mesmos erros de importação vagos que os compiladores C que dizem "Não é possível resolver foobar" e deixam você adivinhar o porquê.

Seria bom tratar os diretórios de uma maneira mais parecida com o módulo, onde cada diretório também pode restringir os arquivos que "exporta" para o exterior.

Eu não acho que exportar mapas seria tão útil. Um módulo que deseja exportar, digamos, um submódulo "bindings" e um submódulo "network" poderia ter apenas arquivos bindings.wasm e network.wasm na raiz.

Um mecanismo para restringir a importação de diretórios pai é necessário para a segurança do sistema de arquivos, mas você não precisa (e praticamente não pode) restringir a importação de diretórios filhos.

Há muito benefício em usar JSON para importar mapas?

Na verdade, não. Eu estava pensando em protobuff também, mas na verdade, qualquer formato poderia servir. Só precisa haver um ou dois formatos padrão.

Eu estava pensando em injeção de dependência, onde (por exemplo) dois módulos precisam usar a mesma instância de react pode importar $react e contar com a raiz do projeto para fornecer o mapeamento.

Você pode indicar parâmetros via $ via convenção de nomenclatura, o que não implica em nenhuma necessidade de preparar isso como um requisito sintático. Obviamente, nem todos os usos seriam parametrização (também conhecido como "injeção de dependência", como algumas almas confusas a renomearam :) ). Para shimming, acho que você precisará remapear módulos arbitrários, incluindo os do sistema (ou seja, nomes começando com @ no que você descreve).

As mensagens de erro parecem um problema solucionável. Especialmente porque o sistema que você descreve é ​​bastante simples, ou seja, o nome só pode ser encontrado em qualquer um dos diretórios pai ou em um mapa nele.

Eu não acho que exportar mapas seria tão útil. Um módulo que deseja exportar, digamos, um submódulo "bindings" e um submódulo "network" poderia ter apenas os arquivos bindings.wasm e network.wasm na raiz.

Isso impediria alguém de substituir a intenção de importar módulos de mapeamento em diretórios aninhados?

As mensagens de erro parecem um problema solucionável.

Bem, sim, eles também custam #include e, no entanto, solucionar um problema de inclusão de biblioteca com falha pode ser surpreendentemente difícil.

De qualquer forma, acho que há um valor inerente em forçar o usuário a especificar sua intenção. Isso cria um poço de sucesso para os projetistas de ferramentas, que sabem o que o usuário quer sem ambiguidade; e permite aos usuários intuir mais facilmente como a cadeia de ferramentas funciona.

Para shimming, acho que você precisará remapear módulos arbitrários, incluindo os do sistema (ou seja, nomes começando com @ no que você descreve).

Re módulos arbitrários, eu escrevi especificamente a proposta para evitar isso. Se você possibilitar o shim de qualquer importação da maneira que os mapas de importação do WICG permitem, você torna o design subjacente menos robusto, menos KISS e se abre para muitos erros / explorações de sombreamento de nomes.

(isso não é uma crítica aos mapas de importação WICG; como eu disse, eles abordam diferentes casos de uso)

Acho que a parametrização de injeção de dependência é suficiente para a maioria dos casos de uso de shimming e, para shimming mais intrusivo, você sempre pode ter uma ferramenta "manualmente" para substituir alguns arquivos por outros.

Re: módulos do sistema, eu estava pensando que o shimming deles seria feito no nível da API WebAssembly.instantiate , já que o caso de uso mais comum seria para testes de unidade.

A sintaxe sempre pode ser mesclada com $ , mas, novamente, há valor em expressar a intenção.

(além disso, na pior das hipóteses, deprecamos o @ e $ e os tornamos uma convenção de nomenclatura opcional; a progressão oposta quebra a compatibilidade)

Isso impediria alguém de substituir a intenção de importar módulos de mapeamento em diretórios aninhados?

Não, mas não tenho certeza se isso é um problema.

No que diz respeito a substituir a intenção do escritor do módulo, bem, essa é a prerrogativa do usuário do módulo. Se eles querem mexer nas partes internas de um módulo, eles o fazem por sua conta e risco, e podem fazê-lo bifurcando o módulo de qualquer maneira.

No que diz respeito à segurança do sandbox, acho que pode ser um problema se um módulo malicioso importar algum módulo externo compartilhado com outros programas, de uma maneira que quebre invariantes (por exemplo, em vez de importar /usr/bin/game_engine/save_progress , o código malicioso importa /usr/bin/game_engine/save_progress/unsafe_filesystem_functions ).

Provavelmente, existem soluções alternativas para isso, e não acho que haja muitos vetores de ataque possíveis.

Mas acho que se estou buscando uma abordagem de "poço de sucesso" e "sandboxing seguro", que é o ponto de ocap e WASI, isso também precisa ser considerado.

o nome só pode ser encontrado em qualquer um dos diretórios pai ou em um mapa nele.

Um mapa nele, sim, qualquer diretório pai, não.

Acessar arquivos em diretórios pai quebra o sandboxing e não é permitido.

Um arquivo .wasm só pode acessar:

  • .wasm arquivos em seu diretório
  • .wasm arquivos em subdiretórios
  • wasm_import_map.json arquivos em diretórios pai, até a raiz de importação

Os próprios mapas de importação obedecem às mesmas regras, podendo apenas acessar arquivos wasm em subdiretórios e importar mapas em pais.

Acho que há um valor inerente em forçar o usuário a especificar sua intenção.

Concordo muito, mas neste caso parece impedir prematuramente os casos de uso válidos associados a uma intenção diferente.

No que diz respeito a substituir a intenção do escritor do módulo, bem, essa é a prerrogativa do usuário do módulo. Se eles querem mexer nas partes internas de um módulo, eles o fazem por sua conta e risco

Sim... Já ouvi esse argumento antes ;). Infelizmente, não é assim que a teoria dos jogos funciona na prática. Se não houver maneira de expressar explicitamente e (até certo ponto) impor tal intenção, alguém a substituirá, porque nada os impede. E é preciso apenas um cliente popular para fazer isso e, de repente, o redator do módulo percebe que todo o ônus está sobre eles para manter a API não oficial ou obter todo o fogo dos clientes transitivos por sua atualização quebrar seu código. Estive lá, fiz isso; é uma situação que um sistema bem projetado deve ajudar a evitar.

O duplo para o bloqueio das importações é o bloqueio das exportações. Os módulos Wasm suportam intencionalmente o encapsulamento adequado por meio de ambos, o mesmo deve ser verdadeiro um nível acima.

Acessar arquivos em diretórios pai quebra o sandboxing e não é permitido.

Certo, eu quis dizer os mapas de importação apenas nos diretórios pai, o "ou" era uma redação desleixada.

Estive lá, fiz isso; é uma situação que um sistema bem projetado deve ajudar a evitar.

É justo.

Concordo muito, mas neste caso parece impedir prematuramente os casos de uso válidos associados a uma intenção diferente.

Para ser claro, sua principal reserva em relação aos prefixos digitados:

  • Que eles impedem casos de uso específicos que um sistema mais geral permitiria?
  • Ou que eles representam uma restrição de design e, portanto, podem impedir futuros casos de uso que não antecipamos?

Não tenho a sensação de que seja o último, mas se for, tenho que objetar, porque o design que estou sugerindo não é arbitrário e abrange problemas que precisam ser resolvidos tanto quanto casos de uso futuros imprevistos.

Se for o primeiro, você tem exemplos específicos em mente, para que eu possa ajustar minha proposta? Obviamente, o ecossistema de pacotes wasm para o qual queremos projetar ainda não existe, mas mesmo um vago caso de uso "temos X dentro do diretório Y, queremos shim Z" me ajudaria.

Para ser claro, minhas razões para incluir prefixos são:

  • Transmita a intenção do usuário de forma inequívoca.
  • Evite problemas de sombra, por exemplo, um pacote quebra porque importa um arquivo chamado streams.wasm e um pacote chamado stream foi adicionado ao projeto raiz.
  • Tenha uma sintaxe reservada para bibliotecas do sistema, sem se preocupar com nomes de pacotes existentes .

Estou curioso para saber se o WASM/WASI deve cobrir algum comportamento do módulo. Meu primeiro pensamento é que eles não deveriam, para que possam ser usados ​​em muitos lugares, independentemente do sistema de módulos. A maneira como o wasmtime carrega e vincula os módulos WASM é muito diferente de como algo como o nó carrega e vincula os módulos WASM, mas, idealmente, ambos devem "simplesmente funcionar".

Gostaria de saber se é possível alinhar entre ambientes web e não web em alguns desses detalhes.

Por exemplo, para detalhes superficiais: Embora eu tenha defendido @namespace/modulename , a web parece estar caminhando para namespace:modulename para módulos embutidos, o que parece bom para mim e deve funcionar para envs não-web também.

Mais amplamente, a proposta de integração Wasm/ESM usa o encanamento JS, mas faz isso para codificar algumas coisas que fazem sentido igualmente bem fora do JS, por exemplo, que você não pode ter vários módulos Wasm que importam circularmente as exportações de função uns dos outros ( com um erro durante a instanciação). Gostaria de saber se poderíamos trabalhar para formular a especificação de integração do ESM para tornar essas semânticas entre ambientes mais claras.

A maneira como o wasmtime carrega e vincula os módulos WASM

Tenho certeza de que wasmtime atualmente não vincula os módulos WASM. Não é um recurso documentado, empiricamente não funciona em meus testes, e olhando para a fonte, a interface Resolver só é implementada com Namespace (que corresponde wasi_unstable e outros nomes codificados) e NullResolver .

Meu primeiro pensamento é que eles não deveriam, para que possam ser usados ​​em muitos lugares, independentemente do sistema de módulos.

Mas não é bem assim que funciona na prática. Dizer "Devemos ter padrões muito gerais, para que todos possam fazer o que melhor atende às suas necessidades" muitas vezes se transforma em "Há uma dúzia de convenções / padrões informais concorrentes, nenhum deles aborda corretamente o espaço do problema e a maioria das ferramentas entende apenas 2 ou 3 deles".

Embora eu tenha defendido @namespace/modulename, a web parece estar indo em direção a namespace:modulename para módulos internos,

Certo. Sintaxe à parte, estou mais interessado na ideia de diferenciar a importação do sistema de arquivos versus importações parametrizadas versus importações de host.

Mais amplamente, a proposta de integração Wasm/ESM usa o encanamento JS, mas faz isso para codificar algumas coisas que fazem sentido igualmente bem fora do JS, por exemplo, que você não pode ter vários módulos Wasm que importam circularmente as exportações de função uns dos outros ( com um erro durante a instanciação).

Sim, acho que esta é a área onde esta proposta é a mais carente. Eu preciso definir melhor a semântica de vinculação e fazer um problema no repositório esm-integration para discutir a sobreposição.

o que eu quis dizer foi mais como, devemos evitar especificar coisas específicas como sintaxes de importação e resolução (ter regras ocap e coisas assim é legal). um sistema que deseja integrar o wasm pode já estar usando uma certa sintaxe e sistema de resolução que pode colidir com o nosso. pode até não ter um conceito de caminhos e arquivos.

Eu não tenho um caso de uso simples fora de mão. No entanto, conceitualmente, existem apenas dois tipos de referências de módulo: as determinadas, que se referem a um módulo fixo conhecido, normalmente parte do mesmo componente, e as indeterminadas, que são efetivamente parâmetros e são resolvidas pelo cliente do módulo. (Curiosamente, o próprio Wasm só tem o último.)

A partir dessa perspectiva, não está totalmente claro para mim o que o terceiro tipo acrescenta. Posso ver como pode ser útil ter um mecanismo de sobreposição personalizável que possa emular os outros tipos. Mas isso só faz sentido se puder assumir as duas formas sintáticas. OTOH, se não pode sobrepor os outros, então como é semanticamente diferente de uma importação de pacote - ou seja, qual é a diferença mais profunda entre @ e $ , e por que é benéfico conectar isso diferença?

Talvez sua resposta seja que a diferença esteja apenas na pragmática da resolução pretendida, mas as convenções de nomenclatura talvez sejam suficientes.

Ok, acho que entendi melhor sua pergunta agora.

qual é a diferença mais profunda entre @ e $, e por que é benéfico conectar essa diferença?

Eu tinha planejado uma resposta mais longa, mas enquanto a estou escrevendo, estou começando a questionar a utilidade dessa diferença.

Meu raciocínio é que queremos referências determinadas para apontar apenas para diretórios filho para segurança de sandbox, mas também precisamos de algum mecanismo para criar referências determinadas para diretórios pai... exceto que não tenho certeza de que fazemos?

Os casos de uso em que eu estava pensando eram principalmente shimming e builds alternativos (testes de unidade versus depuração versus lançamento), mas esses não precisam ser preparados no sistema de pacotes.

Ainda acho que, se adicionarmos mapas de importação padrão, precisamos adicioná-los de uma maneira que não crie problemas de sombra acidentalmente, mas não tenho certeza se os mapas de importação são mais úteis na resolução de importação offline.


De qualquer forma, supondo que abandonemos a sintaxe $ e importemos os mapas, você acha que vale a pena padronizar o restante da proposta?

A proposta alterada seria:

  • Adicione um objeto raiz opcional do sistema de arquivos às APIs de instanciação ( WebAssembly.instantiate , wasm_instance_new , wasm::Instance::make )

  • Em convenções de ferramentas , adicione uma especificação para analisar strings de importação como URIs, com as regras de sandbox descritas acima.

    • Crie uma sintaxe ( @foo/bar ou foo:bar ou o que o W3C decidir) para as importações do host.

O mapa de importação potencial / mapa de exportação / mecanismos de shimming podem ser adicionados posteriormente.

Eu também tenho sentido a necessidade de algo assim. No entanto, sinto que ainda não estamos equipados para especificar nada sem introduzir algum conceito fundamentalmente novo e topologicamente "maior" (que contém vários módulos), como "pacote" (unidade de código em um gerenciador de pacotes como npm) ou "contêiner " (unidade de código que pode ser executada por um container runtime) porque, como apontado acima, não podemos assumir que existe algum sistema de arquivos ambiente cujos caminhos podemos falar. Uma vez que você tenha um conceito de "pacote" ou "contêiner" (ambos acho que fazem sentido, para diferentes fases de implantação), então é muito mais fácil ser concreto sobre como a resolução funciona.

Também compartilho o objetivo do @littledan de que, tudo o que for definido, deve ter a mesma interpretação dentro e fora de um ambiente JS/ESM. Dessa forma, você pode ter um único pacote de módulos wasm que podem ser usados ​​tão bem no Node quanto fora dele.

Compartilho o forte sentimento de que precisamos começar a coordenar e alinhar essas propostas, antecipando a necessidade de um conceito holístico de embalagem ou contêiner entre si. Não devemos excluir a web, mas também devemos manter o module sandbox intacto. Esta proposta está vinculada à proposta WebIDL de @lukewagner e @jgravelle-google, a proposta de integração ESM de @littledan e @linclark e o padrão WASI @sunfishcode @tschneidereit.

Eu vejo o principal potencial de vinculação e interface entre módulos Webassembly na capacidade de:

  • link módulos escritos em diferentes linguagens de programação no mesmo thread de uma maneira viável e comparativamente eficiente que pode evitar serialização ou IPC
  • conter vulnerabilidades de segurança em módulos não confiáveis ​​(acidentais como estouros de buffer ou maliciosos) com o módulo sandbox com permissões claramente definidas (como aplicativo) (ou em termos WASI capacidades de objeto) aplicadas de fora do módulo com a interface do módulo como o portão de segurança
  • permitem construir um ecossistema de módulos universais que podem ser facilmente reutilizados sem a necessidade de compilá-los em um único módulo ou até mesmo um executável nativo o que permite a facilidade de uso do npm install para módulos wasm com uma compensação de desempenho aceitável
  • permitem melhores tempos de carregamento e armazenamento em cache de pequenos módulos de webassembly em vez de um grande "pacote" monolítico (gerenciadores de pacotes como npm com tink, entropic e yarn estão se movendo na direção do hashing/caching baseado em arquivo de arquivos com uma instância singleton global em vez de enviando pacotes inteiros copiando arquivos redundantes.Também empacotadores como o webpack estão avaliando o potencial de pular o empacotamento com http2.

Eu acho que o module sandbox do Webassembly que isola código não confiável dentro de um módulo do lado de fora é algo bastante único e tem o potencial de introduzir portões de segurança ao usar código de terceiros com vulnerabilidades de segurança, como estouro de buffer ou módulos maliciosos que passou amplamente despercebido, mesmo dentro da comunidade, com algumas fortes alegações em contrário de especialistas com perfil na comunidade.

Minha preocupação é que, se não aumentarmos a conscientização sobre esse recurso de sandbox de módulo e coordenarmos essas propostas entre módulos para mantê-lo, podemos perder esse recurso antes mesmo de ver a luz do dia e, essencialmente, voltarmos a processar caixas de areia. Eu sei que existem ataques de temporização de canal lateral que permitem a leitura de memória fora da caixa de proteção do módulo, mas pelo menos é somente leitura e pode-se limitar o dano se, por exemplo, um módulo decodificador jpg usado em milhares de aplicativos for comprometido, ele não pode chamar de lar imediatamente para enviar sua senha se sua interface tiver acesso apenas a buffers de imagem, mas não a APIs de rede.

Se feito corretamente, o Webassembly pode ser o início de um novo ecossistema modular plug-and-play child-safe(r) que ajudará a resolver ou pelo menos conter alguns problemas epidêmicos e profundos nos sistemas de software atuais, como compatibilidade, segurança, administração esforço (contêineres) e vida útil curta do código.

Eu vejo essa abordagem baseada em sistema de arquivos como pragmática, o que pode realmente estar bem próximo do que é necessário, já que uma pasta com um index.js central, como nos módulos ES, provou ser uma unidade natural útil próxima de um módulo acima de um único arquivo que ainda permite o armazenamento em cache de arquivos eficiente, mantendo os arquivos que dependem um do outro juntos. No entanto, acredito que o módulo não deve ser capaz de importar módulos diretamente fora de seu próprio diretório, há uma referência global exclusiva como um nome de pacote npm que é resolvido e a segurança verificada pelo host seria necessária.

Algum pensamento sobre isso?

Manter todo o módulo sandbox é bom. No entanto, eu evitaria focar em coisas como de onde os módulos vêm (dependendo da implementação) e mais no relacionamento entre os módulos. Por exemplo, mesmo dentro de sistemas de arquivos (e existem muitos sistemas de arquivos, nem todos eles têm um conceito de árvore de arquivos), existem divergências sobre as regras desses sistemas de capacidade (transversal ascendente, travessia descendente, arquivos irmãos, links simbólicos, etc). A API WebAssembly do JS nem sequer tem um conceito do local de armazenamento do wasm, eles são apenas blobs de dados flutuando pela VM.

@devsnek Concordo totalmente que deve ser uma abstração não dependendo de implementações de sistemas de arquivos ou mesmo um sistema de arquivos, mas um conceito mais geral e seguro de contenção hierárquica de pacotes.

contenção hierárquica

Ele realmente precisa ser hierárquico no sentido de uma árvore de nós disjuntos ? Ou você quis dizer um gráfico direcionado (ou seja, "árvore com loops")? Existem situações (na verdade não tão incomuns) em que se precisa de uma "dependência não cíclica disfarçada de dependência cíclica" - dependência de A do módulo M1 em um B do módulo M2 e ao mesmo tempo uma dependência de C em M2 em D em M1 , enquanto não há maneira (fácil/suportada/integrada/possível) de fazer uma consulta profunda no módulo e seus dados para descobrir se é realmente um ciclo ou não. Resolver essas dependências apenas no nível dos módulos (árvore com nós disjuntos) tornaria essas coisas impossíveis, complicando desnecessariamente o desenvolvimento etc.

@dumblob Para importações fora da subárvore:

No entanto, acredito que o módulo não deve ser capaz de importar módulos diretamente fora de seu próprio diretório, há uma referência global exclusiva como um nome de pacote npm que é resolvido e a segurança verificada pelo host seria necessária.

Vejo que um relacionamento pai-filho recursivo pode ser útil onde o pai - não apenas o host - concede permissão aos seus filhos. O pai só pode conceder permissões que foram transmitidas por seu próprio pai. Isso pode incluir a permissão para importar um módulo que não está definido na subárvore, mas acessível ao pai. Ele também pode decidir exportar módulos dentro dele para seu próprio pai.

No exemplo dado, o módulo jpg-decoder Wasm depende de um módulo simd-math Wasm. O jpg-decoder declararia a dependência com um identificador de pacote exclusivo global, como [email protected]/simd-math@^1.3.4 e solicitaria acesso a ele de seu pai. Se o módulo pai tiver acesso a este módulo e for considerado "protegido para crianças" pelo pai, o acesso será concedido e o módulo simd-math importado será transferido para o jpg-decoder .

Re: sistema de arquivos

Primeiro, vamos definir o que é a pilha de tecnologia.

Você tem:

  • arquivos binários .wasm ,
  • As APIs WebAssembly (C/C++/JS),
  • O host WebAssembly (JS escrito à mão, Webpack, interpretador WASI),
  • O armazenamento persistente do host (geralmente um sistema de arquivos),
  • Um gerenciador de pacotes wasm,
  • Um repositório central de pacotes,
  • Ferramentas destinadas a fazer upload para esse repositório.

Então, estritamente falando, os binários wasm e as APIs de instanciação não precisam estar cientes do sistema de arquivos. Os binários precisam apenas de uma sintaxe de importação padrão, e a API precisa apenas de um retorno de chamada para o qual possa passar os URIs de importação analisados. Por exemplo:

WebAssembly.instantiateStreaming(
    fetch('someFile.wasm'),
    {
        // ...
        resolveImport: (parsedURI) => { ... }
    }
);

Supondo que usemos uma API semelhante ao exemplo acima, os hosts podem, em teoria, armazenar os módulos da maneira que quiserem; eles podem extrair de um armazenamento que não seja do sistema de arquivos, por exemplo, interpretando URIs como solicitações em um banco de dados SQL.

Na prática, qualquer desenvolvedor que queira reutilizar código criado pela comunidade ainda precisa preencher esse banco de dados com módulos criados pela comunidade, que praticamente sempre serão construídos em um ambiente de sistema de arquivos.

Portanto, embora a parte do host da pilha acima possa ser independente do sistema de arquivos, ela precisa interagir com um formato de pacote semelhante ao sistema de arquivos e um sistema de dependências que é, por natureza, semelhante a uma árvore (dependências que têm suas próprias subdependências que têm suas próprias sub-sub-dependências, sem conflitos de nome de pacote entre diferentes níveis).

No geral, tudo isso volta ao que @lukewagner e @ttraenkler disseram, que precisamos de uma definição holística do que é um pacote (se não qual formato de sistema de arquivos eles usam) antes de podermos estabelecer um esquema de importação.


Ele realmente precisa ser hierárquico no sentido de uma árvore de nós disjuntos? Ou você quis dizer um gráfico direcionado (ou seja, "árvore com loops")? Existem situações (na verdade não tão incomuns) em que se precisa de uma "dependência não cíclica disfarçada de dependência cíclica" - dependência de A do módulo M1 em um B do módulo M2 e ao mesmo tempo uma dependência de C em M2 em D em M1, enquanto não há maneira (fácil/suportada/integrada/possível) de fazer uma consulta profunda no módulo e seus dados para descobrir se é realmente um ciclo ou não. Resolver essas dependências apenas no nível dos módulos (árvore com nós disjuntos) tornaria essas coisas impossíveis, complicando desnecessariamente o desenvolvimento etc.

Essas dependências semicíclicas são realmente comuns quando você contrai subgráficos intramódulos?

Eu esperaria que, se você tratasse cada módulo como um único nó, em 99,9% dos casos de uso você acabasse com um DAG de importação ou uma árvore de importação.

Acho que faria sentido ter um esquema de resolução de importação que assumisse que as dependências podem ser cíclicas dentro de um único pacote, mas são semelhantes a árvores (o modelo npm) com algumas exceções não cíclicas (WASI, bibliotecas de host, dependências de pares, etc).

Ok, depois de ir e voltar nessa proposta, gosto da direção dela. No entanto, não acho que haja necessidade de exigir prefixos específicos ou qualquer outro tipo de mecanismo de "segurança". Eles devem ser tratados pelo build-system/pkg-manager, não pelo interpretador. Um gerenciador de pacotes seguro deve ser capaz de especificar adequadamente a hierarquia de dependência completa e entregá-la ao interpretador wasi, que então interpreta como é dito. Este relacionamento _pode_ se parecer com isso (json semi-válido):

[
  {
    "path": "./hash2k3jc/mod/foo.wasi",
    "dependencies": [
      {
        "import": ["mod", "bar"],  # (import "mod" "bar" ...)
        "path": "./hash2k3jc/bar.wasi",
      }
    ]
  },
  {
    "path": "./hash2k3jc/mod/bar.wasi",
    "dependencies": [],
  },
]

Aqui "mod foo" tem 1 dependência ("mod bar"), que por si só não tem dependências.

Esta é uma lista simples de arquivos wasi ( path ) e especifica como mapear instruções de importação para arquivos wasi específicos ( dependencies com import e path ) .

Você observa que esse sistema de compilação opta por usar hashes para ser 100% explícito e seguro. Outros sistemas de compilação (ou seja, fechados, internos) podem optar por ser mais frouxos para permitir atualizações in-loco. O ponto é que essa representação pode apoiar qualquer uma das abordagens.

IMO isso reflete mais de perto https://webassembly.org/docs/dynamic-linking/ -- não colocando absolutamente nenhum requisito nos arquivos wasi ou no interpretador, em vez disso, optando por oferecer o máximo de flexibilidade e simplicidade.

Nota: Eu acho que assumir um sistema de arquivos para o interpretador wasi é apropriado.

Nota: Estou trabalhando em um "sistema de compilação para a web" wake e gostaria de (eventualmente) ter wasi como o principal alvo _and_ ambiente de compilação. Eu gostaria de poder enviar dependências assinadas para que o compartilhamento de bibliotecas seja mais leve do que o empacotamento do mundo.

Nota: Eu acho que assumir um sistema de arquivos para o interpretador wasi é apropriado.

Na verdade, tenho uma opinião bastante oposta : wink:. Já hoje em dia existem casos de uso WASM sérios onde não há absolutamente nenhum sistema de arquivos e nunca haverá (por exemplo, sistemas operacionais novos ou experimentais que não têm sistemas de arquivos, mas apenas uma interface programática semelhante a um banco de dados, fornecendo apenas o endereçamento de armazenamento persistente mais básico - em sua forma mais simples forma, pode ser um hardware RAM não volátil sem necessidade de estrutura em árvore, como sistema de arquivos). Portanto, um sistema de arquivos não deve ser um pré-requisito para um interpretador WASI (IMHO nem mesmo um gerenciador de pacotes WASI universal deve exigir armazenamento semelhante a um sistema de arquivos, mas deve funcionar com um armazenamento persistente linear do que com armazenamento semelhante a árvore ou armazenamento em bloco ou valor-chave armazenamento ou qualquer outra coisa).

Possivelmente, mas também não quero exigir que todas as dependências estejam em um único bloco contínuo de armazenamento :smile: (como usar position e size em vez de path ). path poderia ser usado como uma chave em uma interface de programação do tipo DB, não? Talvez devêssemos chamar de algo diferente de path ... key talvez?

Eu vejo algum mecanismo para procurar blobs binários por uma chave como um recurso bastante essencial de um gerenciador de pacotes :smile:

na especificação js, ​​não damos aos módulos nenhum tipo de especificador. Realizamos a resolução por (referringModuleInstance, importString) , e o host tem que fazer seu próprio sentido sobre se essa importação deve ser permitida, para onde ela resolve, etc. Acho que Wasm atualmente é ainda mais vago, e WASI pode impor restrições adicionais como "se o resultado da resolução (referringModuleInstance, importString) não deve ser transitivamente acessível ao módulo importador, saia com o código xyz". Para wasmtime, "transitivamente acessível" provavelmente envolve permissões de arquivo do tipo unix. Para algo como fastly/cloudflare, provavelmente significa "parte do mesmo arquivo enviado pelo usuário".

Esta página foi útil?
0 / 5 - 0 avaliações

Questões relacionadas

JimmyVV picture JimmyVV  ·  4Comentários

void4 picture void4  ·  5Comentários

ghost picture ghost  ·  7Comentários

cretz picture cretz  ·  5Comentários

artem-v-shamsutdinov picture artem-v-shamsutdinov  ·  6Comentários