39 lines
1.7 KiB
TypeScript
39 lines
1.7 KiB
TypeScript
import { describe, expect, test } from "bun:test";
|
|
import { isASAN, rss } from "harness";
|
|
|
|
// JSBuffer__bufferFromPointerAndLengthAndDeinit used to skip the deallocator entirely
|
|
// when the resulting length was 0. Buffer.from(<string with no valid hex>, "hex") would
|
|
// allocate len/2 bytes, decode zero bytes, and hand a zero-length slice back to C++ —
|
|
// which then dropped the allocation on the floor.
|
|
|
|
describe("Buffer.from(string, encoding) does not leak when decoded length is 0", () => {
|
|
test("hex with no valid hex digits returns an empty Buffer", () => {
|
|
expect(Buffer.from("zz", "hex")).toEqual(Buffer.alloc(0));
|
|
expect(Buffer.from("z", "hex")).toEqual(Buffer.alloc(0));
|
|
expect(Buffer.from(" ", "base64")).toEqual(Buffer.alloc(0));
|
|
});
|
|
|
|
test("hex with no valid hex digits does not leak the staging allocation", () => {
|
|
// 8192 input chars -> 4096-byte staging allocation which is leaked per call
|
|
// without the fix.
|
|
const str = Buffer.alloc(8192, "z").toString();
|
|
const iterations = 50_000;
|
|
|
|
// warm up so any one-time allocations (JIT tiers, caches) don't count
|
|
for (let i = 0; i < 1000; i++) Buffer.from(str, "hex");
|
|
Bun.gc(true);
|
|
const before = rss();
|
|
|
|
for (let i = 0; i < iterations; i++) Buffer.from(str, "hex");
|
|
Bun.gc(true);
|
|
const after = rss();
|
|
|
|
const growthMB = (after - before) / 1024 / 1024;
|
|
// Without the fix this grows by ~200 MB (50k * 4096 bytes). With the fix,
|
|
// growth is noise. Under ASAN the system allocator's quarantine (default
|
|
// 256 MB) plus glibc never returning freed pages means RSS climbs even
|
|
// when nothing leaks; LSan still catches real leaks separately.
|
|
expect(growthMB).toBeLessThan(isASAN ? 400 : 40);
|
|
});
|
|
});
|