From 4123720c845c34e834ee76e4639fbf2a8b93ee81 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Thu, 24 Sep 2026 00:13:55 +0100 Subject: [PATCH] Ask only tslang's own pointer type_info for its box thunk (Linux) An untyped `catch (e)` on Linux took every exception whose type_info is a __pointer_type_info for a tslang class, and called the box thunk it loads from a fifth field. Only tslang's _ZTIP has that field. A C++ `throw "boom"` (`_ZTIPKc`) from a linked native library read past the end of libstdc++'s type_info and called what it found: a segfault in the __pointer_type_info vtable. The same held for any pointer to a C++ class, and for a type_info emitted before the field existed. tslang's pointer type_info now sets a bit of its own in __flags (0x40000000; libstdc++ defines only the low qualifier bits), and the catch reads the thunk field only when that bit is set. Anything else is boxed as an opaque value. A throw and its catch emit their type_infos alike, so libstdc++'s qualifier check still sees equal flags. New Linux-only test test-jit-foreign-throw-catch: a small C++ library throws a const char*, a pointer to a C++ struct and an int into untyped catches, beside a tslang class that must still box as itself. Co-Authored-By: Claude Opus 5.5 --- .../LowerToLLVM/LLVMRTTIHelperVCLinuxConst.h | 7 +++ .../MLIRLogic/MLIRRTTIHelperVCLinux.h | 6 ++- tslang/lib/TypeScript/LowerToLLVM.cpp | 50 ++++++++++++------- tslang/test/tester/CMakeLists.txt | 16 ++++++ tslang/test/tester/foreign-throw/catch.ts | 44 ++++++++++++++++ .../tester/foreign-throw/foreign_throw.cpp | 22 ++++++++ 6 files changed, 126 insertions(+), 19 deletions(-) create mode 100644 tslang/test/tester/foreign-throw/catch.ts create mode 100644 tslang/test/tester/foreign-throw/foreign_throw.cpp diff --git a/tslang/include/TypeScript/LowerToLLVM/LLVMRTTIHelperVCLinuxConst.h b/tslang/include/TypeScript/LowerToLLVM/LLVMRTTIHelperVCLinuxConst.h index 17c6e60c9..240ddbc4c 100644 --- a/tslang/include/TypeScript/LowerToLLVM/LLVMRTTIHelperVCLinuxConst.h +++ b/tslang/include/TypeScript/LowerToLLVM/LLVMRTTIHelperVCLinuxConst.h @@ -69,6 +69,13 @@ constexpr const auto *pointerTypeInfoName = "_ZTVN10__cxxabiv119__pointer_type_i // or a typed catch of the class) emits the same thunk, so the linkonce_odr copies agree. constexpr const auto *boxThunkPrefix = ".eh.copy.box."; constexpr int boxThunkField = 4; +// Only tslang's own __pointer_type_info has that field, so it says so in __flags - a bit +// libstdc++ leaves alone (it defines the low qualifier bits only, and compares a throw's flags +// with a catch's, which are emitted here alike). Any other pointer exception - a C++ +// `throw "text"` (`_ZTIPKc`), a pointer to a C++ class, a type_info from before the field - +// ends after the pointee, and reading the field there called whatever came next. +constexpr int flagsField = 2; +constexpr int boxThunkFlag = 0x40000000; } // namespace ClassType } // namespace linux diff --git a/tslang/include/TypeScript/MLIRLogic/MLIRRTTIHelperVCLinux.h b/tslang/include/TypeScript/MLIRLogic/MLIRRTTIHelperVCLinux.h index 7efd6f190..46dc00ac0 100644 --- a/tslang/include/TypeScript/MLIRLogic/MLIRRTTIHelperVCLinux.h +++ b/tslang/include/TypeScript/MLIRLogic/MLIRRTTIHelperVCLinux.h @@ -602,8 +602,10 @@ class MLIRRTTIHelperVCLinux if (ti == TypeInfo::Pointer_TypeInfo) { - auto itemValueI32 = rewriter.create(loc, mth.getI32Type(), mth.getI32AttrValue(0)); - setStructValue(loc, structVal, itemValueI32, 2); + // no qualifiers; the flag says the box thunk field below is there + auto itemValueI32 = rewriter.create(loc, mth.getI32Type(), + mth.getI32AttrValue(linux::ClassType::boxThunkFlag)); + setStructValue(loc, structVal, itemValueI32, linux::ClassType::flagsField); // add base class name auto itemValue4 = rewriter.create( diff --git a/tslang/lib/TypeScript/LowerToLLVM.cpp b/tslang/lib/TypeScript/LowerToLLVM.cpp index efdd98b1f..6a84bfe16 100644 --- a/tslang/lib/TypeScript/LowerToLLVM.cpp +++ b/tslang/lib/TypeScript/LowerToLLVM.cpp @@ -5174,26 +5174,42 @@ struct SaveCatchVarOpLowering : public TsLlvmPattern [&](OpBuilder &, Location) -> mlir::Value { auto pointerTypeInfoType = LLVM::LLVMStructType::getLiteral( ctx, {ptrTy, ptrTy, th.getI32Type(), ptrTy, ptrTy}, false); - auto thunkAddress = rewriter.create( + // only a type_info whose flags say so has the thunk field - see boxThunkFlag; + // the field is not read for any other, since it may lie past the object's end + auto flagsAddress = rewriter.create( loc, ptrTy, pointerTypeInfoType, typeInfo, - ArrayRef{0, ::typescript::linux::ClassType::boxThunkField}); - auto thunk = rewriter.create(loc, ptrTy, thunkAddress); - auto hasThunk = rewriter.create(loc, LLVM::ICmpPredicate::ne, thunk, - rewriter.create(loc, ptrTy)); + ArrayRef{0, ::typescript::linux::ClassType::flagsField}); + auto flags = rewriter.create(loc, th.getI32Type(), flagsAddress); + auto flagBit = rewriter.create( + loc, th.getI32Type(), rewriter.getI32IntegerAttr(::typescript::linux::ClassType::boxThunkFlag)); + auto flagged = rewriter.create( + loc, LLVM::ICmpPredicate::ne, rewriter.create(loc, flags, flagBit), + rewriter.create(loc, th.getI32Type(), rewriter.getI32IntegerAttr(0))); return clh.conditionalExpressionLowering( - loc, ptrTy, hasThunk, + loc, ptrTy, flagged, [&](OpBuilder &, Location) -> mlir::Value { - auto anyType = mlir_ts::AnyType::get(ctx); - auto boxSlot = rewriter.create( - loc, mlir_ts::RefType::get(anyType), mlir::Value(), rewriter.getBoolAttr(false), - rewriter.getIndexAttr(0)); - auto boxSlotPtr = rewriter.create(loc, ptrTy, boxSlot); - // void thunk(ref dest, ref src); exceptionInfo is the - // address of the thrown pointer - auto thunkType = LLVM::LLVMFunctionType::get(th.getVoidType(), {ptrTy, ptrTy}); - rewriter.create(loc, thunkType, ValueRange{thunk, boxSlotPtr, exceptionInfo}); - auto boxed = rewriter.create(loc, anyType, boxSlot); - return rewriter.create(loc, ptrTy, boxed); + auto thunkAddress = rewriter.create( + loc, ptrTy, pointerTypeInfoType, typeInfo, + ArrayRef{0, ::typescript::linux::ClassType::boxThunkField}); + auto thunk = rewriter.create(loc, ptrTy, thunkAddress); + auto hasThunk = rewriter.create(loc, LLVM::ICmpPredicate::ne, thunk, + rewriter.create(loc, ptrTy)); + return clh.conditionalExpressionLowering( + loc, ptrTy, hasThunk, + [&](OpBuilder &, Location) -> mlir::Value { + auto anyType = mlir_ts::AnyType::get(ctx); + auto boxSlot = rewriter.create( + loc, mlir_ts::RefType::get(anyType), mlir::Value(), rewriter.getBoolAttr(false), + rewriter.getIndexAttr(0)); + auto boxSlotPtr = rewriter.create(loc, ptrTy, boxSlot); + // void thunk(ref dest, ref src); exceptionInfo is the + // address of the thrown pointer + auto thunkType = LLVM::LLVMFunctionType::get(th.getVoidType(), {ptrTy, ptrTy}); + rewriter.create(loc, thunkType, ValueRange{thunk, boxSlotPtr, exceptionInfo}); + auto boxed = rewriter.create(loc, anyType, boxSlot); + return rewriter.create(loc, ptrTy, boxed); + }, + [&](OpBuilder &, Location) { return boxThrown(mlir_ts::OpaqueType::get(ctx)); }); }, [&](OpBuilder &, Location) { return boxThrown(mlir_ts::OpaqueType::get(ctx)); }); }, diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index d429687fa..e975d869b 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -2459,3 +2459,19 @@ if (TSLANG_TEST_X86) message(STATUS "x86 twins: ${x86_twin_count} registered (${x86_excluded_count} excluded), " "${x86_jit_skipped_count} -jit registrations without a twin") endif() + +# An untyped catch of a C++ exception tslang did not throw (Itanium only: the box thunk field of +# a __pointer_type_info is a Linux mechanism). foreign_throw.cpp throws a `const char*`, a +# pointer to a C++ struct and an int; the catch must box each without calling into the type_info. +if (NOT WIN32) + add_library(tslang_foreign_throw SHARED foreign-throw/foreign_throw.cpp) + add_dependencies(tslang_foreign_throw TypeScriptRuntime) + add_test(NAME test-jit-foreign-throw-catch + COMMAND $ --emit=jit --no-default-lib + --shared-libs=$ + --shared-libs=$ + "${PROJECT_SOURCE_DIR}/test/tester/foreign-throw/catch.ts") + set_tests_properties(test-jit-foreign-throw-catch + PROPERTIES PASS_REGULAR_EXPRESSION "done\\." + FAIL_REGULAR_EXPRESSION "Stack dump|Segmentation|assertion failed") +endif() diff --git a/tslang/test/tester/foreign-throw/catch.ts b/tslang/test/tester/foreign-throw/catch.ts new file mode 100644 index 000000000..fb009a8b3 --- /dev/null +++ b/tslang/test/tester/foreign-throw/catch.ts @@ -0,0 +1,44 @@ +// An untyped catch of a C++ exception tslang did not throw. A pointer exception's type_info is a +// __pointer_type_info, as a thrown tslang class's is, and the catch called the box thunk field +// that only tslang's has - past the end of libstdc++'s, so a `throw "boom"` from C++ made a +// wild call. Only a type_info flagged as tslang's is asked for its thunk now. +declare function foreign_throw_cstr(): void; +declare function foreign_throw_ptr(): void; +declare function foreign_throw_int(): void; + +class Own { + constructor(public n: number) {} +} + +function main() { + let caught = 0; + + try { + foreign_throw_cstr(); + } catch (e) { + caught++; + } + + try { + foreign_throw_ptr(); + } catch (e) { + caught++; + } + + try { + foreign_throw_int(); + } catch (e) { + caught++; + } + + // a tslang class still goes through its box thunk + try { + throw new Own(5); + } catch (e) { + caught++; + assert(e instanceof Own, "own class boxed as itself"); + } + + assert(caught == 4, "every exception caught"); + print("done."); +} diff --git a/tslang/test/tester/foreign-throw/foreign_throw.cpp b/tslang/test/tester/foreign-throw/foreign_throw.cpp new file mode 100644 index 000000000..2b40e2c4a --- /dev/null +++ b/tslang/test/tester/foreign-throw/foreign_throw.cpp @@ -0,0 +1,22 @@ +// C++ exceptions that tslang never threw, for foreign-throw/catch.ts +struct Foreign +{ + int x; +}; + +static Foreign foreign{42}; + +extern "C" void foreign_throw_cstr() +{ + throw "boom"; +} + +extern "C" void foreign_throw_ptr() +{ + throw &foreign; +} + +extern "C" void foreign_throw_int() +{ + throw 7; +}