Compare commits

...

2 Commits

Author SHA1 Message Date
Claude Bot
760c32c4d4 fix(buffer): also check detachment after toWTFString/toStringOrNull in indexOf
Re-fetch the buffer pointer and length right before each use site
(indexOfNumber, indexOfString, indexOfBuffer) rather than once after
toNumber(). This covers additional JS execution points: the
encodingValue.toWTFString() and valueValue.toStringOrNull() calls could
also trigger detachment via toString() callbacks.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-12 04:52:54 +00:00
Claude Bot
f003f0dabf fix(buffer): prevent heap read-after-free in indexOf via valueOf-triggered detachment
The indexOf function (shared by Buffer.indexOf, Buffer.lastIndexOf, and
Buffer.includes) cached the raw typedVector pointer and byteLength before
calling toNumber() on the byteOffset argument. A user-supplied valueOf()
callback could call ArrayBuffer.prototype.transfer() to detach the
underlying ArrayBuffer, freeing the original memory. The stale pointer
was then used to scan freed memory.

Fix: defer fetching typedVector until after the toNumber() call and add
a detachment check. If the buffer was detached, throw a TypeError.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-12 04:38:32 +00:00
2 changed files with 158 additions and 1 deletions

View File

@@ -1489,7 +1489,6 @@ static int64_t indexOfBuffer(JSC::JSGlobalObject* lexicalGlobalObject, bool last
static int64_t indexOf(JSC::JSGlobalObject* lexicalGlobalObject, ThrowScope& scope, JSC::CallFrame* callFrame, typename IDLOperation<JSArrayBufferView>::ClassParameter buffer, bool last)
{
bool dir = !last;
const uint8_t* typedVector = buffer->typedVector();
size_t byteLength = buffer->byteLength();
std::optional<BufferEncodingType> encoding = std::nullopt;
double byteOffsetD = 0;
@@ -1505,17 +1504,40 @@ static int64_t indexOf(JSC::JSGlobalObject* lexicalGlobalObject, ThrowScope& sco
byteOffsetValue = jsUndefined();
byteOffsetD = 0;
} else {
// toNumber() can trigger JavaScript execution (valueOf/Symbol.toPrimitive),
// which could detach the underlying ArrayBuffer. We must re-fetch the
// pointer and length after this call.
byteOffsetD = byteOffsetValue.toNumber(lexicalGlobalObject);
RETURN_IF_EXCEPTION(scope, -1);
if (byteOffsetD > 0x7fffffffp0f) byteOffsetD = 0x7fffffffp0f;
if (byteOffsetD < -0x80000000p0f) byteOffsetD = -0x80000000p0f;
}
// After any call that can trigger JS execution (toNumber, toWTFString,
// toStringOrNull), the buffer may have been detached. We must re-fetch
// typedVector and byteLength from the buffer right before use, and check
// for detachment after all JS calls in each code path are complete.
// Helper: re-fetch buffer state after JS calls, checking for detachment.
// Returns false if the buffer was detached (after throwing a TypeError).
auto refetchBufferState = [&](const uint8_t*& typedVector, size_t& len) -> bool {
if (buffer->isDetached()) [[unlikely]] {
throwVMTypeError(lexicalGlobalObject, scope, "Buffer is detached"_s);
return false;
}
typedVector = buffer->typedVector();
len = buffer->byteLength();
return true;
};
if (std::isnan(byteOffsetD)) byteOffsetD = dir ? 0 : byteLength;
if (valueValue.isNumber()) {
auto byteValue = static_cast<uint8_t>((valueValue.toInt32(lexicalGlobalObject)) % 256);
RETURN_IF_EXCEPTION(scope, -1);
const uint8_t* typedVector;
if (!refetchBufferState(typedVector, byteLength)) return -1;
if (byteLength == 0) return -1;
return indexOfNumber(lexicalGlobalObject, last, typedVector, byteLength, byteOffsetD, byteValue);
}
@@ -1534,11 +1556,17 @@ static int64_t indexOf(JSC::JSGlobalObject* lexicalGlobalObject, ThrowScope& sco
}
auto* str = valueValue.toStringOrNull(lexicalGlobalObject);
RETURN_IF_EXCEPTION(scope, -1);
const uint8_t* typedVector;
if (!refetchBufferState(typedVector, byteLength)) return -1;
if (byteLength == 0) return -1;
return indexOfString(lexicalGlobalObject, last, typedVector, byteLength, byteOffsetD, str, encoding.value());
}
if (auto* array = JSC::jsDynamicCast<JSC::JSUint8Array*>(valueValue)) {
if (!encoding.has_value()) encoding = BufferEncodingType::utf8;
const uint8_t* typedVector;
if (!refetchBufferState(typedVector, byteLength)) return -1;
if (byteLength == 0) return -1;
return indexOfBuffer(lexicalGlobalObject, last, typedVector, byteLength, byteOffsetD, array, encoding.value());
}

View File

@@ -0,0 +1,129 @@
import { describe, expect, test } from "bun:test";
describe("Buffer.indexOf/lastIndexOf/includes with detached buffer via valueOf", () => {
test("indexOf throws TypeError when buffer is detached via valueOf on byteOffset", () => {
const ab = new ArrayBuffer(64);
const buf = Buffer.from(ab);
buf.fill(0x42);
expect(() => {
buf.indexOf(0x42, {
valueOf() {
ab.transfer(2048);
return 0;
},
} as any);
}).toThrow(TypeError);
});
test("lastIndexOf throws TypeError when buffer is detached via valueOf on byteOffset", () => {
const ab = new ArrayBuffer(64);
const buf = Buffer.from(ab);
buf.fill(0x42);
expect(() => {
buf.lastIndexOf(0x42, {
valueOf() {
ab.transfer(2048);
return 0;
},
} as any);
}).toThrow(TypeError);
});
test("includes throws TypeError when buffer is detached via valueOf on byteOffset", () => {
const ab = new ArrayBuffer(64);
const buf = Buffer.from(ab);
buf.fill(0x42);
expect(() => {
buf.includes(0x42, {
valueOf() {
ab.transfer(2048);
return 0;
},
} as any);
}).toThrow(TypeError);
});
test("indexOf with string value throws TypeError when buffer is detached via valueOf on byteOffset", () => {
const ab = new ArrayBuffer(64);
const buf = Buffer.from(ab);
buf.fill(0x41); // 'A'
expect(() => {
buf.indexOf("A", {
valueOf() {
ab.transfer(2048);
return 0;
},
} as any);
}).toThrow(TypeError);
});
test("indexOf with Buffer value throws TypeError when buffer is detached via valueOf on byteOffset", () => {
const ab = new ArrayBuffer(64);
const buf = Buffer.from(ab);
buf.fill(0x42);
const needle = Buffer.from([0x42]);
expect(() => {
buf.indexOf(needle, {
valueOf() {
ab.transfer(2048);
return 0;
},
} as any);
}).toThrow(TypeError);
});
test("indexOf with string value throws TypeError when buffer is detached via encoding toString", () => {
const ab = new ArrayBuffer(64);
const buf = Buffer.from(ab);
buf.fill(0x41); // 'A'
expect(() => {
buf.indexOf("A", 0, {
toString() {
ab.transfer(2048);
return "utf8";
},
} as any);
}).toThrow(TypeError);
});
test("indexOf with Buffer value throws TypeError when buffer is detached via encoding toString", () => {
const ab = new ArrayBuffer(64);
const buf = Buffer.from(ab);
buf.fill(0x42);
const needle = Buffer.from([0x42]);
expect(() => {
buf.indexOf(needle, 0, {
toString() {
ab.transfer(2048);
return "utf8";
},
} as any);
}).toThrow(TypeError);
});
test("indexOf still works correctly when buffer is not detached", () => {
const buf = Buffer.from([1, 2, 3, 4, 5]);
expect(buf.indexOf(3)).toBe(2);
expect(buf.indexOf(3, 3)).toBe(-1);
expect(buf.lastIndexOf(3)).toBe(2);
expect(buf.includes(3)).toBe(true);
expect(buf.includes(6)).toBe(false);
});
test("indexOf with valueOf that does not detach still works correctly", () => {
const buf = Buffer.from([1, 2, 3, 4, 5]);
const result = buf.indexOf(3, {
valueOf() {
return 0;
},
} as any);
expect(result).toBe(2);
});
});