mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-11-07 09:47:23 +00:00
对应主管任务 A/B。**没有实现 wasm 接入**,只交付事实结论与可执行方案。 ## A. P1-4 结论固化(docs/channel-repair-plan.md) 原 P1-4 只有一句「阻塞:二进制不在仓库任何位置」,现补成可审计的事实段: - 绝对路径 / 298606 字节 / sha256 6419471e…55b43d(位于第三方插件 `dsh-our-free-model` 的 vendor 目录,**不随本仓分发**;本仓 `git ls-files | grep -i wasm` = 0); - 它是 wasm-bindgen 产物:31 个 import 全来自单一 JS glue 模块 `./qoder_auth_wasm_bg.js`,且该 glue **上游也没提供**(改为手写 `src/qoder-wasm.ts` 706 行)⇒ 代价在宿主侧; - 31 import / 34 export 各分四类,逐类给出用途与成员; - **我们仓库没有 wasm 运行时入口**:`grep -rniE 'wazero|wasmtime|wasmer| tetratelabs'` = 0 命中,`go.sum` wasm 命中 = 0,Lua 侧只有注释。 只写事实,未写「已修复」。 ## B. 可行性判定(docs/qoder-wasm-integration.md,新文件) 实测推翻了一个此前的假定:**不需要 JS 引擎,纯 Go(wazero)可以驱动它**。 证据来自新增的 tools/qoder-wasm-probe(独立 module,主仓 go.mod 不动): - 实例化:31 桩,实例化阶段真实调用 0 次; - level 2 `generate_runtime_auth_fields` → encrypt_user_info/key 各 172 字符; - level 3 `qodercontext_new` + `prepareInferRequest` → 20 个头,其中 `Authorization: Bearer COSY.<载荷>.<签名>`(len=209),body 60 字节密文; - 逐 import 计数:31 个里 **13 个从未被调用**(node/JS 探针居多); - 量化:≈2.2 ms/请求,线性内存 1.25 MiB,实例可复用(-repeat 50 全 PASS)。 文档含:依赖选型(wazero vs wasmtime-go/wasmer-go,+1.16 MiB 实测)、 接入点设计(走 `__llmsproxy_kernel` 而非新开 Lua 全局,保持「按插件声明授权」)、 31 import 逐个的 Go 实现映射表、6 条 ABI 陷阱(两个 getRandomValues 方向相反、 两套返回布局、requestresult_url 参数顺序、drop_ref 必须 no-op、machineId 必填、 host 不能是 api2-v2)、9 条风险、7 层改动面。 ## 为什么本轮不实现 跨 7 层、需新增内核依赖、且三个前置阻塞都不该由 fix 分支暗定: ① wasm 从哪来(不可再分发 / 需运维决策);② 加密端点的**响应也要解密**且是 SSE 逐帧(第二条未实现链路);③ 账号数 0 ⇒ 端到端不可验证。 ## 纪律声明 - 未动 main、未开 PR、未部署、未 restart; - 主仓 `go.mod`/`go.sum` **零改动**(tools/ 是嵌套 module,`go build ./...` 跳过); - `go test -tags luajit ./...` 全绿(core 420s / gateway 206s); - **本机 qoder 账号数 0**:能证明「产出了签名头」,不能证明「上游接受」。 文档内每处 PASS 都标注是离线结论。 ## 顺带更正一处过时事实 `internal/core/channels_upstream_gaps_test.go` 注释称 wasm「在参考仓库里也不存在」。 实测:文件存在(sha256 6419471e…),只是不在**我们**仓库。文档已记,本轮不改 代码避免动判据;接入时必须同步演进那条判据(否则新旧逻辑互相打脸)。
679 lines
21 KiB
Go
679 lines
21 KiB
Go
// Command qoder-wasm-probe 评测 `qoder-auth-wasm.wasm` 能否被**纯 Go** wasm
|
||
// 运行时(wazero,无 JS 引擎)驱动,并给出它实际需要宿主提供哪些能力。
|
||
//
|
||
// 这是**取证/评测工具**,不是生产代码:它不注册进网关,也不被内核 import。
|
||
// 主仓的 go.mod **不包含** wazero —— 本目录是独立 module。
|
||
//
|
||
// 为什么需要它:qoder 的加密推理端点头由该 wasm 生成,
|
||
// 而 wasm 是 wasm-bindgen 产物(31 个 import 全部来自一个 JS glue 模块)。
|
||
// "能不能实例化" 与 "能不能真正跑出鉴权头" 是两件事 —— 上游参考甚至没有
|
||
// 提供那份 JS glue(手写了 706 行替代),所以必须实测,不能推断。
|
||
//
|
||
// 用法:
|
||
//
|
||
// go run . -wasm /path/to/qoder-auth-wasm.wasm [-json]
|
||
//
|
||
// 退出码:0 = 三级证据全部通过;1 = 任一环节失败(详见输出)。
|
||
package main
|
||
|
||
import (
|
||
"context"
|
||
"crypto/rand"
|
||
"encoding/binary"
|
||
"encoding/json"
|
||
"flag"
|
||
"fmt"
|
||
"math"
|
||
"os"
|
||
"sort"
|
||
"time"
|
||
|
||
"github.com/tetratelabs/wazero"
|
||
"github.com/tetratelabs/wazero/api"
|
||
)
|
||
|
||
const importModule = "./qoder_auth_wasm_bg.js"
|
||
|
||
type probe struct {
|
||
ctx context.Context
|
||
rt wazero.Runtime
|
||
mod api.Module
|
||
hits map[string]int
|
||
order []string
|
||
warns []string
|
||
strict bool
|
||
repeat int
|
||
}
|
||
|
||
// ---- JS-ish 值模型 -------------------------------------------------------
|
||
//
|
||
// wasm-bindgen 的 ABI 把 JS 值当作整数索引传进导出表。纯 Go 没有这张表,
|
||
// 宿主必须自己模拟。qoder 模块实际用到的形状只有三种:
|
||
// 1. Uint8Array —— 字节缓冲
|
||
// 2. Map —— requestresult_headers 的产物
|
||
// 3. crypto 对象 —— 只用来喂 getRandomValues(见 C 类 import)
|
||
|
||
type (
|
||
undefined struct{}
|
||
u8arr []byte
|
||
jsMap struct{ keys, vals []string }
|
||
cryptoObj struct{}
|
||
)
|
||
|
||
// wasm-bindgen 把索引 0..1027 保留给 undefined/null/true/false 等哨兵。
|
||
const sentinelN = 1028
|
||
|
||
func newHeap() []interface{} {
|
||
o := make([]interface{}, sentinelN, 4096)
|
||
for i := range o {
|
||
o[i] = undefined{}
|
||
}
|
||
o[1024], o[1025], o[1026], o[1027] = undefined{}, nil, true, false
|
||
return o
|
||
}
|
||
|
||
func main() {
|
||
wasmPath := flag.String("wasm", "",
|
||
"qoder-auth-wasm.wasm 路径(必填)")
|
||
asJSON := flag.Bool("json", false, "以 JSON 输出结果")
|
||
repeat := flag.Int("repeat", 1, "重复调用 prepareInferRequest 的次数(测实例复用)")
|
||
strict := flag.Bool("strict", false,
|
||
"任何未实现的 import 被调用即失败(默认只告警)")
|
||
flag.Parse()
|
||
if *wasmPath == "" {
|
||
fmt.Fprintln(os.Stderr, "-wasm is required")
|
||
os.Exit(2)
|
||
}
|
||
|
||
p := &probe{ctx: context.Background(), hits: map[string]int{}, strict: *strict, repeat: *repeat}
|
||
res := p.run(*wasmPath)
|
||
|
||
if *asJSON {
|
||
enc := json.NewEncoder(os.Stdout)
|
||
enc.SetIndent("", " ")
|
||
enc.Encode(res)
|
||
} else {
|
||
p.report(res)
|
||
}
|
||
if !res.OK() {
|
||
os.Exit(1)
|
||
}
|
||
}
|
||
|
||
type result struct {
|
||
WasmPath string `json:"wasm_path"`
|
||
WasmSize int `json:"wasm_size"`
|
||
Imports int `json:"imports"`
|
||
Exports int `json:"exports"`
|
||
Instantiated bool `json:"instantiated"`
|
||
CallsAtInstall int `json:"calls_at_instantiate"`
|
||
AuthFields map[string]any `json:"generate_runtime_auth_fields"`
|
||
AuthFieldLens map[string]int `json:"auth_field_lengths"`
|
||
AuthErr string `json:"auth_error,omitempty"`
|
||
InferURL string `json:"infer_url,omitempty"`
|
||
InferBodyLen int `json:"infer_body_len"`
|
||
InferHeaders map[string]string `json:"infer_headers,omitempty"`
|
||
InferErr string `json:"infer_error,omitempty"`
|
||
ImportHits map[string]int `json:"import_hits"`
|
||
NeverCalled []string `json:"imports_never_called"`
|
||
UnimplementedHit []string `json:"unimplemented_imports_hit,omitempty"`
|
||
Warnings []string `json:"warnings,omitempty"`
|
||
// MemPages 是跑完三级调用后 wasm 线性内存的页数(1 页 = 64 KiB)。
|
||
MemPages uint32 `json:"mem_pages"`
|
||
Repeat int `json:"repeat"`
|
||
ElapsedMS float64 `json:"elapsed_ms"`
|
||
}
|
||
|
||
func (r result) OK() bool {
|
||
return r.Instantiated && r.AuthErr == "" && r.InferErr == ""
|
||
}
|
||
|
||
func (p *probe) heap() []byte { b, _ := p.mod.Memory().Read(0, p.mod.Memory().Size()); return b }
|
||
|
||
func (p *probe) readStr(ptr, ln uint32) string {
|
||
if b, ok := p.mod.Memory().Read(ptr, ln); ok {
|
||
return string(b)
|
||
}
|
||
return ""
|
||
}
|
||
|
||
func (p *probe) warn(format string, a ...any) {
|
||
msg := fmt.Sprintf(format, a...)
|
||
p.warns = append(p.warns, msg)
|
||
fmt.Fprintln(os.Stderr, "warn: "+msg)
|
||
}
|
||
|
||
func (p *probe) run(path string) result {
|
||
res := result{WasmPath: path, ImportHits: map[string]int{}}
|
||
t0 := time.Now()
|
||
|
||
b, err := os.ReadFile(path)
|
||
if err != nil {
|
||
res.InferErr = err.Error()
|
||
return res
|
||
}
|
||
res.WasmSize = len(b)
|
||
|
||
p.rt = wazero.NewRuntime(p.ctx)
|
||
defer p.rt.Close(p.ctx)
|
||
|
||
cm, err := p.rt.CompileModule(p.ctx, b)
|
||
if err != nil {
|
||
res.InferErr = "compile: " + err.Error()
|
||
return res
|
||
}
|
||
imports := cm.ImportedFunctions()
|
||
res.Imports = len(imports)
|
||
res.Exports = len(cm.ExportedFunctions())
|
||
|
||
// ---- 宿主模块:31 个 import 的纯 Go 实现 ----
|
||
host := p.rt.NewHostModuleBuilder(importModule)
|
||
for _, f := range imports {
|
||
_, name, _ := f.Import()
|
||
params, results := f.ParamTypes(), f.ResultTypes()
|
||
host.NewFunctionBuilder().
|
||
WithGoModuleFunction(api.GoModuleFunc(p.impl(name)), params, results).
|
||
Export(name)
|
||
}
|
||
if _, err := host.Instantiate(p.ctx); err != nil {
|
||
res.InferErr = "host module: " + err.Error()
|
||
return res
|
||
}
|
||
|
||
p.mod, err = p.rt.InstantiateModule(p.ctx, cm, wazero.NewModuleConfig().WithName("qoder"))
|
||
if err != nil {
|
||
res.InferErr = "instantiate: " + err.Error()
|
||
return res
|
||
}
|
||
res.Instantiated = true
|
||
res.CallsAtInstall = len(p.order) // 实例化阶段真实调用次数
|
||
|
||
p.level2(&res)
|
||
if res.AuthErr == "" {
|
||
for i := 0; i < p.repeat; i++ {
|
||
p.level3(&res)
|
||
if res.InferErr != "" {
|
||
res.InferErr = fmt.Sprintf("iteration %d/%d: %s", i+1, p.repeat, res.InferErr)
|
||
break
|
||
}
|
||
}
|
||
}
|
||
|
||
res.ElapsedMS = float64(time.Since(t0).Microseconds()) / 1000
|
||
if mem := p.mod.Memory(); mem != nil {
|
||
res.MemPages = mem.Size() / 65536
|
||
}
|
||
res.ImportHits = p.hits
|
||
res.NeverCalled = p.neverCalled(imports)
|
||
res.Warnings = p.warns
|
||
return res
|
||
}
|
||
|
||
// ---- 三级证据 ------------------------------------------------------------
|
||
|
||
// level2: generate_runtime_auth_fields —— 第一次真实调用。
|
||
func (p *probe) level2(res *result) {
|
||
payload := `{"uid":"probe-uid","security_oauth_token":"probe-token",` +
|
||
`"organization_id":"","organization_tags":[],"data_policy_agreed":false}`
|
||
ptr, ln := p.writeString(payload)
|
||
raw, err := p.callString("generate_runtime_auth_fields", uint64(ptr), uint64(ln))
|
||
if err != nil {
|
||
res.AuthErr = err.Error()
|
||
return
|
||
}
|
||
var fields map[string]any
|
||
if err := json.Unmarshal([]byte(raw), &fields); err != nil {
|
||
res.AuthErr = "non-JSON output: " + err.Error()
|
||
return
|
||
}
|
||
res.AuthFields = map[string]any{}
|
||
res.AuthFieldLens = map[string]int{}
|
||
for k, v := range fields {
|
||
// 只暴露类型与长度:字段本身是敏感材料,不该进日志。
|
||
switch s := v.(type) {
|
||
case string:
|
||
res.AuthFields[k] = fmt.Sprintf("<string len=%d>", len(s))
|
||
res.AuthFieldLens[k] = len(s)
|
||
default:
|
||
res.AuthFields[k] = fmt.Sprintf("<%T>", v)
|
||
res.AuthFieldLens[k] = 0
|
||
}
|
||
}
|
||
}
|
||
|
||
// level3: qodercontext_new + prepareInferRequest —— 产出真实加密请求。
|
||
func (p *probe) level3(res *result) {
|
||
userInfo := `{"uid":"probe-uid","encrypt_user_info":"probe","key":"probe",` +
|
||
`"organization_id":"","organization_tags":[],"data_policy_agreed":false}`
|
||
meta := `{"client_type":"10","business_product":"qoder",` +
|
||
`"business_type":"default","scene":"chat"}`
|
||
|
||
mp, ml := p.writeString("probe-machine-uuid")
|
||
vp, vl := p.writeString("1.1.49")
|
||
up, ul := p.writeString(userInfo)
|
||
tp, tl := p.writeString(meta)
|
||
|
||
sp := p.stackPtr()
|
||
_, err := p.export("qodercontext_new").Call(p.ctx, uint64(sp),
|
||
uint64(mp), uint64(ml), uint64(vp), uint64(vl),
|
||
uint64(up), uint64(ul), uint64(tp), uint64(tl))
|
||
v := p.dv()
|
||
ctxPtr := int32(v.u32(sp))
|
||
isErr := int32(v.u32(sp + 8))
|
||
errIdx := int32(v.u32(sp + 4))
|
||
p.stackFree()
|
||
if err != nil || isErr != 0 {
|
||
res.InferErr = fmt.Sprintf("qodercontext_new err=%v isErr=%d obj=%v",
|
||
err, isErr, p.getObj(uint32(errIdx)))
|
||
return
|
||
}
|
||
|
||
hp, hl := p.writeString("api2.qoder.sh")
|
||
bp, bl := p.writeString(`{"messages":[{"role":"user","content":"hi"}]}`)
|
||
kp, kl := p.writeString("qfmodel")
|
||
skp, skl := p.writeString("system")
|
||
|
||
sp2 := p.stackPtr()
|
||
_, err = p.export("qodercontext_prepareInferRequest").Call(p.ctx, uint64(sp2),
|
||
uint64(ctxPtr), uint64(hp), uint64(hl), uint64(bp), uint64(bl),
|
||
uint64(kp), uint64(kl), uint64(skp), uint64(skl))
|
||
v2 := p.dv()
|
||
resPtr := int32(v2.u32(sp2))
|
||
rErr := int32(v2.u32(sp2 + 8))
|
||
rErrIdx := int32(v2.u32(sp2 + 4))
|
||
p.stackFree()
|
||
if err != nil || rErr != 0 {
|
||
res.InferErr = fmt.Sprintf("prepareInferRequest err=%v isErr=%d obj=%v",
|
||
err, rErr, p.getObj(uint32(rErrIdx)))
|
||
return
|
||
}
|
||
|
||
hdrIdx, _ := p.export("requestresult_headers").Call(p.ctx, uint64(uint32(resPtr)))
|
||
switch hm := p.getObj(uint32(hdrIdx[0])).(type) {
|
||
case jsMap:
|
||
res.InferHeaders = map[string]string{}
|
||
for i := range hm.keys {
|
||
k, val := hm.keys[i], hm.vals[i]
|
||
// Authorization 是 WASM 产出的签名头:只报形状,不回显载荷。
|
||
if len(val) > 24 {
|
||
res.InferHeaders[k] = val[:24] + fmt.Sprintf("…<len=%d>", len(val))
|
||
} else {
|
||
res.InferHeaders[k] = val
|
||
}
|
||
}
|
||
default:
|
||
res.InferErr = fmt.Sprintf("requestresult_headers -> %T (expected jsMap)", hm)
|
||
}
|
||
|
||
res.InferURL = p.resultString(resPtr, "requestresult_url")
|
||
body := p.resultString(resPtr, "requestresult_body")
|
||
res.InferBodyLen = len(body)
|
||
res.Repeat = p.repeat
|
||
}
|
||
|
||
func (p *probe) report(r result) {
|
||
fmt.Printf("wasm : %s\n", r.WasmPath)
|
||
fmt.Printf("size : %d bytes\n", r.WasmSize)
|
||
fmt.Printf("imports : %d (all from %q)\n", r.Imports, importModule)
|
||
fmt.Printf("exports : %d\n", r.Exports)
|
||
fmt.Printf("instantiate: %v calls-at-instantiate=%d\n", r.Instantiated, r.CallsAtInstall)
|
||
|
||
fmt.Println("\n-- LEVEL 2: generate_runtime_auth_fields --")
|
||
if r.AuthErr != "" {
|
||
fmt.Println(" FAILED:", r.AuthErr)
|
||
} else {
|
||
ks := make([]string, 0, len(r.AuthFields))
|
||
for k := range r.AuthFields {
|
||
ks = append(ks, k)
|
||
}
|
||
sort.Strings(ks)
|
||
for _, k := range ks {
|
||
fmt.Printf(" %-20s %s\n", k, r.AuthFields[k])
|
||
}
|
||
}
|
||
|
||
fmt.Println("\n-- LEVEL 3: qodercontext_new + prepareInferRequest --")
|
||
if r.InferErr != "" {
|
||
fmt.Println(" FAILED:", r.InferErr)
|
||
} else {
|
||
fmt.Printf(" url = %s\n", r.InferURL)
|
||
fmt.Printf(" body = %d bytes (encrypted, not logged)\n", r.InferBodyLen)
|
||
for _, k := range sortedKeys(r.InferHeaders) {
|
||
fmt.Printf(" hdr %-20s %s\n", k, r.InferHeaders[k])
|
||
}
|
||
}
|
||
|
||
fmt.Printf("\n-- IMPORT USAGE (%d of %d touched) --\n", len(r.ImportHits), r.Imports)
|
||
for _, k := range sortedKeys(r.ImportHits) {
|
||
fmt.Printf(" %-50s %d\n", k, r.ImportHits[k])
|
||
}
|
||
fmt.Printf(" never called: %d\n", len(r.NeverCalled))
|
||
for _, n := range r.NeverCalled {
|
||
fmt.Printf(" %s\n", n)
|
||
}
|
||
if len(r.Warnings) > 0 {
|
||
fmt.Printf("\n-- WARNINGS (%d) --\n", len(r.Warnings))
|
||
for _, w := range r.Warnings {
|
||
fmt.Printf(" %s\n", w)
|
||
}
|
||
}
|
||
fmt.Printf("\nRESULT: %s\n", map[bool]string{true: "PASS", false: "FAIL"}[r.OK()])
|
||
}
|
||
|
||
// ---- wasm-bindgen ABI 助手(照 upstream src/qoder-wasm.ts 的语义)---------
|
||
|
||
func (p *probe) export(n string) api.Function {
|
||
f := p.mod.ExportedFunction(n)
|
||
if f == nil {
|
||
panic("missing export " + n)
|
||
}
|
||
return f
|
||
}
|
||
|
||
func (p *probe) dv() dataView {
|
||
b, _ := p.mod.Memory().Read(0, p.mod.Memory().Size())
|
||
return dataView(b)
|
||
}
|
||
|
||
type dataView []byte
|
||
|
||
func (d dataView) u32(off uint32) uint32 { return binary.LittleEndian.Uint32(d[off:]) }
|
||
|
||
func (p *probe) stackPtr() uint32 {
|
||
out, _ := p.export("__wbindgen_add_to_stack_pointer").Call(p.ctx, api.EncodeI32(-16))
|
||
return uint32(api.DecodeI32(out[0]))
|
||
}
|
||
|
||
func (p *probe) stackFree() {
|
||
p.export("__wbindgen_add_to_stack_pointer").Call(p.ctx, api.EncodeI32(16))
|
||
}
|
||
|
||
func (p *probe) writeString(s string) (uint32, uint32) {
|
||
b := []byte(s)
|
||
out, err := p.export("__wbindgen_export2").Call(p.ctx, uint64(len(b)), 1)
|
||
if err != nil {
|
||
panic(err)
|
||
}
|
||
ptr := uint32(out[0])
|
||
p.mod.Memory().Write(ptr, b)
|
||
return ptr, uint32(len(b))
|
||
}
|
||
|
||
// callString 对应「返回值布局 = ptr/len/valIdx/isErr」那一类函数。
|
||
func (p *probe) callString(name string, args ...uint64) (string, error) {
|
||
sp := p.stackPtr()
|
||
defer p.stackFree()
|
||
if _, err := p.export(name).Call(p.ctx, append([]uint64{uint64(sp)}, args...)...); err != nil {
|
||
return "", fmt.Errorf("%s: %w", name, err)
|
||
}
|
||
v := p.dv()
|
||
ptr, ln := int32(v.u32(sp)), int32(v.u32(sp+4))
|
||
if isErr := int32(v.u32(sp + 12)); isErr != 0 {
|
||
return "", fmt.Errorf("%s: error idx=%d (%v)", name, int32(v.u32(sp+8)), p.getObj(v.u32(sp+8)))
|
||
}
|
||
return p.readStr(uint32(ptr), uint32(ln)), nil
|
||
}
|
||
|
||
// resultString 对应 `requestresult_url|body`:参数顺序是 (栈指针, ptr)。
|
||
func (p *probe) resultString(result int32, name string) string {
|
||
sp := p.stackPtr()
|
||
defer p.stackFree()
|
||
if _, err := p.export(name).Call(p.ctx, uint64(sp), uint64(uint32(result))); err != nil {
|
||
p.warn("%s: %v", name, err)
|
||
return ""
|
||
}
|
||
v := p.dv()
|
||
ptr, ln := int32(v.u32(sp)), int32(v.u32(sp+4))
|
||
if ptr == 0 {
|
||
return ""
|
||
}
|
||
return p.readStr(uint32(ptr), uint32(ln))
|
||
}
|
||
|
||
// ---- 对象堆 --------------------------------------------------------------
|
||
|
||
var objects = newHeap()
|
||
|
||
func pushObj(v any) uint32 {
|
||
objects = append(objects, v)
|
||
return uint32(len(objects) - 1)
|
||
}
|
||
|
||
func (p *probe) getObj(i uint32) any {
|
||
if int(i) < len(objects) {
|
||
return objects[i]
|
||
}
|
||
return undefined{}
|
||
}
|
||
|
||
var dbg = os.Getenv("PROBE_DBG") != ""
|
||
|
||
func (p *probe) takeObj(i uint32) {
|
||
if dbg && i >= sentinelN {
|
||
fmt.Printf("[dbg] drop %d (was %T)\n", i, p.getObj(i))
|
||
}
|
||
if i >= sentinelN && int(i) < len(objects) {
|
||
objects[i] = undefined{}
|
||
}
|
||
}
|
||
|
||
func (p *probe) asBytes(i uint32) u8arr {
|
||
switch v := p.getObj(i).(type) {
|
||
case u8arr:
|
||
return v
|
||
case []byte:
|
||
return u8arr(v)
|
||
}
|
||
p.warn("heap[%d] is %T, expected Uint8Array", i, p.getObj(i))
|
||
return nil
|
||
}
|
||
|
||
// ---- 31 个 import 的纯 Go 实现 -------------------------------------------
|
||
//
|
||
// 分四类(与 docs/qoder-wasm-integration.md 的接口映射表对应):
|
||
// A bindgen 对象表 · B 字节缓冲 · C 随机/时间 · D JS 环境探针
|
||
|
||
func (p *probe) impl(name string) func(context.Context, api.Module, []uint64) {
|
||
mark := func(n string) {
|
||
p.hits[n]++
|
||
if len(p.order) < 4096 {
|
||
p.order = append(p.order, n)
|
||
}
|
||
}
|
||
wrap := func(f func(s []uint64)) func(context.Context, api.Module, []uint64) {
|
||
return func(_ context.Context, _ api.Module, s []uint64) {
|
||
mark(name)
|
||
defer func() {
|
||
if r := recover(); r != nil {
|
||
p.warn("host func %s panicked: %v", name, r)
|
||
}
|
||
}()
|
||
f(s)
|
||
}
|
||
}
|
||
// 未在 auth 路径上验证过的 import:被调用就是新信息,必须记下来。
|
||
unimpl := func(what string) func(context.Context, api.Module, []uint64) {
|
||
return wrap(func(s []uint64) {
|
||
p.warn("import %s reached (%s) -- not exercised by the auth path", name, what)
|
||
if p.strict {
|
||
panic("unimplemented import reached: " + name)
|
||
}
|
||
})
|
||
}
|
||
|
||
switch name {
|
||
// -- A: bindgen 对象表 ------------------------------------------------
|
||
case "__wbindgen_object_drop_ref":
|
||
// ⚠️ 必须**不释放**槽位:实测该模块会 drop 一个 Map 之后继续用同一索引
|
||
// 调 set(日志见 docs)。wasm-bindgen 新版把 drop 退化为空操作
|
||
// (externref 表交给 JS GC),因此这里保持 no-op —— 代价是索引只增不回收。
|
||
return wrap(func(s []uint64) {})
|
||
case "__wbindgen_object_clone_ref":
|
||
return wrap(func(s []uint64) { s[0] = uint64(pushObj(p.getObj(uint32(s[0])))) })
|
||
case "__wbindgen_cast_0000000000000001": // wasm slice -> Uint8Array
|
||
return wrap(func(s []uint64) {
|
||
a, e := uint32(s[0]), uint32(s[1])
|
||
dst := make(u8arr, e)
|
||
copy(dst, p.heap()[a:a+e])
|
||
s[0] = uint64(pushObj(dst))
|
||
})
|
||
case "__wbindgen_cast_0000000000000002": // wasm str -> JS string
|
||
return wrap(func(s []uint64) {
|
||
s[0] = uint64(pushObj(p.readStr(uint32(s[0]), uint32(s[1]))))
|
||
})
|
||
|
||
// -- B: 字节缓冲 ------------------------------------------------------
|
||
case "__wbg_new_with_length_9cedd08484b73942":
|
||
return wrap(func(s []uint64) { s[0] = uint64(pushObj(make(u8arr, uint32(s[0])))) })
|
||
case "__wbg_length_0c32cb8543c8e4c8":
|
||
return wrap(func(s []uint64) { s[0] = uint64(len(p.asBytes(uint32(s[0])))) })
|
||
case "__wbg_subarray_0f98d3fb634508ad":
|
||
return wrap(func(s []uint64) {
|
||
b := p.asBytes(uint32(s[0]))
|
||
lo, hi := uint32(s[1]), uint32(s[2])
|
||
if hi > uint32(len(b)) {
|
||
hi = uint32(len(b))
|
||
}
|
||
if lo > hi {
|
||
lo = hi
|
||
}
|
||
s[0] = uint64(pushObj(b[lo:hi]))
|
||
})
|
||
case "__wbg_set_08463b1df38a7e29":
|
||
// 两种形态:Uint8Array.set(src, off) 与 Map.set()(headers 集合)。
|
||
return wrap(func(s []uint64) {
|
||
switch dst := p.getObj(uint32(s[0])).(type) {
|
||
case jsMap:
|
||
k, ok := p.getObj(uint32(s[1])).(string)
|
||
if !ok {
|
||
k = fmt.Sprint(p.getObj(uint32(s[1])))
|
||
}
|
||
dst.keys = append(dst.keys, k)
|
||
dst.vals = append(dst.vals, fmt.Sprint(p.getObj(uint32(s[2]))))
|
||
objects[s[0]] = dst
|
||
case u8arr:
|
||
src := p.asBytes(uint32(s[1]))
|
||
off := int(uint32(s[2]))
|
||
if off > len(dst) {
|
||
return
|
||
}
|
||
copy(dst[off:], src)
|
||
default:
|
||
if dbg {
|
||
fmt.Printf("[dbg] set arg0=%d type=%T arg2=%d\n", s[0], p.getObj(uint32(s[0])), s[2])
|
||
}
|
||
p.warn("set on unsupported %T", p.getObj(uint32(s[0])))
|
||
}
|
||
})
|
||
case "__wbg_prototypesetcall_3e05eb9545565046":
|
||
return wrap(func(s []uint64) {
|
||
a, e := uint32(s[0]), uint32(s[1])
|
||
copy(p.heap()[a:a+e], p.asBytes(uint32(s[2])))
|
||
})
|
||
case "__wbg_new_99cabae501c0a8a0": // new Map()
|
||
return wrap(func(s []uint64) {
|
||
idx := pushObj(jsMap{})
|
||
if dbg {
|
||
fmt.Printf("[dbg] newMap -> %d\n", idx)
|
||
}
|
||
s[0] = uint64(idx)
|
||
})
|
||
|
||
// -- C: 随机源与时间 --------------------------------------------------
|
||
//
|
||
// ⚠️ 两个 getRandomValues 的签名**方向相反**(upstream 注释里踩过的坑):
|
||
// _d49329ff89a07af1 = (wasmPtr, len) -> 直接写 wasm 内存
|
||
// _c44a50d8cfdaebeb = (cryptoObjIdx, u8arrIdx) -> 调 JS 对象的方法
|
||
// 写反会得到 Rust panic `unreachable`。
|
||
case "__wbg_getRandomValues_d49329ff89a07af1":
|
||
return wrap(func(s []uint64) {
|
||
b := make([]byte, uint32(s[1]))
|
||
rand.Read(b)
|
||
p.mod.Memory().Write(uint32(s[0]), b)
|
||
})
|
||
case "__wbg_getRandomValues_c44a50d8cfdaebeb":
|
||
return wrap(func(s []uint64) {
|
||
if _, ok := p.getObj(uint32(s[0])).(cryptoObj); !ok {
|
||
p.warn("getRandomValues(B): arg0 is %T, want cryptoObj", p.getObj(uint32(s[0])))
|
||
}
|
||
if b := p.asBytes(uint32(s[1])); b != nil {
|
||
rand.Read(b)
|
||
}
|
||
})
|
||
case "__wbg_randomFillSync_6c25eac9869eb53c":
|
||
return wrap(func(s []uint64) {
|
||
if b := p.asBytes(uint32(s[1])); b != nil {
|
||
rand.Read(b)
|
||
}
|
||
})
|
||
case "__wbg_now_88621c9c9a4f3ffc":
|
||
return wrap(func(s []uint64) {
|
||
s[0] = math.Float64bits(float64(time.Now().UnixMilli()))
|
||
})
|
||
|
||
// -- D: JS / Node 环境探针 --------------------------------------------
|
||
case "__wbg_crypto_38df2bab126b63dc", "__wbg_msCrypto_bd5a034af96bcba6":
|
||
return wrap(func(s []uint64) { s[0] = uint64(pushObj(cryptoObj{})) })
|
||
case "__wbg_static_accessor_GLOBAL_THIS_a1248013d790bf5f",
|
||
"__wbg_static_accessor_GLOBAL_f2e0f995a21329ff":
|
||
return wrap(func(s []uint64) { s[0] = uint64(pushObj(undefined{})) })
|
||
case "__wbg_static_accessor_SELF_24f78b6d23f286ea",
|
||
"__wbg_static_accessor_WINDOW_59fd959c540fe405":
|
||
// 服务端没有 window/self:wasm-bindgen 约定用 0 表示缺席。
|
||
return wrap(func(s []uint64) { s[0] = 0 })
|
||
case "__wbg___wbindgen_is_object_40c5a80572e8f9d3":
|
||
return wrap(func(s []uint64) {
|
||
switch p.getObj(uint32(s[0])).(type) {
|
||
case u8arr, jsMap, cryptoObj:
|
||
s[0] = 1
|
||
}
|
||
})
|
||
case "__wbg___wbindgen_is_string_b29b5c5a8065ba1a":
|
||
return wrap(func(s []uint64) {
|
||
if _, ok := p.getObj(uint32(s[0])).(string); ok {
|
||
s[0] = 1
|
||
}
|
||
})
|
||
case "__wbg___wbindgen_is_undefined_c0cca72b82b86f4d":
|
||
return wrap(func(s []uint64) {
|
||
if _, ok := p.getObj(uint32(s[0])).(undefined); ok {
|
||
s[0] = 1
|
||
}
|
||
})
|
||
case "__wbg___wbindgen_throw_81fc77679af83bc6":
|
||
return wrap(func(s []uint64) {
|
||
p.warn("wasm throw: %s", p.readStr(uint32(s[0]), uint32(s[1])))
|
||
})
|
||
case "__wbg_Error_2e59b1b37a9a34c3":
|
||
return wrap(func(s []uint64) {
|
||
s[0] = uint64(pushObj(fmt.Errorf("%s", p.readStr(uint32(s[0]), uint32(s[1])))))
|
||
})
|
||
case "__wbg_process_44c7a14e11e9f69e", "__wbg_versions_276b2795b1c6a219",
|
||
"__wbg_node_84ea875411254db1", "__wbg_require_b4edbdcf3e2a1ef0",
|
||
"__wbg_call_d578befcc3145dee", "__wbg___wbindgen_is_function_49868bde5eb1e745":
|
||
return unimpl("node/JS 环境探针")
|
||
}
|
||
return unimpl("未知")
|
||
}
|
||
|
||
func (p *probe) neverCalled(imports []api.FunctionDefinition) []string {
|
||
var out []string
|
||
for _, f := range imports {
|
||
_, name, _ := f.Import()
|
||
if p.hits[name] == 0 {
|
||
out = append(out, name)
|
||
}
|
||
}
|
||
sort.Strings(out)
|
||
return out
|
||
}
|
||
|
||
func sortedKeys[V any](m map[string]V) []string {
|
||
ks := make([]string, 0, len(m))
|
||
for k := range m {
|
||
ks = append(ks, k)
|
||
}
|
||
sort.Strings(ks)
|
||
return ks
|
||
}
|