diff --git a/tslang/include/TypeScript/LowerToLLVM/LLVMCodeHelperBase.h b/tslang/include/TypeScript/LowerToLLVM/LLVMCodeHelperBase.h index 34cf68bf2..d02706ad4 100644 --- a/tslang/include/TypeScript/LowerToLLVM/LLVMCodeHelperBase.h +++ b/tslang/include/TypeScript/LowerToLLVM/LLVMCodeHelperBase.h @@ -208,6 +208,39 @@ class LLVMCodeHelperBase return _MemoryAlloc(sizeOfAlloc, zero); } + // A copy of `count` bytes into a block with `destSize` bytes left, which ends the process + // rather than write past the block. A Windows target calls the CRT's memcpy_s (its invalid + // parameter handler ends the process); glibc, Bionic and Darwin have no memcpy_s, and get the + // fortified __memcpy_chk instead ("buffer overflow detected"), which LLVM turns back into a + // plain memcpy where it can prove the copy fits. Wasm has neither: llvm.memcpy. + void MemoryCopy(mlir::Value dest, mlir::Value destSize, mlir::Value src, mlir::Value count) + { + auto loc = op->getLoc(); + if (compileOptions.isWasm) + { + rewriter.create(loc, dest, src, count, /*isVolatile=*/false); + return; + } + + TypeHelper th(rewriter); + auto llvmIndexType = count.getType(); + if (compileOptions.isWindows) + { + // errno_t memcpy_s(void *dest, size_t destSize, const void *src, size_t count) + auto memcpySFuncOp = getOrInsertFunction( + "memcpy_s", + th.getFunctionType(rewriter.getI32Type(), {th.getPtrType(), llvmIndexType, th.getPtrType(), llvmIndexType})); + rewriter.create(loc, memcpySFuncOp, ValueRange{dest, destSize, src, count}); + return; + } + + // void *__memcpy_chk(void *dest, const void *src, size_t count, size_t destSize) + auto memcpyChkFuncOp = getOrInsertFunction( + "__memcpy_chk", + th.getFunctionType(th.getPtrType(), {th.getPtrType(), th.getPtrType(), llvmIndexType, llvmIndexType})); + rewriter.create(loc, memcpyChkFuncOp, ValueRange{dest, src, count, destSize}); + } + mlir::Value MemoryAlloc(mlir::Type storageType, MemoryAllocSet zero = MemoryAllocSet::None) { TypeHelper th(rewriter); diff --git a/tslang/lib/TypeScript/LowerToLLVM.cpp b/tslang/lib/TypeScript/LowerToLLVM.cpp index beb8b1240..31ac76fdd 100644 --- a/tslang/lib/TypeScript/LowerToLLVM.cpp +++ b/tslang/lib/TypeScript/LowerToLLVM.cpp @@ -249,30 +249,38 @@ class ParseIntOpLowering : public TsLlvmPattern TypeHelper th(rewriter); LLVMCodeHelper ch(op, rewriter, getTypeConverter(), tsLlvmContext->compileOptions); - // A result wider than 32 bits (a bigint is an i64) needs the `long long` functions: `atoi` - // returns an int, and `strtol` a `long`, which is also 32 bits on Windows. - auto resultType = getTypeConverter()->convertType(op.getType()); - auto wide = resultType.isIntOrFloat() && resultType.getIntOrFloatBitWidth() > 32; - auto parsedType = wide ? rewriter.getI64Type() : rewriter.getI32Type(); + CodeLogicHelper clh(op, rewriter); - // Insert the `atoi` declaration if necessary. + auto loc = op->getLoc(); + + // Always `strtoll`, base 10 unless one is given: `long long` is 64 bits on every target, + // while `strtol` returns a `long`, 64 bits on Linux and 32 on Windows, and `atoi` leaves a + // value out of range undefined. strtoll saturates at the i64 range. auto i8PtrTy = th.getPtrType(); - LLVM::LLVMFuncOp parseIntFuncOp; - if (transformed.getBase()) - { - parseIntFuncOp = ch.getOrInsertFunction( - wide ? "strtoll" : "strtol", - th.getFunctionType(parsedType, {i8PtrTy, th.getPtrType(), rewriter.getI32Type()})); - auto nullOp = rewriter.create(op->getLoc(), th.getPtrType()); - rewriter.replaceOpWithNewOp(op, parseIntFuncOp, - ValueRange{transformed.getArg(), nullOp, transformed.getBase()}); - } - else + auto i64Type = rewriter.getI64Type(); + auto parseIntFuncOp = ch.getOrInsertFunction( + "strtoll", th.getFunctionType(i64Type, {i8PtrTy, th.getPtrType(), rewriter.getI32Type()})); + auto nullOp = rewriter.create(loc, th.getPtrType()); + mlir::Value base = transformed.getBase() ? transformed.getBase() : clh.createI32ConstantOf(10); + mlir::Value parsed = + rewriter.create(loc, parseIntFuncOp, ValueRange{transformed.getArg(), nullOp, base}).getResult(); + + // A result narrower than 64 bits saturates at its own range, as strtoll does at i64's + auto resultType = getTypeConverter()->convertType(op.getType()); + auto width = resultType.isIntOrFloat() ? resultType.getIntOrFloatBitWidth() : 64; + if (width < 64) { - parseIntFuncOp = ch.getOrInsertFunction(wide ? "atoll" : "atoi", th.getFunctionType(parsedType, {i8PtrTy})); - rewriter.replaceOpWithNewOp(op, parseIntFuncOp, ValueRange{transformed.getArg()}); + auto maxValue = rewriter.create( + loc, i64Type, rewriter.getI64IntegerAttr(llvm::APInt::getSignedMaxValue(width).getSExtValue())); + auto minValue = rewriter.create( + loc, i64Type, rewriter.getI64IntegerAttr(llvm::APInt::getSignedMinValue(width).getSExtValue())); + parsed = rewriter.create(loc, parsed, maxValue); + parsed = rewriter.create(loc, parsed, minValue); + parsed = rewriter.create(loc, resultType, parsed); } + rewriter.replaceOp(op, parsed); + return success(); } }; @@ -292,12 +300,15 @@ class ParseFloatOpLowering : public TsLlvmPattern auto loc = op->getLoc(); - // Insert the `atof` declaration if necessary. + // `strtod`, not `atof`: atof leaves a value out of range undefined, strtod gives an infinity + // or 0 auto i8PtrTy = th.getPtrType(); - auto parseFloatFuncOp = ch.getOrInsertFunction("atof", th.getFunctionType(rewriter.getF64Type(), {i8PtrTy})); + auto parseFloatFuncOp = ch.getOrInsertFunction( + "strtod", th.getFunctionType(rewriter.getF64Type(), {i8PtrTy, th.getPtrType()})); + auto nullOp = rewriter.create(loc, th.getPtrType()); - // atof gives a double whatever the result is: `"2.5"` wants a float - auto funcCall = rewriter.create(loc, parseFloatFuncOp, ValueRange{transformed.getArg()}); + // strtod gives a double whatever the result is: `"2.5"` wants a float + auto funcCall = rewriter.create(loc, parseFloatFuncOp, ValueRange{transformed.getArg(), nullOp}); auto resultType = getTypeConverter()->convertType(op.getType()); if (resultType == rewriter.getF64Type()) { @@ -781,7 +792,7 @@ class StringResizeOpLowering : public TsLlvmPattern auto fits = rewriter.create(loc, LLVM::ICmpPredicate::ult, sourceBytes, size); auto copyBytes = rewriter.create(loc, fits, sourceBytes, size); - rewriter.create(loc, newStringValue, source, copyBytes, /*isVolatile=*/false); + ch.MemoryCopy(newStringValue, size, source, copyBytes); rewriter.replaceOp(op, ValueRange{newStringValue}); return success(); @@ -865,7 +876,8 @@ class StringConcatOpLowering : public TsLlvmPattern for (auto [oper, length] : llvm::zip(opers, lengths)) { auto dest = rewriter.create(loc, i8PtrTy, th.getI8Type(), newStringValue, ValueRange{offset}); - rewriter.create(loc, dest, oper, length, /*isVolatile=*/false); + auto left = rewriter.create(loc, llvmIndexType, ValueRange{size, offset}); + ch.MemoryCopy(dest, left, oper, length); offset = rewriter.create(loc, llvmIndexType, ValueRange{offset, length}); } @@ -909,7 +921,7 @@ class StringCopyOpLowering : public TsLlvmPattern loc, llvmIndexType, ValueRange{bytes, rewriter.create(loc, llvmIndexType, rewriter.getIntegerAttr(llvmIndexType, 1))}); auto copy = ch.MemoryAlloc(bytes); - rewriter.create(loc, copy, source, bytes, /*isVolatile=*/false); + ch.MemoryCopy(copy, bytes, source, bytes); return copy; }, [&](OpBuilder &, Location) -> mlir::Value { return source; }); @@ -3650,7 +3662,7 @@ struct ArrayViewOpLowering : public TsLlvmPattern auto sizeOfTypeValue = rewriter.create(loc, llvmIndexType, sizeOfTypeValueMLIR); auto bytes = rewriter.create(loc, llvmIndexType, ValueRange{sizeOfTypeValue, count}); auto copy = ch.MemoryAlloc(bytes); - rewriter.create(loc, copy, arrayOffset, bytes, /*isVolatile=*/false); + ch.MemoryCopy(copy, bytes, arrayOffset, bytes); // the copy holds a second reference to each element, and its release gives them back { diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index cbcc095b5..e32b56e78 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -360,6 +360,7 @@ tslang_add_test(NAME test-compile-00-for-condition-narrowing COMMAND test-runner tslang_add_test(NAME test-compile-00-param-assigned-owned COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00param_assigned_owned.ts") tslang_add_test(NAME test-compile-00-string-empty-falsy COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00string_empty_falsy.ts") tslang_add_test(NAME test-compile-00-string-concat-bounds COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00string_concat_bounds.ts") +tslang_add_test(NAME test-compile-00-parse-number-range COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00parse_number_range.ts") tslang_add_test(NAME test-compile-00-narrowed-assign-other-member COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00narrowed_assign_other_member.ts") tslang_add_test(NAME test-compile-00-const-record-owned-fields COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00const_record_owned_fields.ts") tslang_add_test(NAME test-compile-00-array-length-valid COMMAND test-runner "${PROJECT_SOURCE_DIR}/test/tester/tests/00array_length_valid.ts") @@ -878,6 +879,7 @@ tslang_add_test(NAME test-jit-00-for-condition-narrowing COMMAND test-runner -ji tslang_add_test(NAME test-jit-00-param-assigned-owned COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00param_assigned_owned.ts") tslang_add_test(NAME test-jit-00-string-empty-falsy COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00string_empty_falsy.ts") tslang_add_test(NAME test-jit-00-string-concat-bounds COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00string_concat_bounds.ts") +tslang_add_test(NAME test-jit-00-parse-number-range COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00parse_number_range.ts") tslang_add_test(NAME test-jit-00-narrowed-assign-other-member COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00narrowed_assign_other_member.ts") tslang_add_test(NAME test-jit-00-const-record-owned-fields COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00const_record_owned_fields.ts") tslang_add_test(NAME test-jit-00-array-length-valid COMMAND test-runner -jit "${PROJECT_SOURCE_DIR}/test/tester/tests/00array_length_valid.ts") @@ -1725,6 +1727,7 @@ set(TSLANG_CORPUS 00param_assigned_owned.ts 00string_empty_falsy.ts 00string_concat_bounds.ts + 00parse_number_range.ts 00narrowed_assign_other_member.ts 00const_record_owned_fields.ts 00array_length_valid.ts diff --git a/tslang/test/tester/tests/00parse_number_range.ts b/tslang/test/tester/tests/00parse_number_range.ts new file mode 100644 index 000000000..1f2515ee3 --- /dev/null +++ b/tslang/test/tester/tests/00parse_number_range.ts @@ -0,0 +1,30 @@ +// parseInt, parseFloat and a cast from a string to a number parse with strtoll/strtod: atoi and +// atof leave a value out of range undefined (MSVC saturated, glibc wrapped), and strtol returns a +// `long`, 64 bits on Linux, which was read as 32. A 32-bit result saturates at the i32 range. +function main() { + assert(parseInt("42") == 42, "parseInt"); + assert(parseInt(" -17abc") == -17, "leading spaces, sign, trailing text"); + assert(parseInt("abc") == 0, "no digits"); + assert(parseInt("010") == 10, "decimal, not octal, without a radix"); + assert(parseInt("ff", 16) == 255, "radix 16"); + assert(parseInt("-101", 2) == -5, "radix 2"); + + assert(parseInt("2147483647") == 2147483647, "i32 max"); + assert(parseInt("-2147483648") == -2147483648, "i32 min"); + assert(parseInt("3000000000") == 2147483647, "above i32 saturates"); + assert(parseInt("-3000000000") == -2147483648, "below i32 saturates"); + assert(parseInt("99999999999999999999999") == 2147483647, "above i64 saturates"); + assert(parseInt("100000000", 16) == 2147483647, "radix 16 above i32 saturates"); + + const s = "123"; + assert(s == 123, "cast to i32"); + assert("9007199254740993" == 9007199254740993, "cast to i64"); + + assert(parseFloat("2.5e3") == 2500, "parseFloat"); + assert(parseFloat(" -0.25x") == -0.25, "leading spaces, trailing text"); + assert(parseFloat("1e400") > 1e308, "above the double range is infinite"); + assert(parseFloat("1e-400") == 0, "below the double range is 0"); + assert("2.5" == 2.5, "cast to f32"); + + print("done."); +} diff --git a/tslang/tslang/exe.cpp b/tslang/tslang/exe.cpp index e878762e3..f05f856d9 100644 --- a/tslang/tslang/exe.cpp +++ b/tslang/tslang/exe.cpp @@ -546,10 +546,21 @@ static llvm::Error writeAsInvokerManifestObj(llvm::COFF::MachineTypes machine, l } char *buf = res->getBufferStart(); + char *bufEnd = res->getBufferEnd(); memset(buf, 0, resSize); + // a copy bounded by what is left of the resource: memcpy_s is MSVC only + auto copyInto = [&](char *dest, const void *src, size_t count) { +#ifdef _WIN32 + memcpy_s(dest, static_cast(bufEnd - dest), src, count); +#else + assert(count <= static_cast(bufEnd - dest)); + memcpy(dest, src, count); +#endif + }; + // the file header: the magic, then a null entry - memcpy(buf, llvm::COFF::WinResMagic, sizeof(llvm::COFF::WinResMagic)); + copyInto(buf, llvm::COFF::WinResMagic, sizeof(llvm::COFF::WinResMagic)); buf += llvm::object::WIN_RES_MAGIC_SIZE + llvm::object::WIN_RES_NULL_ENTRY_SIZE; auto *prefix = reinterpret_cast(buf); @@ -570,7 +581,7 @@ static llvm::Error writeAsInvokerManifestObj(llvm::COFF::MachineTypes machine, l suffix->Characteristics = 0; buf += sizeof(llvm::object::WinResHeaderSuffix); - memcpy(buf, manifest, manifestSize); + copyInto(buf, manifest, manifestSize); auto resource = llvm::object::WindowsResource::createWindowsResource(res->getMemBufferRef()); if (!resource) diff --git a/tslang/tslang/jit.cpp b/tslang/tslang/jit.cpp index 15a0dbc35..9c60742ec 100644 --- a/tslang/tslang/jit.cpp +++ b/tslang/tslang/jit.cpp @@ -713,6 +713,10 @@ static int prepareJitProcess(CompileOptions &compileOptions) } addSym("memset", (void*)&memset); addSym("memcpy", (void*)&memcpy); +#ifdef _WIN32 + // the bounded copy a Windows target emits (LLVMCodeHelperBase::MemoryCopy) + addSym("memcpy_s", (void*)&memcpy_s); +#endif addSym("fflush", (void*)&fflush); // see jitAssertFailed above: bound to ucrtbase this is a modal message box, which an // unattended run cannot answer @@ -874,6 +878,9 @@ static std::unique_ptr createJit(llvm::orc::JITTargetMachineBu } addOverride("memset", (void *)&memset); addOverride("memcpy", (void *)&memcpy); +#ifdef _WIN32 + addOverride("memcpy_s", (void *)&memcpy_s); +#endif addOverride("fflush", (void *)&fflush); // see jitAssertFailed above: bound to ucrtbase this is a modal message box, which an // unattended run cannot answer