// 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("", 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(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 }