Crash in reads with short timeout - race.

Peter Dons Tychsen via Libusb-win32-devel <[email protected]> Sun, 01 Apr 2018 18:32:55 +0200
Newsgroups gmane.comp.lib.libusb.devel.windows
Organization DonTech ApS
Message-ID <[email protected]>
Hi libusb,

If anyone could review and eventually apply the fix below it would be
great, as i am probably not the only only getting this rare crash.

I still use the old libusb-win32 (still works great by the way) on some
of my older projects, and i have had an annoying crash bug which i
never got fixed.

I decided to use some time finding the "easter egg" that was causing
this. After lots of debugging, it turned out that there is a race in
the DLL.

When calling e.g. usb_interrupt_read() that times out it will call:

1) usb_interrupt_read
2) _usb_transfer_sync
3) usb_reap_async
4) _usb_reap_async
5) _usb_cancel_io

This is where it goes south. To cancel the I/O operation it calls:

_usb_abort_ep() to send the LIBUSB_IOCTL_ABORT_ENDPOINT to the driver
synchroniously. Even though the LIBUSB_IOCTL_ABORT_ENDPOINT is sent
synchroniously to the driver, the driver just forwards the request to
"usbd" further down the IRP chain as a "reset endpoint" request, which
has asynchrinous elements to it. So all-in-all you cannot regard the
request as cancelled when returning from _usb_abort_ep().

To fix this it was introduced that using WaitForSingleObject() would be
used to wait for the cancel to go through.

The first revision that had this was:

https://github.com/mcuee/libusb-win32/commit/6eaec78aed8e34e1aaf2e835b7
01497d352bc3e6#diff-b5691da501465b646e19ae693a116e5e

It was later reformatted into being a part of _usb_cancel_io().

However, WaitForSingleObject() is not called correctly. If you want to
be sure that the cancel has been executed, you must call
WaitForSingleObject() with a timeout that is large enough to ensure
that the request is processed. In many cases on a loaded system, this
ends tragically.

A simple statement can crash, e.g.:

-- snip --

void test(void)
{
  uint8_t data[20];
  usb_interrupt_read(handle, ep_int_in, data, 20, 1);
}

-- snip --

The above code crashes in some cases, as the read is in some rare cases
executed *after* leaving usb_interrupt_read() and test(), which then
corrupts the local stack as "data" is not valid any more.

To fix this, simply use INFINITE as timeout, as you can *not* return
from _usb_cancel_io until the operation has been truely cancelled. It
is also described on MSDN for WaitForSingleObject() that the only was
to be sure is to call it with INFINITE.

I have tested the fix on lots of setups, and i think the fix is OK, and
does not seem to create other new side-effects.

It was also partially fixable by cancelling all I/O to the driver using
CancelIo(). However, this will cancel all I/O and will ruin other
running endpoints (async readers and such), so that will not work.
Newer version of windows have CancelIoEx() which can cancel a specific
I/O operation, but this is only available in 2008/win7 and forward, and
i need this to still run on xp/vista.

So this solution is the most compatible.

I have attached a patch.

Comments are welcome.

Thanks,

/pedro

------------------------------------------------------------------------------
Check out the vibrant tech community on one of the world's most
engaging tech sites, Slashdot.org! http://sdm.link/slashdot

_______________________________________________
Libusb-win32-devel mailing list
[email protected]
https://lists.sourceforge.net/lists/listinfo/libusb-win32-devel
cancel_io_wait.patch (text/x-patch, 414 B)
Index: libusb/src/windows.c
===================================================================
--- libusb/src/windows.c	(revision 413)
+++ libusb/src/windows.c	(working copy)
@@ -1156,7 +1156,7 @@
 {
     int ret;
     ret = _usb_abort_ep(context->dev, context->req.endpoint.endpoint);
-    WaitForSingleObject(context->ol.hEvent, 0);
+    WaitForSingleObject(context->ol.hEvent, INFINITE);
     return ret;
 }