SAL notation and good practices

First question. I have a minifilter and the MSDN docs say a particular callback is called at a certain IRQL, do you guys still put the SAL notation even though it’s stated on the MSDN docs. i.e. IRQL_requires(PASSIVE_LEVEL)

Second question. What additional ways are there to improve safety of kernel driver code with use of verifier and such.

I use SAL notations where possible i.e. https://docs.microsoft.com/en-us/windows-hardware/drivers/devtest/irql-annotations-for-drivers
Use of PAGED_CODE() macro etc
Do you guys have a list of common good practices for drivers?
Do you also happen to know where coding style is documented by MS for kernel code, i.e. the waterfall model approach to functions after n characters, which whilst can all be set in Visual Studio settings,
//
// comment style
//
etc, I’d like too see a guide if one exists publicly.

I typically look at MS driver sample code, and follow these practices. I have the “microsoft windows driver model” book by Walter Oney, but it doesn’t really describe good and safe practices.

the MSDN docs say a particular callback is called at a certain IRQL, do you guys still put the SAL notation even though it’s stated on the MSDN docs

Make sure you’re using the role-type declaration (PFLT_PRE_OPERATION_CALLBACK or whatever), and yes… put the IRQL annotation. Because otherwise CA and SDV won’t know the IRQL assumption (cuz the role-type declaration doesn’t include an annotation for IRQL).

I’d recommend you read Scott Noone’s excellent blog post about SAL annotations here.

Embracing SAL annotations is a journey. If you embrace them fully, they can really add a lot of value. It’s taken me several years to come to this conclusion.

I typically look at MS driver sample code, and follow these practices

That’s usually good for a start. But, bear in mind that a lot of the sample drivers aren’t really designed to be “golden samples” – they’re meant to illustrate some points or demonstrate the use of a certain technology.

I was giving a talk last week about “Best Practices” for WDF driver development, the majority of which should apply to any Windows driver mode, and I summarized my recommendations as follows:

  • Build with warning level /W4
  • Enable Code Analysis on all builds, with most strict applicable rule set
  • Embrace SAL annotations – Particularly for locking.
  • Enable WDF Verifier during all your testing (assuming you’re writing code in WDF) – enable verbose logging
  • Enable Windows Driver Verifier for all testing – enable all options except the Low Resource Simulation options
  • Periodically run SDV – It might be a PITA, but it finds real bugs
  • Don’t be quick to dismiss/suppress stuff from CA or SDV that doesn’t immediately look like a legitimate problem. If you’re going to spend the time to use these tools, spend some quality time really trying to understand what the diagnostics mean. Suppress only after careful consideration and as a last resort.

ETA:

Do you also happen to know where coding style is documented by MS for kernel code

It’s not documented. It’s referred to as Cutler Normal Form, and it is basically the format used by Dave Cutler (the father of Windows NT).

Hope that helps,

Peter

@“Peter_Viscarola_(OSR)” said:
Make sure you’re using the role-type declaration (PFLT_PRE_OPERATION_CALLBACK or whatever), and yes… put the IRQL annotation. Because otherwise CA and SDV won’t know the IRQL assumption (cuz the role-type declaration doesn’t include an annotation for IRQL).
So I’ve looked at the declaration of that in specstrings.h. but it’s rather limited. So using your mentioned role-type declaration would that be replacing a Outptr with a stricter Flt_CompletionContext_Outptr if available? What is the way to check if you have these role-type declaration SAL notations available to use?

In my instance I’m developing an NDIS filter, and I can’t exactly find the declaration to callback FilterAttach() as FILTER_ATTACH_HANDLER. But looking at ndis.h for the declaration as seen below, is that all the standard SAL notations I could apply. And adding SAL notations to any custom behaviour with ptrs/mem.

_IRQL_requires_max_(PASSIVE_LEVEL)
_Function_class_(FILTER_ATTACH)
NDIS_STATUS
(FILTER_ATTACH)(
    _In_  NDIS_HANDLE                     NdisFilterHandle,
    _In_  NDIS_HANDLE                     FilterDriverContext,
    _In_  PNDIS_FILTER_ATTACH_PARAMETERS  AttachParameters
    );
typedef FILTER_ATTACH (*FILTER_ATTACH_HANDLER);

I’d recommend you read Scott Noone’s excellent blog post about SAL annotations here.
Yeah that was a really good read, tah.

Yes that “best practices” block looks like a good list to follow. Any other MS docs you’ve seen on practices?

Yes that’s it. Regarding CNF form. Are there any documents on it? I’ve read the limited couple of pages on Google regarding CNF, and it’s mostly people’s thoughts and reactions to using it.

I’m developing an NDIS filter

Ah, sorry… I read “filter” and I immediately thought “file system minifilter” – What you see is a function of where you sit, as I like to say.

What is the way to check if you have these role-type declaration SAL notations available to use?

The docs have, for the past few years, tried to include them wherever possible. Be aware that the primary reason for them is that they include hint the static analysis tools the ROLE of the function (“this is a callback from the Filter Manager”)… and you get the side benefit of having the SAL Annotations as well.

Regarding CNF form. Are there any documents on it?

I’ve never seen a style guide or anything like that, even for internal use at MSFT. People always said “Read the NTOS sources and write code that looks like that” – which I guess externally would translate to “read whatever you can find, the WDF source code on github is a decent example (though it uses C++ “as a better C” and you won’t see THAT in the NTOS sources), and write code that looks like that.” ETA later: Or, you could, ahem, Google for the Windows Research Kernel and, ahem, look at some of that source code. Cough. In GitHub. I’d look at Mm, Io, and Ke, particularly for code style examples.

Not to turn this into a massive digression on CNF, but there’s considerable latitude in the specific stylistic practices used. For example, there’s no clear consensus in the OS sources on the goodness or badness of using “goto” for exits, or whether or not “every function should have only one entry point and one exit point” – Just to choose two familiar points of religious contention.

I will note that One True Brace style is always used, and “else” statements are always “cuddled” as follows:

if (fred = true) {

// do something

} else {

// something else

}

Line widths are always limited to 80 chars. Embed spaces not tabs, indents are 4 characters each.

But these are really matters of stylistics (and thus purely a matter of conformity and not wanting to be “the nail that sticks out”) not actual code quality.

Oh, and be aware that most of the code in userland does not follow CNF… So, like, if you look at the source code for Windows Calculator, it’s not likely to be follow CNF :slight_smile:

Note that a subset of C++ is allowed in kernel-mode in Windows. Here at OSR, we have used C++ “as a better C” for a very long time and thus always use “.cpp” as our source code extension/file type. We’re particularly fans of strong type checking, and not fans of OO design in kernel-mode. Interestingly, there’s starting to be more C++ use internally (in kernel mode) at MSFT as well (Mr. Tippet from the NDIS team posts here occasionally, and has mentioned that he’s been championing some of this work in his “copious spare time”).

ETA:

Another tip I’ll “throw in” here… One of the primary complaints about SAL is that it makes your code unreadable. And so it can do. What we do at OSR to help combat this is that we put all our SAL annotations for our functions in the forward declaration for the function (which we put in the header file). That makes these declaration unreadable, of course. But at the point of implementation, where we really want the code to be readable, we prefix the function with “use_decl_annotations” – which allows us to NOT have to repeat (and match!) all the SAL annotations at point of implementation and thus leave the function implementation readable.

Peter

thus always use “.cpp” as our source code extension/file type. We’re particularly fans of strong type checking,

Agreed…

For example, consider the following lines

unsigned int x;

if(x<0) do_things();

Unless you just want to make sure that do_things() never ever gets invoked, this code is obviously buggy. Although C++ would not allow it to pass through, C is going to compile it without any problem.

and not fans of OO design in kernel-mode.

Actually, OOP is quite a good thing in itself. However, I don’t see any reason why it should be necessarily done in C++. After all, this is nothing more than just a way of thinking - at the assembly level, it all boils down just to passing an extra context argument to a function,
and filling tables with the function pointers that have to match a certain specification of the argument list and the return value. Therefore, it can be easily done in plain C.

For example, IRP_MJ_CREATE request is obviously going to have different meanings for a regular file, directory, and volume object. Instead of choosing the right path in an ugly switch-case block, you can fill a table with functions pointers, and save it in VCB (or whatever you would call the vnode equivalent in the Windows world) that FILE_OBJECT refers to. From now on, whenever your IRP_MJ_CREATE gets invoked, it can simply get the address of the target table from forward vnode, and forward a call to the appropriate function.

Anton Bassov

Any discussion in this forum in any way related to software development
ends up in “C++ bad hell.”
I personally am outraged that I cannot write drivers in haskell.

Mark Roddy

Why not argue about Rust? :wink:

d

Bent from my phone


From: Mark_Roddy
Sent: Saturday, March 9, 2019 2:06:32 PM
To: Doron Holan
Subject: Re: [NTDEV] SAL notation and good practices

OSR https://nam06.safelinks.protection.outlook.com/?url=http%3A%2F%2Fosr.vanillacommunities.com%2F&amp;data=02|01|doron.holan%40microsoft.com|4975aa57994c473ed73b08d6a4db7da6|72f988bf86f141af91ab2d7cd011db47|1|0|636877659949270873&amp;sdata=GfQJ33UOyWJW8NVpe6yv0gVc9IiT7lFbFN5Na1YqI%2Bk%3D&amp;reserved=0
Mark_Roddy commented on SAL notation and good practices

Any discussion in this forum in any way related to software development
ends up in “C++ bad hell.”
I personally am outraged that I cannot write drivers in haskell.

Mark Roddy

Any discussion in this forum in any way related to software development ends up in “C++ bad hell.”

Actually, I don’t really understand what led you to such a “bold” conclusion…

To begin with, if we express time in terms of Mr.Rolle’s posts to NTDEV, the last time we spoke about C++ was, IIRC,
more than 130 Mr.Rolle’s posts ago. We had had quite a few interesting discussions on NTDEV since, and, IIRC, C++ had not been
anywhere in sight until yesterday. The most interesting this is that, when it comes to this particular discussion, no one on this thread has made a SINGLE unfavourable reference to C++ in so far. Furthermore, we even pointed out certain advantages that C++ has over C (namely, strong type checking).

Therefore, my question is " WTF is your observation based upon???"…

I personally am outraged that I cannot write drivers in haskell.

Irony aside, I suspect you are not the only one who feels this way. Imagine what our life would be like - you write your code, test it once, and if it works on this particular occasion you can be 100% sure that it will always work this way, no matter what and no matter how…

Anton Bassov

Why not argue about Rust?

Because rust never sleeps.

Hey, hey… my, my…

Peter

(empty message)

Because rust never sleeps

…but still there are some interesting projects that make use of it. Taking into consideration your longstanding interest in microkernels,
I think you may find the one below of particular interest

https://www.redox-os.org/

What I find of particular interest for myself is their TFS filesystem - according to them, they find ZFS too slow because of monolithic design, so that they are developing their own ZFS-like filesystem

https://github.com/redox-os/tfs

Anton Bassov

Thanks @“Peter_Viscarola_(OSR)” that really helps.

Do you guys also create unit tests for your driver functions to test a variety of min/max val inputs? If so, how do you go about doing it - an existing framework, another VS project calling into funcs, something else?

There really are no good frameworks for thoroughly testing kernel mode code. So, all our testing is ad hoc, using a custom written test utility. We’re also fans of writing as much as possible in first, testing the shit out of there where it’s easier, and then moving the code to driver land. We did this a while back with a reasonably complex caching subsystem (even going so far as to write multithreaded tests) and it was super effective.

Oh, and don’t ignore running the HLK tests. Either from the whole hideous infrastructure or as stand alone tests. Always annoying, but usually quite helpful in sussing out problems.

Peter

SAL annotations are pretty great; just a couple weeks ago they found a codepath where I’d forgotten to acquire a spinlock. There aren’t many hard rules for how to use them; you’re naturally allowed to adapt any guidelines to fit your needs. E.g., a smaller project might not have much locking, and so might not need to trot out all the fancy lock annotations.

In general (and in particular for NDIS filter drivers – my own area), when there’s a standard callback that you have to implement, the OS header should have the IRQL annotation on the function prototype that’s in the WDK header file. For example, FILTER_ATTACH is a WDK-provided typedef with an accompanying annotation:

typedef
_IRQL_requires_max_(PASSIVE_LEVEL)
_Function_class_(FILTER_ATTACH)
NDIS_STATUS
(FILTER_ATTACH)(
    _In_  NDIS_HANDLE                     NdisFilterHandle,
    _In_  NDIS_HANDLE                     FilterDriverContext,
    _In_  PNDIS_FILTER_ATTACH_PARAMETERS  AttachParameters
    );

When you implement this, I suggest (guideline!) that you use only _Use_decl_annotation_ on the implementation, so that you don’t have to update your code when you update the WDK. Example:

FILTER_ATTACH MyFilterAttach;

_Use_decl_annotations_
NDIS_STATUS
MyFilterAttach(
    NDIS_HANDLE NdisFilterHandle,
    NDIS_HANDLE FilterDriverContext,
    PNDIS_FILTER_ATTACH_PARAMETERS AttachParameters)
{ . . . }

While it’s totally legal to copy all the annotations from the header, you’re signing yourself up for keeping those up-to-date if the header ever changes. Note that some older WDK function prototypes don’t have SAL annotations. In that case, you do gotta write them yourself.

In contrast, when you control both the header and the source file (this likely will be most of the functions in your driver), I tend to choose to avoid _Use_decl_annotations_ and instead duplicate the annotations on both the .H and .C files. Duplication is bad (DRY etc), but I don’t mind this one since it serves as good documentation.

A common error: if you peek at the definition of PAGED_CODE(), you’ll see that it’s a macro that expands out to an assert on the IRQL. However, it is not the same thing as NT_ASSERT(KeGetCurrentIrql() < DISPATCH_LEVEL). The difference is that the PAGED_CODE() macro asserts that this code can be paged out, while the latter only asserts something about the IRQL. To see an example where this makes a difference, consider this function:

_IRQL_requires_(PASSIVE_LEVEL)
void F()
{
    PAGED_CODE(); // wrong!

    if (RtlEqualUnicodeString(. . .)) { // requires passive level
        KIRQL oldIrql;
        KeAcquireSpinLock(&my_lock, &oldIrql); // raises to dispatch level
    . . .
}

You must enter the function at PASSIVE_LEVEL, so you can access the string comparison routine. But the whole function cannot be pageable, because it does sometimes raise the
IRQL when grabbing the spinlock. So it’s wrong to mark the function as PAGED_CODE(), because this code cannot be paged. The developer really meant to say NT_ASSERT(KeGetCurrentIrql() == PASSIVE_LEVEL) instead.

Another common error: annotations are about contracts, not implementations. Just because a function currently could run at DIRQL doesn’t mean that you should annotate it as such. Eventually you’ll develop a “feel” for what sort of code goes into which IRQL. E.g., for NDIS, the datapath runs <=DISPATCH_LEVEL, but controlpath mostly runs at PASSIVE_LEVEL. So when you go to annotate your own functions, think about which IRQLs this code ever deserves to run at, not what its current implementation could do. If you are conservative in what you annotate for, that gives you more latitude to later change the implementation without breaking the contract.

Microsoft does not have any rules for driver code style. Use whatever style makes you most productive and your colleagues most happy. There’s no single style used uniformly by all 60 hojillion lines of code in Windows, so you can’t say “I just want to use Windows style”. You can use CNF if you like, although CNF tends to fall apart with C++ code, and it has a few quirks that are typically considered antipatterns for modern code (e.g., declaring all variables at the top of the functions). But really, there’s enough people clamoring to tell you how to style your code that you don’t need Microsoft to join into the fray too.

Thanks for weighing-in Mr. Tippet.

typically considered antipatterns for modern code (e.g., declaring all variables at the top of the functions)

Oh, you just HAD to pick one of the conventions I love, didn’t you! :slight_smile:

Peter