Windows System Software -- Consulting, Training, Development -- Unique Expertise, Guaranteed Results
The free OSR Learning Library has more than 50 articles on a wide variety of topics about writing and debugging device drivers and Minifilters. From introductory level to advanced. All the articles have been recently reviewed and updated, and are written using the clear and definitive style you've come to expect from OSR over the years.
Check out The OSR Learning Library at: https://www.osr.com/osr-learning-library/
Context: I'm a Windows kernel driver newbie trying to use KMDF to build a filter driver, specifically to intercept a specific IOCTL_KS_PROPERTY
request on an audio driver stack. My understanding is that KS involves lots of METHOD_NEITHER
IOCTLs (including the one I'm interested in), hence this question.
According to MSDN, handling "neither" I/O in KMDF is somewhat subtle. The documentation describes in detail how to do it assuming that my driver wants to handle the request itself. But what if I simply want to forward it down to the next driver down the stack? The documentation doesn't mention that case.
I looked at various samples, including the toaster filter sample and OSR's very own GenFilter sample. In both cases, IoDeviceControl
requests are simply forwarded in the usual KMDF way with no special consideration given to potential METHOD_NEITHER
requests.
I don't understand how that approach could possibly be correct. EvtIoDeviceControl
runs in arbitrary thread context. By the time that request handler is called, the original thread context might already be gone, and attempts to forward the request as-is to the next driver down the stack might just result in that driver accessing an invalid buffer.
This brings me to the conclusion that any filter driver that implements EvtIoDeviceControl
and could potentially be confronted with METHOD_NEITHER
IOCTLs (including ones it might not know exist!) has to implement an EvtIoInCallerContext
callback and go through the whole process of locking the user buffer. (As a side note I'm not sure I understand why KMDF can't do that automatically…) One example of that process is the NONPNP
sample, but note that is not a filter driver.
So let's say I implement EvtIoInCallerContext
and go through this whole dance. Then I want to forward the request to the next-lower driver. But wait, I can't simply use WDF_REQUEST_SEND_OPTION_SEND_AND_FORGET
or WdfRequestFormatRequestUsingCurrentType
and call it a day, can I? I would still be forwarding the original user buffer which might not be usable. So… what now? Do I swap the buffer before forwarding the request, so that the I/O target gets my buffer obtained from WdfMemoryGetBuffer()
?
Given this rabbit hole I find myself into and my complete lack of experience, I am starting to doubt myself. Am I overthinking all this and trying to solve a problem that doesn't exist? Am I missing something here?
Upcoming OSR Seminars | ||
---|---|---|
OSR has suspended in-person seminars due to the Covid-19 outbreak. But, don't miss your training! Attend via the internet instead! | ||
Internals & Software Drivers | 19-23 June 2023 | Live, Online |
Writing WDF Drivers | 10-14 July 2023 | Live, Online |
Kernel Debugging | 16-20 October 2023 | Live, Online |
Developing Minifilters | 13-17 November 2023 | Live, Online |
Comments
I will get spanked for saying this, but
EvtIoDeviceControl
only runs in a system thread context if you have enabledSyncronizationScope
on your queue or if you setExecutionLevel
. If you setSynchronizationScope
toWdfSynchronizationScopeNone
, then your handler will run in the caller context and you can fire-and-forget. It would be unfriendly for filter drivers to alter the context so much.Filtering the KS ioctls is complicated. The rules are not documented, and because of that they actually DO change from time to time. Making it even more complicated, there is a filter driver above you called "ksthunk.sys" that actually does map some of the user addresses into kernel mode, so your IRPs might already be converted.
For your own processing, the input buffer is always in
UserBuffer
. You can useWdfRequestRetrieveUnsafeUserInputBuffer
to fetch that. If that is a kernel address (meaning the pointer is < 0), then someone has done the mapping work for you. Otherwise, you need to callWdfRequestProbeAndLockUserBufferForRead
. I save the resulting pointer in a request context to fetch later.If the output buffer has already been converted, it will be in the
SystemBuffer
field (WdfRequestWdfGetIrp(Request)->AssociatedIrp.SystemBuffer
). If that is non-zero, you can use it. Otherwise, you need to callWdfRequestRetrieveUnsafeUserOutputBuffer
and callWdfRequestProbeAndLockUserBufferForWrite
on it.The input buffer contains the "property descriptor", usually a
KSPROPERTY
structure orKSP_PIN
structure. The output buffer contains the "property value type".Tim Roberts, [email protected]
Providenza & Boekelheide, Inc.
Both WDF_REQUEST_SEND_OPTION_SEND_AND_FORGET or WdfRequestFormatRequestUsingCurrentType from EvtIoInCallerContext will send the request down the stack in the same context (the user mode process). You mention buffer swapping, is that a feature you want the filter to implement or as a way to compensate for context problems you are anticipating?
If you can describe the higher level functionality and problem you are trying to solve in the filter, you will get better, more concrete guidance. For instance, EvtWdfDeviceWdmIrpPreprocess is also called in the sender's context and you can accomplish the same inspect + fire & forget from this callback without the WDFREQUEST abstraction. But without understanding what you are trying to do, it is hard to recommend one over the other.
Stopping the queue (manually or for a power managed queue, during a device power state transition) will also change the context of EvtIoDeviceControl, even for a ScopeNone WDFQUEUE.
(Sorry if this ends up being a double post. I previously posted a response, and then tried to edit it, but that made my post disappear with a warning that it would be "queued for moderation" (?!). I can't figure out what happened to my original post so I'm reposting just in case.)
Thanks, I expected as much. Seems like this would be relying on undocumented behaviour, but I guess one could easily invoke Hyrum's law given the number of KMDF filter drivers that might already be relying (perhaps unknowingly) on being able to transparently forward any IOCTL, including
METHOD_NEITHER
, from withinEvtIoDeviceControl
.Thanks, I was going to tackle that part next. I appreciate the warning about getting both kinds of buffers - I definitely would not have expected that!
Oh, nice! I didn't know that was a possibility. That would nicely take care of the problem. I can't find any documentation that says I can forward a request to the local I/O target from within
EvtIoInCallerContext
, but I can't find any documentation that says I can't, either… I guess I was worried that I might confuse the framework by neither completing the request nor enqueuing it before returning from the callback.By the way, it looks like most KMDF filter drivers would benefit from using this approach, because most filter drivers (presumably) want to transparently and generically forward any IOCTL that it does not understand. Since the filter driver doesn't know what kind of IOCTL it might be called upon to forward, it has to assume the worst case, i.e.
METHOD_NEITHER
, and therefore forward any unknown IOCTLs inEvtIoInCallerContext
, notEvtIoDeviceControl
. If that conclusion is correct, then there's a number of sample KMDF filter drivers out there that need updating, and possibly a number of real drivers too…The latter. I thought I would be forced to do it, but from this discussion it looks like that won't actually be necessary.
The business logic I want to implement in my filter driver is extremely trivial. I only want to listen for a very specific kind of IOCTL (
IOCTL_KS_PROPERTY
), and if I get one, register a completion routine that makes a small change to the contents of the output buffer before it's returned to the application. That's it. All other requests should be forwarded transparently, fire-and-forget style.Thanks. I doubt that will be necessary - looks like I can achieve my goals without dealing with WDM directly. I'll keep that option in mind though in case I'm mistaken.
My understanding is that filter drivers only use non-power-managed queues. So, assuming that I never stop the queue,
EvtIoDeviceControl
should run in the original thread context, right? Though obviously your proposed approach of usingEvtIoInCallerContext
still seems cleaner since it would make that a strong guarantee instead of relying on undocumented behaviour.You've gotten some excellent advice already. Let's see if I can fill things out a bit more.
Ah! Well, what you may not realize is that except for the file system stack, METHOD_NEITHER IOCTLs are pretty rare. This is because of the need to handle them in the context of the requesting process, the mechanics that are required to properly validate and access the buffers, and the need to "capture" the buffer contents if the buffer contains anything other than pure data (to prevent TOCU issues). When you couple this with the speed at which modern processor implement block move operations, most things that you would use METHOD_NEITHER for are better implemented at METHOD_BUFFERED or METHOD_DIRECT.
Assuming that your EvtIoDeviceControl Event Processing Callback will run in the context of the requesting process relies on an artifact of implementation. Given that there has always been an architecturally specified way to have your driver called in the context of the requesting process, I would say that relying on the implementation detail (assuming EvtIoDeviceControl is sometimes called in the current process' context) is a serious bug and I would strongly advise you not to do it.
I would hope that SDV would catch this and flag it.
Like I said above, in the real world, this really isn't an issue. There's little use of METHOD_NEITHER. And one doesn't generally write "generic" filter drivers, or filter drivers for branches of the device tree that one does not understand.
When I've seen METHOD_NEITHER used (in 3rd party AND in in-box MSFT developed drivers) it is generally sign of a bad design, or an outright error. We once did a project where we closed almost 100 high-priority security issues that were caused by gratuitous use of METHOD_NEITHER. Fortunately, most of these bad uses have been purged and in the 21st century folks don't use METHOD_NEITHER when something else will do.
Having said that, nothing I've written should be construed to imply that using METHOD_NEITHER is always bad. It's not. But in practical terms, its use in a device driver almost always is.
Peter
Peter Viscarola
OSR
@OSRDrivers
Remember that you get literally thousands of
IOCTL_KS_PROPERTY
requests when a KS filter starts up. You're going to want to be very picky about matching the property set, the property ID, and the flags (GET vs PUT vs BASICSUPPORT) before you get involved. It certainly can be done -- I've done some very strange magic in KS filters -- but it's easy to get it wrong.It's also easy to get tangled up in the terminology. In that paragraph, I used the term "KS filter" twice with two very different meanings. One is "filter" in the DirectShow sense, in that every KS driver exposes a filter with pins. The other is "filter" in the PnP sense, as in a driver that inserts itself into the IRP chain.
Tim Roberts, [email protected]
Providenza & Boekelheide, Inc.
Thanks for the help everyone! I was able to successfully intercept the request I was interested in, access the output buffer, and make the desired changes before returning control to the application. Works like a charm as far as I can tell, but I haven't done any thorough testing yet.
A few observations I made along the way:
Well, actually… I spoke too soon. I did find some documentation that says I'm not really supposed to do that:
Note the "must". This would seem to indicate that calling
WdfRequestSend
from withinEvtIoInCallerContext
is illegal.That said I tested this approach and it works. I think it still makes more sense to do this in
EvtIoInCallerContext
than inEvtIoDeviceControl
so I'm going to stick to that unless anyone has a better idea.Thank you for warning me about this. I did encounter this problem. At first I tried using the standard method for METHOD_NEITHER (i.e. using
WdfRequestRetrieveUnsafeUserOutputBuffer
andWdfRequestProbeAndLockUserBufferForWrite
) but that didn't work - I was getting a pointer all right, and the size was correct, but the buffer it was pointing to wasn't being populated by the next-lower driver (in my caseusbaudio.sys
).What I found confusing at first is that the IRP
AssociatedIrp.SystemBuffer
field wasn't set, either. But that was because I was checking that field at the time I was receiving the request. It turns out that it was the next-lower driver that was setting that field! Therefore I had to check for it in my completion routine instead. Once I did that, I realized that was where the actual output buffer was located and at this point I was able to inspect it and make my changes.It's somewhat mind-boggling that a next-lower driver would suddenly decide to change the rules in the middle of an IRP and respond to a METHOD_NEITHER IOCTL with a kernel buffer. Clearly it's giving the middle finger to the official METHOD_NEITHER rules and doing its own thing instead. I wonder how the output buffer eventually makes it back to the application then. At first I thought it was
ksthunk
that was patching things up but clearly that's not the case because nothing seems to change if I removeksthunk
from the driver stack. So I guess it must be the I/O manager that notices there'sAssociatedIrp.SystemBuffer
is set and copies it back to the user buffer?That suggests that your filter loaded ABOVE ksthunk instead of BELOW ksthunk, which is where you really want to be. It's not so bad for 64-bit clients, but it's important for 32-bit clients. You might check the
UpperFilters
value in the registry to see if you shouldn't revise the order.It's the same pages, just mapped with a different virtual address. No copies required. Whatever you write to the physical pages will be seen immediately by everyone who has the buffer mapped.
Tim Roberts, [email protected]
Providenza & Boekelheide, Inc.
I thought about that, yes. My driver was always loaded below ksthunk the entire time (I set it up as a device filter, and ksthunk is a class filter). When I tried loading it as a class filter above ksthunk (or without ksthunk in the stack at all) I didn't really notice any difference, but I'll admit I haven't examined things too closely.
I thought so too, and I was curious to see if the 32-bit version of KS Studio was affected by this. To my surprise the answer was no:
x86/ksstudio.exe
still works even if I remove ksthunk completely, and my driver sees no difference to how buffers are exchanged between the 32-bit and 64-bit versions (at least as far asIOCTL_KS_PROPERTY
is concerned, anyway). This leaves me a bit confused as to what role ksthunk plays in this. ksthunk appears to be completely undocumented as far as I can tell. For reference, I do all my testing on the latest Windows 11 (from the stable channel).That doesn't match what I'm seeing. At the time my completion routine is called,
UserBuffer
andSystemBuffer
are pointing to different buffers that have different contents (UserBuffer
is pointing to garbage,SystemBuffer
is pointing to the actual data), not just different addresses. But somehow it's the contents ofSystemBuffer
that makes it back to the application, notUserBuffer
. I don't understand why that works for aMETHOD_BUFFER
IOCTL.Right. UserBuffer is the 1st buffer in DeviceIoControl -- the "input" buffer (the KSPROPERTY structure here). SystemBuffer is the 2nd buffer -- the "output" buffer (the property data here). They are different.
With METHOD_BUFFER, the first buffer is copied in and out. The second buffer is page mapped.
Tim Roberts, [email protected]
Providenza & Boekelheide, Inc.
Now I'm even more confused - it feels like we're observing completely different things.
UserBuffer
is definitely not the input buffer in my case. I've checked: the contents at that address are always garbage for the full duration of the IRP, they don't resemble neitherKSPROPERTY
nor theKSMULTIPLE_ITEM
output type I'm dealing with.According to the official docs, for
METHOD_NEITHER
, the input buffer isParameters.DeviceIoControl.Type3InputBuffer
, the output buffer isUserBuffer
, andSystemBuffer
is not mentioned.You're saying that the input buffer is
UserBuffer
and the output buffer isSystemBuffer
.What I'm actually observing is that the input buffer is
Parameters.DeviceIoControl.Type3InputBuffer
(well, I assume that's the case - sinceWdfRequestRetrieveUnsafeUserInputBuffer
andWdfRequestProbeAndLockUserBufferForRead
just work I haven't looked more closely), the output buffer isSystemBuffer
, andUserBuffer
is unused.So it looks like the official docs, you, and I are saying different things. I don't know how to reconcile all that. But anyway, as long as it works…
Sorry, I misspoke - I meant "I don't understand how that works for
METHOD_NEITHER
", notMETHOD_BUFFER
in my previous post, of course.@Tim_Roberts Okay so this got me intrigued, so I wrote some code that, on IRP dispatch and completion, prints the addresses of all 3 relevant pointers and attempts to "probe" them to guess what their contents look like. I then ran the driver under all possible combinations:
ksthunk
below and above my filter driver, as well as completely removed; with the user application being the Windows audio engine, 32-bit KS Studio, or 64-bit KS Studio - so 9 combinations in total. The next-lower driver isusbaudio.sys
.What I found was all 9 combinations, except one, produced something similar to the following:
The only exception is when
ksthunk
is above my filter driver and 32-bit KS Studio is used:My conclusions are as follows:
usbaudio.sys
, is usingType3InputBuffer
as the input buffer, consistent with the official documentation.METHOD_NEITHER
rules. The rules state the output buffer isUserBuffer
, but in practice the driver seems to be ignoring that field. The driver is usingSystemBuffer
as the output buffer instead.SystemBuffer
is NULL when the IRP is dispatched. It is the next-lower driver that is settingSystemBuffer
before completing the IRP. The only exception is ifksthunk
sits on top and a 32-bit user application is used; in this case,SystemBuffer
is already allocated when the IRP is dispatched.I also observed that, somewhat surprisingly, as far as the user application is concerned, things still appear to work even if
ksthunk
is completely removed from the driver stack. That includes even 32-bit KS Studio. So whateverksthunk
is doing, it doesn't appear to be strictly necessary for proper operation. At least as far as KS property queries are concerned, that is.Obviously it's quite possible that some of the above conclusions only hold for specific IOCTLs and/or user-mode applications and/or specific next-lower drivers and/or specific Windows versions. I guess only testing will tell.
There are many weird things in the KS communication path. When an audio device is opened, the file name in the IRP_MJ_CREATE call includes the WAVEFORMATEX structure that defines the client's desired format. No one promised that file names would be simple zero-terminated strings, but finding a binary structure in there was a surprise.
Tim Roberts, [email protected]
Providenza & Boekelheide, Inc.