Improved SCardGetStatusChange() for "\\?PnP?\Notification" special reader

SCardGetStatusChange() works fine to detect a change in one of the listed readers. It is also possible to detect when a reader has been added or removed using the special reader name "\\?PnP?\Notification". See How to use SCardGetStatusChange()?.

One old problem is:

how to detect that a reader has been added between two calls to SCardGetStatusChange()?

Even if you call SCardGetStatusChange() just after the previous SCardGetStatusChange() returned you may still suffer from a race condition and miss a reader event.

In pcsc-lite version 2.3.0 I implemented a solution for that use case.

SCardGetStatusChange() will now use the high word (high 16-bits) of the .dwEventState field for the special reader "\\?PnP?\Notification" to store the number of reader events (since the start of the pcscd daemon).

Pseudo code algorithm

The suggested way to fix the problem is to use something like that:

  1. initial call to SCardGetStatusChange()

  2. get the initial value from .dwEventState

  3. copy this value in .dwCurrentState

  4. second call to SCardGetStatusChange()

If a reader has been added before the second SCardGetStatusChange() call then SCardGetStatusChange() will return immediately.

Sample code

File sample.c:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>

#include <winscard.h>

#define CHECK(f, rv, panic) \
 printf(f "(): 0x%08lX, %s\n", rv, pcsc_stringify_error(rv)); \
 if ((SCARD_S_SUCCESS != rv) && panic) \
 { \
  return -1; \
 }

#define PANIC true
#define DONT_PANIC false

#define MAX_READERS 16

int main(void)
{
    LONG rv;
    SCARDCONTEXT hContext;
    LPTSTR mszReaders;
    DWORD dwReaders = 0;
    char *ptr, *readers[MAX_READERS] = {};
    int nb_readers;
    SCARD_READERSTATE rgReaderStates[MAX_READERS];

    rv = SCardEstablishContext(SCARD_SCOPE_SYSTEM, NULL, NULL, &hContext);
    CHECK("SCardEstablishContext", rv, PANIC)

    rv = SCardListReaders(hContext, NULL, NULL, &dwReaders);
    CHECK("SCardListReaders", rv, DONT_PANIC)

    mszReaders = calloc(dwReaders, sizeof(char));
    rv = SCardListReaders(hContext, NULL, mszReaders, &dwReaders);
    CHECK("SCardListReaders", rv, DONT_PANIC)
    printf("Found readers:\n");
    nb_readers = 0;
    for (ptr=mszReaders; *ptr; ptr += strlen(ptr) +1)
    {
        printf(" %s\n", ptr);
        readers[nb_readers++] = ptr;
    }
    printf("Found %d readers\n", nb_readers);

    /* add the special reader */
    readers[nb_readers++] = "\\\\?PnP?\\Notification";

    /* get the initial states */
    for (int i=0; i<nb_readers; i++)
    {
        rgReaderStates[i].szReader = readers[i];
        rgReaderStates[i].dwCurrentState = SCARD_STATE_UNAWARE;
    }
    rv = SCardGetStatusChange(hContext, 0, rgReaderStates, nb_readers);
    CHECK("SCardGetStatusChange", rv, DONT_PANIC)

    /* set the current state */
    for (int i=0; i<nb_readers; i++)
    {
        rgReaderStates[i].dwCurrentState = rgReaderStates[i].dwEventState;
        printf("reader: %s, events #: %d\n",
            readers[i], rgReaderStates[i].dwEventState >> 16);
        printf("reader: %s, events state 0x%04X\n",
            readers[i], rgReaderStates[i].dwEventState & 0xFFFF);
    }

    /* wait for a change */
    rv = SCardGetStatusChange(hContext, INFINITE, rgReaderStates, nb_readers);
    CHECK("SCardGetStatusChange", rv, PANIC)

    /* new state */
    for (int i=0; i<nb_readers; i++)
    {
        printf("reader: %s, events #: %d\n",
            readers[i], rgReaderStates[i].dwEventState >> 16);
        printf("reader: %s, events state 0x%04X\n",
            readers[i], rgReaderStates[i].dwEventState & 0xFFFF);
    }

    free(mszReaders);

    rv = SCardReleaseContext(hContext);
    CHECK("SCardReleaseContext", rv, PANIC)

    return 0;
}

File Makefile:

# Linux
PCSC_CFLAGS := $(shell pkg-config --cflags libpcsclite)
LDLIBS := $(shell pkg-config --libs libpcsclite)

CFLAGS = $(PCSC_CFLAGS) -g

sample: sample.c

clean:
    rm -f sample

Connect a reader

We start the sample code with no reader connected:

 SCardEstablishContext(): 0x00000000, Command successful.
 SCardListReaders(): 0x8010002E, Cannot find a smart card reader.
 SCardListReaders(): 0x8010002E, Cannot find a smart card reader.
 Found readers:
 Found 0 readers
 SCardGetStatusChange(): 0x8010000A, Command timeout.
 reader: \\?PnP?\Notification, events #: 33
 reader: \\?PnP?\Notification, events state 0x0000

 >>> connection of a new reader here

 SCardGetStatusChange(): 0x00000000, Command successful.
 reader: \\?PnP?\Notification, events #: 34
 reader: \\?PnP?\Notification, events state 0x0002
 SCardReleaseContext(): 0x00000000, Command successful.

State value 0x0002 is SCARD_STATE_CHANGED.

The first SCardGetStatusChange() returns a reader events of 33. After I connect a reader the number goes to 34.

Disconnect a reader

We restart the sample code with the reader connected:

 SCardEstablishContext(): 0x00000000, Command successful.
 SCardListReaders(): 0x00000000, Command successful.
 SCardListReaders(): 0x00000000, Command successful.
 Found readers:
  Gemalto PC Twin Reader 00 00
 Found 1 readers
 SCardGetStatusChange(): 0x00000000, Command successful.
 reader: Gemalto PC Twin Reader 00 00, events #: 0
 reader: Gemalto PC Twin Reader 00 00, events state 0x0012
 reader: \\?PnP?\Notification, events #: 34
 reader: \\?PnP?\Notification, events state 0x0000

 >>> removal of the reader here

 SCardGetStatusChange(): 0x00000000, Command successful.
 reader: Gemalto PC Twin Reader 00 00, events #: 0
 reader: Gemalto PC Twin Reader 00 00, events state 0x000E
 reader: \\?PnP?\Notification, events #: 35
 reader: \\?PnP?\Notification, events state 0x0002
 SCardReleaseContext(): 0x00000000, Command successful.

The reader "Gemalto PC Twin Reader 00 00" has been removed. Its state value 0x000E contains the bits:

  • SCARD_STATE_UNKNOWN

  • SCARD_STATE_UNAVAILABLE

  • SCARD_STATE_CHANGED

Connect a second reader

We restart the sample code with the reader connected:

 SCardEstablishContext(): 0x00000000, Command successful.
 SCardListReaders(): 0x00000000, Command successful.
 SCardListReaders(): 0x00000000, Command successful.
 Found readers:
  Gemalto PC Twin Reader 00 00
 Found 1 readers
 SCardGetStatusChange(): 0x00000000, Command successful.
 reader: Gemalto PC Twin Reader 00 00, events #: 0
 reader: Gemalto PC Twin Reader 00 00, events state 0x0012
 reader: \\?PnP?\Notification, events #: 36
 reader: \\?PnP?\Notification, events state 0x0000

 >>> connection of a second reader

 SCardGetStatusChange(): 0x00000000, Command successful.
 reader: Gemalto PC Twin Reader 00 00, events #: 0
 reader: Gemalto PC Twin Reader 00 00, events state 0x0010
 reader: \\?PnP?\Notification, events #: 37
 reader: \\?PnP?\Notification, events state 0x0002
 SCardReleaseContext(): 0x00000000, Command successful.

The Gemalto reader event state is 0x0010 = SCARD_STATE_EMPTY because it does not contains a smart card. It does not have the bit SCARD_STATE_CHANGED set because it's state has not changed.

The PnP reader state is 0x0002 = SCARD_STATE_CHANGED because a new reader was connected.

Remarks

Timeout

It is important to use a timeout of 0 (parameter dwTimeout) in the first call to SCardGetStatusChange() to make the function returns immediately.

Unitary Test

If you want to know if your implementation of PC/SC includes the fix or not (or test on Windows or macOS) you can use the program CardGetStatusChange_PnP_Events.py included in pcsc-lite UnitaryTests/SCardGetStatusChange/ directory.

Windows

Microsoft implemented a similar mechanism. But instead of using the number of reader events it uses the number of connected readers.

So between the 2 calls to SCardGetStatusChange() if one reader is added and one reader is removed you may not be notified.

macOS

Apple does not support the special reader "\\?PnP?\Notification". See OS X El Capitan missing feature: SCardGetStatusChange() and "\\?PnP?\Notification".

So there is no way it supports the mechanism presented here.

Backward compatibility

To not break existing codes the new SCardGetStatusChange() beahavior is enabled only if the number of reader events in .dwCurrentState is not zero.

That is also one reason why you should use a dwTimeout value of 0 for the first call.

.dwEventState for normal readers

The high 16-bits of .dwEventState for the normal readers is also used. It contains the number of card events for this particular reader.

This is an old behavior that is documented in the API documentation:

dwEventState also contains a number of events in the upper 16 bits (dwEventState & 0xFFFF0000). This number of events is incremented for each card insertion or removal in the specified reader. This can be used to detect a card removal/insertion between two calls to SCardGetStatusChange()

Conclusion

I was aware of this race issue with SCardGetStatusChange() since a long time (at least 2018. Thanks Maksim). But I got a new motivation boost after an issue was reported SCardGetStatusChange: Race condition when attaching multiple readers at the same time.

It is now the job of applications programmers to use this new feature in their codes.

If you find a regression in your application because of this change please open an issue.