diff --git a/tslang/include/TypeScript/TypeScriptOps.td b/tslang/include/TypeScript/TypeScriptOps.td index 9f5e15283..9958dff85 100644 --- a/tslang/include/TypeScript/TypeScriptOps.td +++ b/tslang/include/TypeScript/TypeScriptOps.td @@ -2354,9 +2354,11 @@ def TypeScript_SetLengthOfOp : TypeScript_Op<"SetLengthOf"> { let results = (outs ); } -def TypeScript_SetStringLengthOp : TypeScript_Op<"SetStringLength"> { - let arguments = (ins Arg:$op, Index:$size); - let results = (outs ); +// A new string of `size` bytes holding the start of `op` - `s.length = n`, whose result is then +// assigned to `s`. The source is left alone: whoever else holds it still does. +def TypeScript_StringResizeOp : TypeScript_Op<"StringResize"> { + let arguments = (ins TypeScript_String:$op, Index:$size); + let results = (outs Res:$result); } def TypeScript_StringLengthOp : TypeScript_Op<"StringLength"> { diff --git a/tslang/lib/TypeScript/LowerToAffineLoops.cpp b/tslang/lib/TypeScript/LowerToAffineLoops.cpp index 8177da8ef..e08535693 100644 --- a/tslang/lib/TypeScript/LowerToAffineLoops.cpp +++ b/tslang/lib/TypeScript/LowerToAffineLoops.cpp @@ -2657,7 +2657,7 @@ void AddTsAffineLegalOps(ConversionTarget &target) mlir_ts::ElementRefOp, mlir_ts::PointerOffsetRefOp, mlir_ts::FuncOp, mlir_ts::GlobalOp, mlir_ts::GlobalResultOp, mlir_ts::DefaultOp, mlir_ts::HasValueOp, mlir_ts::ValueOp, mlir_ts::ValueOrDefaultOp, mlir_ts::NullOp, mlir_ts::ParseFloatOp, mlir_ts::ParseIntOp, mlir_ts::IsNaNOp, mlir_ts::PrintOp, mlir_ts::ConvertFOp, mlir_ts::SizeOfOp, mlir_ts::TypeDescriptorOp, mlir_ts::RetainOp, mlir_ts::ReleaseOp, mlir_ts::RetainSlotOp, mlir_ts::ReleaseSlotOp, mlir_ts::RetainCellOp, mlir_ts::ReleaseCellOp, mlir_ts::StoreOp, mlir_ts::SymbolRefOp, mlir_ts::LengthOfOp, mlir_ts::SetLengthOfOp, - mlir_ts::StringLengthOp, mlir_ts::SetStringLengthOp, mlir_ts::StringConcatOp, mlir_ts::StringCompareOp, mlir_ts::AnyCompareOp, + mlir_ts::StringLengthOp, mlir_ts::StringResizeOp, mlir_ts::StringConcatOp,mlir_ts::StringCompareOp, mlir_ts::AnyCompareOp, mlir_ts::LoadOp, mlir_ts::LoadSaveOp, mlir_ts::NewOp, mlir_ts::CreateTupleOp, mlir_ts::DeconstructTupleOp, mlir_ts::CreateArrayOp, mlir_ts::NewEmptyArrayOp, mlir_ts::NewArrayOp, mlir_ts::DeleteOp, mlir_ts::PropertyRefOp, mlir_ts::InsertPropertyOp, mlir_ts::ExtractPropertyOp, mlir_ts::LogicalBinaryOp, mlir_ts::UndefOp, mlir_ts::VariableOp, mlir_ts::DebugVariableOp, mlir_ts::AllocaOp, diff --git a/tslang/lib/TypeScript/LowerToLLVM.cpp b/tslang/lib/TypeScript/LowerToLLVM.cpp index b9a79d179..a0e6a922d 100644 --- a/tslang/lib/TypeScript/LowerToLLVM.cpp +++ b/tslang/lib/TypeScript/LowerToLLVM.cpp @@ -666,47 +666,45 @@ class SetLengthOfOpLowering : public TsLlvmPattern rewriter.create(loc, allocated, currentPtrPtr); - // `arr.length = n` on a grown array exposes slots the allocator has not written, and a - // store into one gives up what the slot held first (`isOwnedElementSlot`) - so the - // release reads whatever was last in that memory. `result.length = this.length` followed - // by `result[i] = ..` is exactly the default library's `Array.map`, which is why - // `arrS.map(e => e + "_")` crashed about one run in three (§9.37). + // `arr.length = n` on a grown array exposes slots the allocator has not written. They read + // as zero: the default library's Set and Map grow their `int[]` buckets this way and take + // an unwritten bucket for an empty one, and under rc and own a store into a slot gives up + // what the slot held first (`isOwnedElementSlot`), so the release would read whatever was + // last in that memory - `result.length = this.length` followed by `result[i] = ..` is the + // default library's `Array.map`, and `arrS.map(e => e + "_")` crashed about one run in + // three (§9.37). // - // Only where an element owns something, and only under -mm=rc: nothing reads an - // unwritten slot in the other models, and the memset is not free. - if (tsLlvmContext->compileOptions.tracksOwnership()) - { - MLIRTypeHelper mth(rewriter.getContext(), tsLlvmContext->compileOptions); - if (mth.ownsHeapMemory(loc, elementType)) - { - auto oldBytes = rewriter.create(loc, th.getIndexType(), - ValueRange{sizeOfTypeAsIndexType, - rewriter.create( - loc, th.getIndexType(), countAsIndexType)}); - auto grew = rewriter.create(loc, mlir::index::IndexCmpPredicate::UGT, - multSizeOfTypeValue, oldBytes); - - auto *currentBlock = rewriter.getInsertionBlock(); - auto *continuationBlock = rewriter.splitBlock(currentBlock, rewriter.getInsertionPoint()); - auto *zeroBlock = rewriter.createBlock(continuationBlock); - - rewriter.setInsertionPointToEnd(zeroBlock); - auto tailStart = rewriter.create(loc, ptrType, th.getI8Type(), allocated, - ValueRange{rewriter.create( - loc, llvmIndexType, oldBytes)}); - auto tailBytes = rewriter.create(loc, th.getIndexType(), - multSizeOfTypeValue, oldBytes); - rewriter.create( - loc, tailStart, - rewriter.create(loc, th.getI8Type(), rewriter.getI8IntegerAttr(0)), - rewriter.create(loc, llvmIndexType, tailBytes), /*isVolatile=*/false); - rewriter.create(loc, ValueRange{}, continuationBlock); - - rewriter.setInsertionPointToEnd(currentBlock); - rewriter.create(loc, grew, zeroBlock, continuationBlock); - - rewriter.setInsertionPointToStart(continuationBlock); - } + // Every model but gc, whose collector hands back cleared memory - also when a block + // shrunk in place grows again, GC_realloc having cleared what the shrink gave up. + if (!tsLlvmContext->compileOptions.needsGCRuntime()) + { + auto oldBytes = rewriter.create(loc, th.getIndexType(), + ValueRange{sizeOfTypeAsIndexType, + rewriter.create( + loc, th.getIndexType(), countAsIndexType)}); + auto grew = rewriter.create(loc, mlir::index::IndexCmpPredicate::UGT, + multSizeOfTypeValue, oldBytes); + + auto *currentBlock = rewriter.getInsertionBlock(); + auto *continuationBlock = rewriter.splitBlock(currentBlock, rewriter.getInsertionPoint()); + auto *zeroBlock = rewriter.createBlock(continuationBlock); + + rewriter.setInsertionPointToEnd(zeroBlock); + auto tailStart = rewriter.create(loc, ptrType, th.getI8Type(), allocated, + ValueRange{rewriter.create( + loc, llvmIndexType, oldBytes)}); + auto tailBytes = rewriter.create(loc, th.getIndexType(), + multSizeOfTypeValue, oldBytes); + rewriter.create( + loc, tailStart, + rewriter.create(loc, th.getI8Type(), rewriter.getI8IntegerAttr(0)), + rewriter.create(loc, llvmIndexType, tailBytes), /*isVolatile=*/false); + rewriter.create(loc, ValueRange{}, continuationBlock); + + rewriter.setInsertionPointToEnd(currentBlock); + rewriter.create(loc, grew, zeroBlock, continuationBlock); + + rewriter.setInsertionPointToStart(continuationBlock); } auto newCountAsLLVMType = rewriter.create(loc, llvmIndexType, newCountAsIndexType); @@ -743,34 +741,46 @@ class StringLengthOpLowering : public TsLlvmPattern } }; -class SetStringLengthOpLowering : public TsLlvmPattern +// A new block of `size` bytes with as much of the source as fits, its terminator included; the +// source is read and nothing else. A null source is copied as the empty string. +class StringResizeOpLowering : public TsLlvmPattern { public: - using TsLlvmPattern::TsLlvmPattern; + using TsLlvmPattern::TsLlvmPattern; - LogicalResult matchAndRewrite(mlir_ts::SetStringLengthOp op, Adaptor transformed, + LogicalResult matchAndRewrite(mlir_ts::StringResizeOp op, Adaptor transformed, ConversionPatternRewriter &rewriter) const final { TypeHelper th(rewriter); - CodeLogicHelper clh(op, rewriter); LLVMCodeHelper ch(op, rewriter, getTypeConverter(), tsLlvmContext->compileOptions); TypeConverterHelper tch(getTypeConverter()); auto loc = op->getLoc(); - - // TODO implement str concat auto i8PtrTy = th.getPtrType(); + auto llvmIndexType = tch.convertType(th.getIndexType()); - mlir::Value ptr = transformed.getOp(); - mlir::Value size = transformed.getSize(); + auto strType = mlir_ts::StringType::get(rewriter.getContext()); + mlir::Value source = transformed.getOp(); + mlir::Value emptyText = rewriter.create( + loc, tch.convertType(strType), rewriter.create(loc, strType, rewriter.getStringAttr(""))); + auto isNull = rewriter.create(loc, LLVM::ICmpPredicate::eq, source, + rewriter.create(loc, i8PtrTy)); + source = rewriter.create(loc, isNull, emptyText, source); - mlir::Value strPtr = rewriter.create(loc, i8PtrTy, ptr); + mlir::Value size = transformed.getSize(); + auto newStringValue = ch.MemoryAlloc(size); - mlir::Value newStringValue = ch.MemoryRealloc(strPtr, size); + auto strlenFuncOp = ch.getOrInsertFunction("strlen", th.getFunctionType(llvmIndexType, {i8PtrTy})); + mlir::Value sourceBytes = rewriter.create(loc, strlenFuncOp, ValueRange{source}).getResult(); + sourceBytes = rewriter.create( + loc, llvmIndexType, + ValueRange{sourceBytes, rewriter.create(loc, llvmIndexType, rewriter.getIntegerAttr(llvmIndexType, 1))}); + auto fits = rewriter.create(loc, LLVM::ICmpPredicate::ult, sourceBytes, size); + auto copyBytes = rewriter.create(loc, fits, sourceBytes, size); - rewriter.create(loc, newStringValue, ptr); - rewriter.eraseOp(op); + rewriter.create(loc, newStringValue, source, copyBytes, /*isVolatile=*/false); + rewriter.replaceOp(op, ValueRange{newStringValue}); return success(); } }; @@ -4171,7 +4181,7 @@ struct GlobalOpLowering : public TsLlvmPattern isa(op) || isa(op) || isa(op) || isa(op) || isa(op) || isa(op) || isa(op) || isa(op) || - isa(op) || isa(op) || + isa(op) || isa(op) || isa(op) || isa(op) || // a tagged union's value goes in and out through memory (an alloca and a copy), // which a global's initializer cannot hold @@ -7819,7 +7829,7 @@ void TypeScriptToLLVMLoweringPass::runOnOperation() DeconstructTupleOpLowering, CreateArrayOpLowering, NewEmptyArrayOpLowering, NewArrayOpLowering, ArrayPushOpLowering, ArrayPopOpLowering, ArrayUnshiftOpLowering, ArrayShiftOpLowering, ArraySpliceOpLowering, ArrayViewOpLowering, DeleteOpLowering, ParseFloatOpLowering, ParseIntOpLowering, IsNaNOpLowering, PrintOpLowering, ConvertFOpLowering, StoreOpLowering, SizeOfOpLowering, TypeDescriptorOpLowering, RetainOpLowering, ReleaseOpLowering, RetainSlotOpLowering, ReleaseSlotOpLowering, RetainCellOpLowering, ReleaseCellOpLowering, - InsertPropertyOpLowering, LengthOfOpLowering, SetLengthOfOpLowering, StringLengthOpLowering, SetStringLengthOpLowering, StringConcatOpLowering, + InsertPropertyOpLowering, LengthOfOpLowering, SetLengthOfOpLowering, StringLengthOpLowering, StringResizeOpLowering, StringConcatOpLowering, StringCompareOpLowering, AnyCompareOpLowering, CharToStringOpLowering, UndefOpLowering, CopyStructOpLowering, MemoryCopyOpLowering, MemoryMoveOpLowering, LoadSaveValueLowering, ThrowUnwindOpLowering, ThrowCallOpLowering, VariableOpLowering, DebugVariableOpLowering, AllocaOpLowering, InvokeOpLowering, InvokeHybridOpLowering, VirtualSymbolRefOpLowering, ThisVirtualSymbolRefOpLowering, InterfaceSymbolRefOpLowering, diff --git a/tslang/lib/TypeScript/MLIRGenImpl.h b/tslang/lib/TypeScript/MLIRGenImpl.h index b86f77e1a..54573c20a 100644 --- a/tslang/lib/TypeScript/MLIRGenImpl.h +++ b/tslang/lib/TypeScript/MLIRGenImpl.h @@ -5525,17 +5525,18 @@ class MLIRGenImpl } else if (auto stringLength = leftExpressionValueBeforeCast.getDefiningOp()) { - MLIRCodeLogic mcl(builder, compileOptions); - auto stringValueLoaded = mcl.GetReferenceFromValue(location, stringLength.getOp()); - if (!stringValueLoaded) - { - emitError(location) << "Can't get reference of the string, ensure const string is not used"; - return mlir::failure(); - } - - // special case to resize array + // A string is a value: resizing one makes a new string, copied at its new size and + // assigned as `s = s + t` would be, and whoever else held the old one still does - the + // caller whose string a `this: string` parameter borrowed, a temporary, another + // variable. Resizing in place reallocated the block, which freed it when it moved + // under every model but gc, and under rc the holder then released it again. syncSavingValue(stringLength.getResult().getType()); - builder.create(location, stringValueLoaded, savingValue); + auto resized = builder.create(location, getStringType(), stringLength.getOp(), + savingValue); + markFreshBlockOwned(location, resized); + + auto saved = mlirGenSaveLogicOneItem(location, stringLength.getOp(), resized, genContext); + EXIT_IF_FAILED(saved) } else { @@ -7485,8 +7486,18 @@ class MLIRGenImpl operands.pop_back_n(toIndex - fromIndex); + auto packed = !varArgOperands.empty() || isa(varArgsType); + + // the data block about to be filled releases every element when it dies; without + // this, `a.concat(["x"])` handed the callee an array nobody held, and its first + // `for (const item of other)` freed it (§9.21, as for an array literal) + if (packed) + { + mlirGenRetainCaptured(location, varArgOperands); + } + // create array - auto array = varArgOperands.empty() && !isa(varArgsType) + auto array = !packed ? V(builder.create(location, varArgsType)) : V(builder.create(location, varArgsType, varArgOperands)); operands.push_back(array); diff --git a/tslang/lib/TypeScript/OwnershipFacts.h b/tslang/lib/TypeScript/OwnershipFacts.h index eb98a3e44..0630acdca 100644 --- a/tslang/lib/TypeScript/OwnershipFacts.h +++ b/tslang/lib/TypeScript/OwnershipFacts.h @@ -183,7 +183,7 @@ inline bool isFresh(mlir::Value value) } if (mlir::isa(def)) + mlir_ts::StringResizeOp, mlir_ts::CharToStringOp>(def)) { return true; } diff --git a/tslang/lib/TypeScript/OwnershipInferencePass.cpp b/tslang/lib/TypeScript/OwnershipInferencePass.cpp index 69d756c3c..ddedc8c46 100644 --- a/tslang/lib/TypeScript/OwnershipInferencePass.cpp +++ b/tslang/lib/TypeScript/OwnershipInferencePass.cpp @@ -1446,7 +1446,8 @@ class OwnershipInferencePass : public mlir::PassWrapper(user)) + mlir_ts::ArithmeticBinaryOp, mlir_ts::LogicalBinaryOp, mlir_ts::StringConcatOp, + mlir_ts::StringResizeOp>(user)) { return true; } diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index 2a6d62279..6ccbcb978 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -429,6 +429,9 @@ tslang_add_test(NAME test-compile-00-owned-interfaces COMMAND test-runner "${PRO tslang_add_test(NAME test-compile-00-owned-closures COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00owned_closures.ts") tslang_add_test(NAME test-compile-00-owned-any-boxing COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00owned_any_boxing.ts") tslang_add_test(NAME test-compile-00-owned-strings COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00owned_strings.ts") +tslang_add_test(NAME test-compile-00-string-length-resize COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00string_length_resize.ts") +tslang_add_test(NAME test-compile-00-array-length-grow-zeroed COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00array_length_grow_zeroed.ts") +tslang_add_test(NAME test-compile-00-rest-args-pack-owned COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00rest_args_pack_owned.ts") tslang_add_test(NAME test-compile-00-owned-generators COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00owned_generators.ts") tslang_add_test(NAME test-compile-00-owned-globals COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00owned_globals.ts") tslang_add_test(NAME test-compile-00-owned-generator-locals COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00owned_generator_locals.ts") @@ -885,6 +888,9 @@ tslang_add_test(NAME test-jit-00-owned-interfaces COMMAND test-runner -jit "${PR tslang_add_test(NAME test-jit-00-owned-closures COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00owned_closures.ts") tslang_add_test(NAME test-jit-00-owned-any-boxing COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00owned_any_boxing.ts") tslang_add_test(NAME test-jit-00-owned-strings COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00owned_strings.ts") +tslang_add_test(NAME test-jit-00-string-length-resize COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00string_length_resize.ts") +tslang_add_test(NAME test-jit-00-array-length-grow-zeroed COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00array_length_grow_zeroed.ts") +tslang_add_test(NAME test-jit-00-rest-args-pack-owned COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00rest_args_pack_owned.ts") tslang_add_test(NAME test-jit-00-owned-generators COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00owned_generators.ts") tslang_add_test(NAME test-jit-00-owned-globals COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00owned_globals.ts") tslang_add_test(NAME test-jit-00-owned-generator-locals COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00owned_generator_locals.ts") @@ -1573,6 +1579,7 @@ set(TSLANG_CORPUS 00any.ts 00array_assignment5.ts 00array_cond_access.ts + 00array_length_grow_zeroed.ts 00array_move_wide_elements.ts 00array_of.ts 00array_shift.ts @@ -1804,6 +1811,7 @@ set(TSLANG_CORPUS 00reference_index_bug.ts 00reference_null_bug.ts 00reference_ref_deref.ts + 00rest_args_pack_owned.ts 00safe_cast_bug.ts 00safe_cast_field_access.ts 00safe_cast_early_exit.ts @@ -1819,6 +1827,7 @@ set(TSLANG_CORPUS 00spread.ts 00stack_test.ts 00str_null.ts + 00string_length_resize.ts 00strings.ts 00switch.ts 00symbol.ts diff --git a/tslang/test/tester/tests/00array_length_grow_zeroed.ts b/tslang/test/tester/tests/00array_length_grow_zeroed.ts new file mode 100644 index 000000000..efb8c1591 --- /dev/null +++ b/tslang/test/tester/tests/00array_length_grow_zeroed.ts @@ -0,0 +1,54 @@ +// `arr.length = n` on a grown array exposes slots nothing has written, and they read as zero. The +// default library's Set and Map grow their `int[]` buckets this way and take an unwritten bucket for +// an empty one; under every model but gc the new slots held whatever the allocator's memory held +// last, which broke the hash chains - `new Set([1, 2]).union(new Set([2, 3]))` lost elements, a run +// in two, depending on what the heap had been used for. +// +// So the heap is dirtied first: arrays of the same size are filled and dropped, and the one grown +// next is handed memory that still holds their values. + +function dirty() { + for (let k = 0; k < 8; k++) { + let d: int[] = []; + d.length = 64; + for (let i = 0; i < 64; i++) { + d[i] = 12345; + } + } +} + +function sum(a: int[]) { + let total = 0; + for (let i = 0; i < a.length; i++) { + total += a[i]; + } + + return total; +} + +function grownFromEmpty() { + dirty(); + let a: int[] = []; + a.length = 64; + return sum(a) == 0; +} + +// Shrunk and grown again: the slots the shrink gave up come back empty, not with what they held. +function shrunkThenGrown() { + let a: int[] = []; + a.length = 64; + for (let i = 0; i < 64; i++) { + a[i] = 7; + } + + a.length = 2; + a.length = 64; + return sum(a) == 14; +} + +function main() { + assert(grownFromEmpty(), "a grown array's new slots read as zero"); + assert(shrunkThenGrown(), "slots given up by a shrink read as zero when the array grows again"); + + print("done."); +} diff --git a/tslang/test/tester/tests/00rest_args_pack_owned.ts b/tslang/test/tester/tests/00rest_args_pack_owned.ts new file mode 100644 index 000000000..d7e01cc4b --- /dev/null +++ b/tslang/test/tester/tests/00rest_args_pack_owned.ts @@ -0,0 +1,70 @@ +// The array a rest parameter receives is built at the call from the arguments, and like an array +// literal's it releases every element when it dies - so it has to take a reference to each. Under rc +// a call through an extension function, `fruits.concat(["x", "y"])`, packed its arguments without +// one: the callee's first `for (const item of other)` took the only reference and gave it back, +// freeing the array, and the second loop read what was left (the default library's Array.concat, +// which printed garbage for the first element it copied and corrupted the heap). + +namespace __Array { + function joinedWith(this: T[], ...other: T[][]): string { + let count = this.length; + for (const item of other) { + count += item.length; + } + + let result = ""; + for (const v of this) { + result += v; + } + + for (const item of other) { + for (const v of item) { + result += v; + } + } + + return result + count; + } +} + +class Vec { + x: number; + + constructor(x: number) { + this.x = x; + } +} + +// Allocate over whatever has just been freed, so a use-after-free reads something else. +function churn() { + for (let i = 0; i < 64; i++) { + let filler = new Vec(999); + } +} + +function viaExtension() { + const fruits = ["a", "b"]; + return fruits.joinedWith(["x", "y"], ["z"]); +} + +// a plain function's rest parameter, the elements made at the call +function lengths(...items: string[][]): int { + let n = 0; + for (const item of items) { + n += item.length; + } + + churn(); + for (const item of items) { + n += item.length; + } + + return n; +} + +function main() { + assert(viaExtension() == "abxyz5", "a rest parameter's array holds its elements through an extension call"); + assert(lengths(["p", "q"], ["r" + 1]) == 6, "a rest parameter's array holds elements made at the call"); + + print("done."); +} diff --git a/tslang/test/tester/tests/00string_length_resize.ts b/tslang/test/tester/tests/00string_length_resize.ts new file mode 100644 index 000000000..02c61b876 --- /dev/null +++ b/tslang/test/tester/tests/00string_length_resize.ts @@ -0,0 +1,114 @@ +// `s.length = n` resizes a string, and a string is a value: the resized one is a new string, and +// whoever held the old one still holds it. It used to reallocate the block in place, which frees +// the block when it moves - under every model but gc - while something else still held it. Under +// rc that was fatal whenever the string came in through a parameter: the default library's +// `"".clone().resize(n)`, behind padStart, padEnd, repeat, the regexp results and more, resized +// the caller's temporary through `this`, and the caller then released the freed block (heap +// corruption, 0xC0000374 on Windows). +// +// The block has to move for the bug to show, so every case grows its string well past the size +// it started at, and the heap is churned before anything is read back. + +namespace __String { + function copyOf(this: string): string { + return this + ""; + } + + // the default library's `resize` + function resized(this: string, newSize: index): string { + this.length = newSize + 1; + this[newSize] = 0; + return this; + } +} + +class Vec { + x: number; + + constructor(x: number) { + this.x = x; + } +} + +class Box { + text: string; + + constructor(text: string) { + this.text = text; + } +} + +// Allocate over whatever has just been freed, so a use-after-free reads something else. +function churn() { + for (let i = 0; i < 64; i++) { + let filler = new Vec(999); + } +} + +function repeated(c: string, n: int): string { + let s = ""; + for (let i = 0; i < n; i++) { + s += c; + } + + return s; +} + +// The default library's shape: the string is the caller's temporary, borrowed by `this`. +function filled(n: int, c: string): string { + const s = "ab".copyOf().resized(n); + for (let i = 0; i < n; i++) { + s[i] = c[0]; + } + + return s; +} + +function resizedThroughParameter() { + let last = ""; + for (let k = 0; k < 200; k++) { + last = filled(100 + k, "x"); + } + + churn(); + return last == repeated("x", 299); +} + +// A second variable holds the same string; resizing one of them leaves the other as it was. +function aliasKeepsItsString() { + let a = "ab" + 1; + let b = a; + b.length = 64; + b[0] = "z"[0]; + churn(); + + return a == "ab1" && b == "zb1"; +} + +// A field is resized; a local still holding what the field held keeps it. +function fieldResized() { + let box = new Box("cd" + 2); + let before = box.text; + box.text.length = 128; + box.text[1] = "q"[0]; + churn(); + + return before == "cd2" && box.text == "cq2"; +} + +// Smaller: the copy keeps only what fits, and `resized` writes the terminator. +function shrunk() { + let s = "abcdef".copyOf().resized(2); + churn(); + + return s == "ab"; +} + +function main() { + assert(resizedThroughParameter(), "a string resized through `this` leaves the caller's own alone"); + assert(aliasKeepsItsString(), "resizing a string leaves another variable's copy as it was"); + assert(fieldResized(), "resizing a field's string leaves a local's copy as it was"); + assert(shrunk(), "a string resized smaller keeps its start"); + + print("done."); +}