Skip to content

Commit 2eb7ded

Browse files
committed
Recover only nonfatal LTSM exceptions
1 parent 5c1bf1d commit 2eb7ded

4 files changed

Lines changed: 191 additions & 179 deletions

File tree

‎pkg/ltsm/embind.go‎

Lines changed: 14 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -151,11 +151,6 @@ type Imports struct {
151151
classByName map[string]*ClassInfo
152152
emval *EmvalTable
153153

154-
// dead is set when a WASM call hits a fatal path (_abort, C++ throw,
155-
// EM_JS throw) that cannot be recovered. Later calls return deadErr.
156-
dead bool
157-
deadErr error
158-
159154
// File descriptor state for /dev/urandom emulation
160155
urandomFD uint32
161156
nextFD uint32
@@ -177,26 +172,18 @@ func (imp *Imports) SetModule(m *Module) {
177172
imp.mod = m
178173
}
179174

180-
// markDead records a fatal WASM error so subsequent calls fail fast.
181-
// The first fatal error is retained.
182-
func (imp *Imports) markDead(v any) {
183-
if imp.dead {
184-
return
185-
}
186-
imp.dead = true
187-
if err, ok := v.(error); ok {
188-
imp.deadErr = err
189-
} else {
190-
imp.deadErr = fmt.Errorf("%v", v)
191-
}
192-
}
175+
// wasmException marks a normal exception reported by the embedded module.
176+
type wasmException string
193177

194-
// checkDead is the defer target for functions that dispatch into the WASM
195-
// module: a fatal panic is converted to the dead-runtime error.
196-
func (imp *Imports) checkDead(err *error) {
178+
// recoverError converts ordinary WASM exceptions to errors. An abort or
179+
// unexpected panic may leave the runtime unusable, so propagate it.
180+
func recoverError(err *error) {
197181
if v := recover(); v != nil {
198-
imp.markDead(v)
199-
*err = imp.deadErr
182+
if exception, ok := v.(wasmException); ok {
183+
*err = errors.New(string(exception))
184+
return
185+
}
186+
panic(v)
200187
}
201188
}
202189

@@ -523,7 +510,7 @@ func (imp *Imports) Import_o(p0, p1, p2 uint32) uint32 { // EM_JS dispatcher
523510
if len(args) >= 2 {
524511
errMsg = imp.readCStr(args[1])
525512
}
526-
panic(fmt.Sprintf("ltsm: EM_JS error: %s: %s", errType, errMsg))
513+
panic(wasmException(fmt.Sprintf("ltsm: EM_JS error: %s: %s", errType, errMsg)))
527514

528515
case 1456985: // init getRandomValues
529516
return 0
@@ -713,7 +700,7 @@ func (imp *Imports) Import_A(p0, p1 uint32) uint32 { // _emval_get_property
713700
}
714701

715702
func (imp *Imports) Import_B(p0, p1, p2 uint32) { // __cxa_throw
716-
panic(fmt.Sprintf("ltsm: C++ exception thrown (type=%d, ptr=%d)", p1, p0))
703+
panic(wasmException(fmt.Sprintf("ltsm: C++ exception thrown (type=%d, ptr=%d)", p1, p0)))
717704
}
718705

719706
func (imp *Imports) Import_C(p0 uint32) uint32 { // emscripten_resize_heap
@@ -866,9 +853,6 @@ func toFloat64(v any) float64 {
866853

867854
// CallMethod calls an embind instance method on a C++ object.
868855
func (imp *Imports) CallMethod(className, methodName string, thisPtr uint32, args ...uint32) (uint32, error) {
869-
if imp.dead {
870-
return 0, imp.deadErr
871-
}
872856
ci := imp.classByName[className]
873857
if ci == nil {
874858
return 0, fmt.Errorf("ltsm: class %q not found", className)
@@ -897,9 +881,6 @@ func (imp *Imports) CallMethod(className, methodName string, thisPtr uint32, arg
897881

898882
// CallStatic calls an embind static method.
899883
func (imp *Imports) CallStatic(className, methodName string, args ...uint32) (uint32, error) {
900-
if imp.dead {
901-
return 0, imp.deadErr
902-
}
903884
ci := imp.classByName[className]
904885
if ci == nil {
905886
return 0, fmt.Errorf("ltsm: class %q not found", className)
@@ -920,9 +901,6 @@ func (imp *Imports) CallStatic(className, methodName string, args ...uint32) (ui
920901

921902
// Construct calls an embind class constructor.
922903
func (imp *Imports) Construct(className string, args ...uint32) (uint32, error) {
923-
if imp.dead {
924-
return 0, imp.deadErr
925-
}
926904
ci := imp.classByName[className]
927905
if ci == nil {
928906
return 0, fmt.Errorf("ltsm: class %q not found", className)
@@ -943,10 +921,7 @@ func (imp *Imports) Construct(className string, args ...uint32) (uint32, error)
943921

944922
// Destroy invokes the registered C++ destructor for an embind object.
945923
func (imp *Imports) Destroy(className string, ptr uint32) (err error) {
946-
defer imp.checkDead(&err)
947-
if imp.dead {
948-
return imp.deadErr
949-
}
924+
defer recoverError(&err)
950925
if ptr == 0 {
951926
return nil
952927
}
@@ -989,7 +964,7 @@ func (imp *Imports) MarkSecureKeyExportable(ptr uint32) {
989964
// callIndirect dispatches to the appropriate callIndirectTN based on parameter count and types.
990965
// Most embind methods use all-uint32 params, but some have uint64 (bigint) params or returns.
991966
func (imp *Imports) callIndirect(invokerIdx uint32, argTypes []uint32, callArgs []uint32) (ret uint32, err error) {
992-
defer imp.checkDead(&err)
967+
defer recoverError(&err)
993968
m := imp.mod
994969
nArgs := len(callArgs)
995970

‎pkg/ltsm/embind_test.go‎

Lines changed: 66 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"crypto/sha256"
55
"encoding/base64"
66
"errors"
7+
"strings"
78
"testing"
89
)
910

@@ -467,39 +468,78 @@ func TestCurve25519KeyGenerate(t *testing.T) {
467468
}
468469
}
469470

470-
func TestCallIndirectConvertsPanicToError(t *testing.T) {
471+
func TestUnexpectedDispatchPanicIsFatal(t *testing.T) {
471472
_, imp := initModule(t)
473+
defer func() {
474+
if recover() == nil {
475+
t.Fatal("unexpected dispatch panic was swallowed")
476+
}
477+
}()
478+
_, _ = imp.callIndirect(0, nil, nil)
479+
}
480+
481+
func TestAbortRemainsFatal(t *testing.T) {
482+
var err error
483+
defer func() {
484+
recovered := recover()
485+
fatal, ok := recovered.(error)
486+
if !ok || !errors.Is(fatal, ErrAbort) || err != nil {
487+
t.Fatalf("abort was swallowed: recovered=%v, err=%v", recovered, err)
488+
}
489+
}()
490+
func() {
491+
defer recoverError(&err)
492+
panic(ErrAbort)
493+
}()
494+
}
472495

473-
// A nil argTypes slice panics inside the dispatch; the fatal panic must
474-
// be converted to an error and mark the runtime dead.
475-
ret, err := imp.callIndirect(0, nil, nil)
476-
if ret != 0 {
477-
t.Fatalf("ret = %d, want 0", ret)
496+
func TestV1FallbackAfterOrdinaryV2DecryptError(t *testing.T) {
497+
rt, err := NewRuntime()
498+
if err != nil {
499+
t.Fatal(err)
478500
}
479-
if err == nil {
480-
t.Fatal("expected error from recovered panic")
501+
senderKey, err := rt.Curve25519KeyGenerate()
502+
if err != nil {
503+
t.Fatal(err)
481504
}
482-
if !imp.dead {
483-
t.Fatal("runtime not marked dead after recovered panic")
505+
receiverKey, err := rt.Curve25519KeyGenerate()
506+
if err != nil {
507+
t.Fatal(err)
484508
}
485-
}
486-
487-
func TestDeadRuntimeFailsFast(t *testing.T) {
488-
_, imp := initModule(t)
489-
490-
imp.markDead(ErrAbort)
491-
imp.markDead("second fatal error") // first fatal error wins
492-
493-
if _, err := imp.CallMethod("SecureKey", "loadToken", 0); !errors.Is(err, ErrAbort) {
494-
t.Fatalf("CallMethod on dead runtime: %v", err)
509+
senderPub, err := rt.Curve25519KeyGetPublicKey(senderKey)
510+
if err != nil {
511+
t.Fatal(err)
495512
}
496-
if _, err := imp.CallStatic("SecureKey", "loadToken", 0); !errors.Is(err, ErrAbort) {
497-
t.Fatalf("CallStatic on dead runtime: %v", err)
513+
receiverPub, err := rt.Curve25519KeyGetPublicKey(receiverKey)
514+
if err != nil {
515+
t.Fatal(err)
516+
}
517+
senderChannel, err := rt.Curve25519KeyCreateChannel(senderKey, receiverPub)
518+
if err != nil {
519+
t.Fatal(err)
498520
}
499-
if _, err := imp.Construct("Hmac"); !errors.Is(err, ErrAbort) {
500-
t.Fatalf("Construct on dead runtime: %v", err)
521+
receiverChannel, err := rt.Curve25519KeyCreateChannel(receiverKey, senderPub)
522+
if err != nil {
523+
t.Fatal(err)
501524
}
502-
if err := imp.Destroy("SecureKey", 1); !errors.Is(err, ErrAbort) {
503-
t.Fatalf("Destroy on dead runtime: %v", err)
525+
plaintext := []byte("iOS V1 message")
526+
ciphertext, err := rt.E2EEChannelEncryptV1(senderChannel, plaintext)
527+
if err != nil {
528+
t.Fatal(err)
529+
}
530+
if _, err := rt.E2EEChannelDecryptV2(receiverChannel, "to", "from", 1, 2, 0, ciphertext); err == nil || !strings.Contains(err.Error(), "C++ exception thrown") {
531+
t.Fatalf("V2 error = %v, want ordinary C++ exception", err)
532+
}
533+
tampered := append([]byte(nil), ciphertext...)
534+
tampered[len(tampered)-1] ^= 1
535+
if _, err := rt.E2EEChannelDecryptV1(receiverChannel, tampered); err == nil || !strings.Contains(err.Error(), "EM_JS error") {
536+
t.Fatalf("tampered V1 error = %v, want ordinary EM_JS exception", err)
537+
}
538+
decrypted, err := rt.E2EEChannelDecryptV1(receiverChannel, ciphertext)
539+
if err != nil {
540+
t.Fatalf("V1 fallback failed: %v", err)
541+
}
542+
if string(decrypted) != string(plaintext) {
543+
t.Fatalf("V1 fallback = %q, want %q", decrypted, plaintext)
504544
}
505545
}

‎pkg/ltsm/runtime.go‎

Lines changed: 4 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -21,25 +21,10 @@ func NewRuntime() (*Runtime, error) {
2121
mod := NewModule(imp)
2222
imp.SetModule(mod)
2323

24-
var initErr error
25-
func() {
26-
defer func() {
27-
if v := recover(); v != nil {
28-
if err, ok := v.(error); ok {
29-
initErr = err
30-
} else {
31-
initErr = fmt.Errorf("%v", v)
32-
}
33-
}
34-
}()
35-
// Phase 1: __wasm_call_ctors
36-
mod.fP()
37-
// Phase 2: embind type/class registrations
38-
mod.fT()
39-
}()
40-
if initErr != nil {
41-
return nil, fmt.Errorf("ltsm: runtime init failed: %w", initErr)
42-
}
24+
// Phase 1: __wasm_call_ctors
25+
mod.fP()
26+
// Phase 2: embind type/class registrations
27+
mod.fT()
4328

4429
return &Runtime{mod: mod, imp: imp}, nil
4530
}
@@ -49,12 +34,6 @@ func (rt *Runtime) ModuleMem() []byte {
4934
return rt.mod.mem
5035
}
5136

52-
// Dead reports whether the runtime suffered a fatal WASM error.
53-
func (rt *Runtime) Dead() bool { return rt.imp.dead }
54-
55-
// DeadError returns the fatal error that stopped the runtime, if any.
56-
func (rt *Runtime) DeadError() error { return rt.imp.deadErr }
57-
5837
// Close releases resources. No-op for the transpiled module.
5938
func (rt *Runtime) Close() {}
6039

0 commit comments

Comments
 (0)