Commit 7b4fedd3 authored by Reilly Grant's avatar Reilly Grant Committed by Commit Bot

Check for detached ArrayBuffers in WebUSB transfer functions

This change adds checks to verify that the ArrayBuffers passed to WebUSB
functions controlTransferOut(), isochronousTransferOut(), and
transferOut() have not been detached. If so then an InvalidStateError
is thrown.

While the previous behavior was safe from use-after-frees it silently
sent the USB device an empty buffer which would be difficult to debug.

Change-Id: I94249dd0097991b2b4933093ac85f50f0ccda7c3
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1598048Reviewed-by: default avatarJeremy Roman <jbroman@chromium.org>
Commit-Queue: Reilly Grant <reillyg@chromium.org>
Cr-Commit-Position: refs/heads/master@{#657350}
parent 33ef35c1
...@@ -38,6 +38,7 @@ namespace blink { ...@@ -38,6 +38,7 @@ namespace blink {
namespace { namespace {
const char kDetachedBuffer[] = "The data buffer has been detached.";
const char kDeviceStateChangeInProgress[] = const char kDeviceStateChangeInProgress[] =
"An operation that changes the device state is in progress."; "An operation that changes the device state is in progress.";
const char kDeviceDisconnected[] = "The device was disconnected."; const char kDeviceDisconnected[] = "The device was disconnected.";
...@@ -91,19 +92,32 @@ String ConvertTransferStatus(const UsbTransferStatus& status) { ...@@ -91,19 +92,32 @@ String ConvertTransferStatus(const UsbTransferStatus& status) {
} }
} }
Vector<uint8_t> ConvertBufferSource( bool ConvertBufferSource(const ArrayBufferOrArrayBufferView& buffer_source,
const ArrayBufferOrArrayBufferView& buffer) { Vector<uint8_t>* vector,
DCHECK(!buffer.IsNull()); ScriptPromiseResolver* resolver) {
Vector<uint8_t> vector; DCHECK(!buffer_source.IsNull());
if (buffer.IsArrayBuffer()) { if (buffer_source.IsArrayBuffer()) {
vector.Append(static_cast<uint8_t*>(buffer.GetAsArrayBuffer()->Data()), ArrayBuffer* array_buffer = buffer_source.GetAsArrayBuffer()->Buffer();
buffer.GetAsArrayBuffer()->ByteLength()); if (array_buffer->IsNeutered()) {
resolver->Reject(DOMException::Create(
DOMExceptionCode::kInvalidStateError, kDetachedBuffer));
return false;
}
vector->Append(static_cast<uint8_t*>(array_buffer->Data()),
array_buffer->ByteLength());
} else { } else {
vector.Append(static_cast<uint8_t*>( ArrayBufferView* view = buffer_source.GetAsArrayBufferView().View()->View();
buffer.GetAsArrayBufferView().View()->BaseAddress()), if (!view->Buffer() || view->Buffer()->IsNeutered()) {
buffer.GetAsArrayBufferView().View()->byteLength()); resolver->Reject(DOMException::Create(
DOMExceptionCode::kInvalidStateError, kDetachedBuffer));
return false;
}
vector->Append(static_cast<uint8_t*>(view->BaseAddress()),
view->ByteLength());
} }
return vector; return true;
} }
} // namespace } // namespace
...@@ -367,18 +381,23 @@ ScriptPromise USBDevice::controlTransferOut( ...@@ -367,18 +381,23 @@ ScriptPromise USBDevice::controlTransferOut(
const ArrayBufferOrArrayBufferView& data) { const ArrayBufferOrArrayBufferView& data) {
auto* resolver = MakeGarbageCollected<ScriptPromiseResolver>(script_state); auto* resolver = MakeGarbageCollected<ScriptPromiseResolver>(script_state);
ScriptPromise promise = resolver->Promise(); ScriptPromise promise = resolver->Promise();
if (EnsureDeviceConfigured(resolver)) { if (!EnsureDeviceConfigured(resolver))
auto parameters = ConvertControlTransferParameters(setup, resolver); return promise;
if (parameters) {
Vector<uint8_t> buffer = ConvertBufferSource(data); auto parameters = ConvertControlTransferParameters(setup, resolver);
unsigned transfer_length = buffer.size(); if (!parameters)
device_requests_.insert(resolver); return promise;
device_->ControlTransferOut(
std::move(parameters), buffer, 0, Vector<uint8_t> buffer;
WTF::Bind(&USBDevice::AsyncControlTransferOut, WrapPersistent(this), if (!ConvertBufferSource(data, &buffer, resolver))
transfer_length, WrapPersistent(resolver))); return promise;
}
} unsigned transfer_length = buffer.size();
device_requests_.insert(resolver);
device_->ControlTransferOut(
std::move(parameters), buffer, 0,
WTF::Bind(&USBDevice::AsyncControlTransferOut, WrapPersistent(this),
transfer_length, WrapPersistent(resolver)));
return promise; return promise;
} }
...@@ -416,15 +435,19 @@ ScriptPromise USBDevice::transferOut(ScriptState* script_state, ...@@ -416,15 +435,19 @@ ScriptPromise USBDevice::transferOut(ScriptState* script_state,
const ArrayBufferOrArrayBufferView& data) { const ArrayBufferOrArrayBufferView& data) {
auto* resolver = MakeGarbageCollected<ScriptPromiseResolver>(script_state); auto* resolver = MakeGarbageCollected<ScriptPromiseResolver>(script_state);
ScriptPromise promise = resolver->Promise(); ScriptPromise promise = resolver->Promise();
if (EnsureEndpointAvailable(false /* out */, endpoint_number, resolver)) { if (!EnsureEndpointAvailable(false /* out */, endpoint_number, resolver))
Vector<uint8_t> buffer = ConvertBufferSource(data); return promise;
unsigned transfer_length = buffer.size();
device_requests_.insert(resolver); Vector<uint8_t> buffer;
device_->GenericTransferOut( if (!ConvertBufferSource(data, &buffer, resolver))
endpoint_number, buffer, 0, return promise;
WTF::Bind(&USBDevice::AsyncTransferOut, WrapPersistent(this),
transfer_length, WrapPersistent(resolver))); unsigned transfer_length = buffer.size();
} device_requests_.insert(resolver);
device_->GenericTransferOut(
endpoint_number, buffer, 0,
WTF::Bind(&USBDevice::AsyncTransferOut, WrapPersistent(this),
transfer_length, WrapPersistent(resolver)));
return promise; return promise;
} }
...@@ -451,13 +474,18 @@ ScriptPromise USBDevice::isochronousTransferOut( ...@@ -451,13 +474,18 @@ ScriptPromise USBDevice::isochronousTransferOut(
Vector<unsigned> packet_lengths) { Vector<unsigned> packet_lengths) {
auto* resolver = MakeGarbageCollected<ScriptPromiseResolver>(script_state); auto* resolver = MakeGarbageCollected<ScriptPromiseResolver>(script_state);
ScriptPromise promise = resolver->Promise(); ScriptPromise promise = resolver->Promise();
if (EnsureEndpointAvailable(false /* out */, endpoint_number, resolver)) { if (!EnsureEndpointAvailable(false /* out */, endpoint_number, resolver))
device_requests_.insert(resolver); return promise;
device_->IsochronousTransferOut(
endpoint_number, ConvertBufferSource(data), packet_lengths, 0, Vector<uint8_t> buffer;
WTF::Bind(&USBDevice::AsyncIsochronousTransferOut, WrapPersistent(this), if (!ConvertBufferSource(data, &buffer, resolver))
WrapPersistent(resolver))); return promise;
}
device_requests_.insert(resolver);
device_->IsochronousTransferOut(
endpoint_number, buffer, packet_lengths, 0,
WTF::Bind(&USBDevice::AsyncIsochronousTransferOut, WrapPersistent(this),
WrapPersistent(resolver)));
return promise; return promise;
} }
......
...@@ -34,6 +34,13 @@ function assertRejectsWithInterfaceStateChangeInProgressError(promise) { ...@@ -34,6 +34,13 @@ function assertRejectsWithInterfaceStateChangeInProgressError(promise) {
'An operation that changes interface state is in progress.'); 'An operation that changes interface state is in progress.');
} }
function detachBuffer(buffer) {
if (self.GLOBAL.isWindow())
window.postMessage('', '*', [buffer]);
else
self.postMessage('', [buffer]);
}
usb_test(() => { usb_test(() => {
return getFakeDevice().then(({ device, fakeDevice }) => { return getFakeDevice().then(({ device, fakeDevice }) => {
return waitForDisconnect(fakeDevice) return waitForDisconnect(fakeDevice)
...@@ -635,6 +642,47 @@ usb_test(() => { ...@@ -635,6 +642,47 @@ usb_test(() => {
}); });
}, 'requests to interfaces and endpoint require an interface claim'); }, 'requests to interfaces and endpoint require an interface claim');
usb_test(async () => {
const { device } = await getFakeDevice();
await device.open();
await device.selectConfiguration(1);
await device.claimInterface(0);
const transfer_params = {
requestType: 'vendor',
recipient: 'device',
request: 0,
value: 0,
index: 0
};
try {
const array_buffer = new ArrayBuffer(64 * 8);
const result =
await device.controlTransferOut(transfer_params, array_buffer);
assert_equals(result.status, 'ok');
detachBuffer(array_buffer);
await device.controlTransferOut(transfer_params, array_buffer);
assert_unreached();
} catch (e) {
assert_equals(e.code, DOMException.INVALID_STATE_ERR);
}
try {
const typed_array = new Uint8Array(64 * 8);
const result =
await device.controlTransferOut(transfer_params, typed_array);
assert_equals(result.status, 'ok');
detachBuffer(typed_array.buffer);
await device.controlTransferOut(transfer_params, typed_array);
assert_unreached();
} catch (e) {
assert_equals(e.code, DOMException.INVALID_STATE_ERR);
}
}, 'controlTransferOut rejects if called with a detached buffer');
usb_test(() => { usb_test(() => {
return getFakeDevice().then(({ device }) => { return getFakeDevice().then(({ device }) => {
return device.open() return device.open()
...@@ -763,6 +811,38 @@ usb_test(() => { ...@@ -763,6 +811,38 @@ usb_test(() => {
}); });
}, 'transferOut rejects if called on a disconnected device'); }, 'transferOut rejects if called on a disconnected device');
usb_test(async () => {
const { device } = await getFakeDevice();
await device.open();
await device.selectConfiguration(1);
await device.claimInterface(1);
try {
const array_buffer = new ArrayBuffer(64 * 8);
const result = await device.transferOut(2, array_buffer);
assert_equals(result.status, 'ok');
detachBuffer(array_buffer);
await device.transferOut(2, array_buffer);
assert_unreached();
} catch (e) {
assert_equals(e.code, DOMException.INVALID_STATE_ERR);
}
try {
const typed_array = new Uint8Array(64 * 8);
const result = await device.transferOut(2, typed_array);
assert_equals(result.status, 'ok');
detachBuffer(typed_array.buffer);
await device.transferOut(2, typed_array);
assert_unreached();
} catch (e) {
assert_equals(e.code, DOMException.INVALID_STATE_ERR);
}
}, 'transferOut rejects if called with a detached buffer');
usb_test(() => { usb_test(() => {
return getFakeDevice().then(({ device }) => { return getFakeDevice().then(({ device }) => {
return device.open() return device.open()
...@@ -854,6 +934,45 @@ usb_test(() => { ...@@ -854,6 +934,45 @@ usb_test(() => {
}); });
}, 'isochronousTransferOut rejects when called on a disconnected device'); }, 'isochronousTransferOut rejects when called on a disconnected device');
usb_test(async () => {
const { device } = await getFakeDevice();
await device.open();
await device.selectConfiguration(2);
await device.claimInterface(0);
await device.selectAlternateInterface(0, 1);
try {
const array_buffer = new ArrayBuffer(64 * 8);
const result = await device.isochronousTransferOut(
1, array_buffer, [64, 64, 64, 64, 64, 64, 64, 64]);
for (let i = 0; i < result.packets.length; ++i)
assert_equals(result.packets[i].status, 'ok');
detachBuffer(array_buffer);
await device.isochronousTransferOut(
1, array_buffer, [64, 64, 64, 64, 64, 64, 64, 64]);
assert_unreached();
} catch (e) {
assert_equals(e.code, DOMException.INVALID_STATE_ERR);
}
try {
const typed_array = new Uint8Array(64 * 8);
const result = await device.isochronousTransferOut(
1, typed_array, [64, 64, 64, 64, 64, 64, 64, 64]);
for (let i = 0; i < result.packets.length; ++i)
assert_equals(result.packets[i].status, 'ok');
detachBuffer(typed_array.buffer);
await device.isochronousTransferOut(
1, typed_array, [64, 64, 64, 64, 64, 64, 64, 64]);
assert_unreached();
} catch (e) {
assert_equals(e.code, DOMException.INVALID_STATE_ERR);
}
}, 'isochronousTransferOut rejects when called with a detached buffer');
usb_test(() => { usb_test(() => {
return getFakeDevice().then(({ device }) => { return getFakeDevice().then(({ device }) => {
return device.open().then(() => device.reset()).then(() => device.close()); return device.open().then(() => device.reset()).then(() => device.close());
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment