Skip to content

Commit 06edf90

Browse files
author
Greg Domzalski
committed
Fix fundamental misunderstanding of how HID I/O reports work on macOS
This addresses a bug where if a YubiKey command would result in multiple input reports being generated, we would get the callbacks but drop the results on the floor. This would generally result in the runloop timing out because we already had received the report via the callback but had no way of caching it.
1 parent 5a1645f commit 06edf90

File tree

2 files changed

+66
-49
lines changed

2 files changed

+66
-49
lines changed

Yubico.Core/src/Yubico/Core/Devices/Hid/MacOSHidIOReportConnection.cs

Lines changed: 65 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@
1313
// limitations under the License.
1414

1515
using System;
16+
using System.Collections.Concurrent;
1617
using System.Globalization;
17-
using System.Linq;
1818
using System.Runtime.InteropServices;
1919
using System.Text;
2020
using Yubico.Core.Buffers;
@@ -35,8 +35,12 @@ internal sealed class MacOSHidIOReportConnection : IHidConnection
3535
private readonly MacOSHidDevice _device;
3636
private readonly IntPtr _loopId;
3737
private readonly Logger _log = Log.GetLogger();
38+
3839
private readonly byte[] _readBuffer;
39-
private readonly GCHandle _readHandle;
40+
private GCHandle _readHandle;
41+
42+
private readonly ConcurrentQueue<byte[]> _reportsQueue;
43+
private GCHandle _pinnedReportsQueue;
4044

4145
/// <summary>
4246
/// The correct size, in bytes, for the data buffer to be transmitted to the device.
@@ -67,9 +71,15 @@ public MacOSHidIOReportConnection(MacOSHidDevice device, long entryId)
6771
byte[] cstr = Encoding.UTF8.GetBytes($"fido2-loopid-{entryId}");
6872
_loopId = CFStringCreateWithCString(IntPtr.Zero, cstr, 0);
6973

74+
// The following buffer must be pinned because the native function must retain a pointer (i.e. the address)
7075
_readBuffer = new byte[64];
7176
_readHandle = GCHandle.Alloc(_readBuffer, GCHandleType.Pinned);
7277

78+
// This object is marshalled using a normal handle since .NET is on the other side and is capable of using
79+
// the GCHandle (i.e. we do not need the address)
80+
_reportsQueue = new ConcurrentQueue<byte[]>();
81+
_pinnedReportsQueue = GCHandle.Alloc(_reportsQueue);
82+
7383
SetupConnection();
7484

7585
InputReportSize = IOKitHelpers.GetIntPropertyValue(_deviceHandle, IOKitHidConstants.MaxInputReportSize);
@@ -124,7 +134,7 @@ private void SetupConnection()
124134
_readBuffer,
125135
_readBuffer.Length,
126136
reportCallback,
127-
GCHandle.ToIntPtr(_readHandle));
137+
GCHandle.ToIntPtr(_pinnedReportsQueue));
128138

129139
IntPtr callback = Marshal.GetFunctionPointerForDelegate<IOHIDCallback>(RemovalCallback);
130140
IOHIDDeviceRegisterRemovalCallback(_deviceHandle, callback, _deviceHandle);
@@ -150,49 +160,50 @@ private void SetupConnection()
150160
/// </exception>
151161
public byte[] GetReport()
152162
{
153-
try
163+
if (_reportsQueue.TryDequeue(out byte[] report))
154164
{
155-
IntPtr runLoop = CFRunLoopGetCurrent();
165+
// If there's already a report in the queue (i.e. the callback beat us to calling GetReport) return
166+
// that one immediately.
167+
_log.SensitiveLogInformation(
168+
"GetReport returned buffer: {Report}",
169+
Hex.BytesToHex(report));
156170

157-
IOHIDDeviceScheduleWithRunLoop(_deviceHandle, runLoop, _loopId);
171+
return report;
172+
}
158173

159-
// The YubiKey has a reclaim timeout of 3 seconds. This can cause the SDK some trouble if we just
160-
// switched out of a different USB interface (like Keyboard or CCID). We previously used a fairly
161-
// tight timeout of 4 seconds, but that seemed to not always work. 6 seconds (double the timeout)
162-
// seems like a more reasonable timeout for the operating system.
163-
int runLoopResult = CFRunLoopRunInMode(_loopId, 6, true);
174+
// Otherwise start up the IO runloop and see if we find more reports to pick up.
175+
IntPtr runLoop = CFRunLoopGetCurrent();
164176

165-
_device.LogDeviceAccessTime();
177+
IOHIDDeviceScheduleWithRunLoop(_deviceHandle, runLoop, _loopId);
166178

167-
if (runLoopResult != kCFRunLoopRunHandledSource)
168-
{
169-
throw new PlatformApiException(
170-
string.Format(
171-
CultureInfo.CurrentCulture,
172-
ExceptionMessages.WrongIOKitRunLoopMode,
173-
runLoopResult));
174-
}
179+
// The YubiKey has a reclaim timeout of 3 seconds. This can cause the SDK some trouble if we just
180+
// switched out of a different USB interface (like Keyboard or CCID). We previously used a fairly
181+
// tight timeout of 4 seconds, but that seemed to not always work. 6 seconds (double the timeout)
182+
// seems like a more reasonable timeout for the operating system.
183+
int runLoopResult = CFRunLoopRunInMode(_loopId, 6, true);
175184

176-
IOHIDDeviceUnscheduleFromRunLoop(_deviceHandle, runLoop, _loopId);
177-
178-
_log.SensitiveLogInformation(
179-
"GetReport returned buffer: {Report}",
180-
Hex.BytesToHex(_readBuffer));
185+
_device.LogDeviceAccessTime();
181186

182-
// Return a copy of the report
183-
return _readBuffer.ToArray();
184-
}
185-
finally
187+
if (runLoopResult != kCFRunLoopRunHandledSource)
186188
{
187-
IOHIDDeviceRegisterInputReportCallback(
188-
_deviceHandle,
189-
_readBuffer,
190-
_readBuffer.Length,
191-
IntPtr.Zero,
192-
IntPtr.Zero);
193-
194-
IOHIDDeviceRegisterRemovalCallback(_deviceHandle, IntPtr.Zero, IntPtr.Zero);
189+
throw new PlatformApiException(
190+
string.Format(
191+
CultureInfo.CurrentCulture,
192+
ExceptionMessages.WrongIOKitRunLoopMode,
193+
runLoopResult));
195194
}
195+
196+
IOHIDDeviceUnscheduleFromRunLoop(_deviceHandle, runLoop, _loopId);
197+
198+
// We should be guaranteed to have a report here - otherwise the runloop would have timed out
199+
// and the PlatformApiException above would have been thrown.
200+
_ = _reportsQueue.TryDequeue(out report);
201+
202+
_log.SensitiveLogInformation(
203+
"GetReport returned buffer: {Report}",
204+
Hex.BytesToHex(report));
205+
206+
return report;
196207
}
197208

198209
/// <summary>
@@ -232,7 +243,7 @@ private static void ReportCallback(
232243

233244
log.LogInformation("MacOSHidIOReportConnection.ReportCallback has been called.");
234245

235-
if (result != 0 || type != IOKitHidConstants.kIOHidReportTypeOutput || reportId != 0 || reportLength < 0)
246+
if (result != 0 || type != IOKitHidConstants.kIOHidReportTypeInput || reportId != 0 || reportLength < 0)
236247
{
237248
// Something went wrong. We don't currently signal, just continue.
238249
log.LogWarning(
@@ -242,19 +253,10 @@ private static void ReportCallback(
242253
type,
243254
reportId,
244255
reportLength);
245-
246-
return;
247256
}
248257

249-
byte[] buffer = (byte[])GCHandle.FromIntPtr(context).Target;
250-
long length = Math.Min(buffer.Length, reportLength);
251-
log.LogInformation(
252-
"Buffer length determined to be {Length} bytes. (buffer.Length was {BufferLength}, and reportLength was {ReportLength}",
253-
length,
254-
buffer.Length,
255-
reportLength);
256-
257-
Array.Copy(report, buffer, length);
258+
var reportsQueue = (ConcurrentQueue<byte[]>)GCHandle.FromIntPtr(context).Target;
259+
reportsQueue.Enqueue(report);
258260
}
259261

260262
/// <summary>
@@ -324,11 +326,25 @@ private void Dispose(bool disposing)
324326
// Dispose managed state here
325327
}
326328

329+
IOHIDDeviceRegisterInputReportCallback(
330+
_deviceHandle,
331+
_readBuffer,
332+
_readBuffer.Length,
333+
IntPtr.Zero,
334+
IntPtr.Zero);
335+
336+
IOHIDDeviceRegisterRemovalCallback(_deviceHandle, IntPtr.Zero, IntPtr.Zero);
337+
327338
if (_readHandle.IsAllocated)
328339
{
329340
_readHandle.Free();
330341
}
331342

343+
if (_pinnedReportsQueue.IsAllocated)
344+
{
345+
_pinnedReportsQueue.Free();
346+
}
347+
332348
if (_deviceHandle != IntPtr.Zero)
333349
{
334350
_ = IOHIDDeviceClose(_deviceHandle, 0);

Yubico.Core/src/Yubico/PlatformInterop/macOS/IOKitFramework/IOKitHidConstants.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ internal static class IOKitHidConstants
2424
public const string MaxInputReportSize = "MaxInputReportSize";
2525
public const string MaxOutputReportSize = "MaxOutputReportSize";
2626

27+
public const int kIOHidReportTypeInput = 0;
2728
public const int kIOHidReportTypeOutput = 1;
2829
public const int kIOHidReportTypeFeature = 2;
2930
}

0 commit comments

Comments
 (0)