macOS Sierra bug: PC/SC transactions can't be nested

This is the second new PC/SC bug I report for macOS Sierra (first one was "macOS Sierra bug: SCardTransmit() silently truncates the card response").
I imagine this bug is also present in El Capitan and Yosemite but I have not checked.

SCardEndTransaction() ends all the transactions

SCardEndTransaction() does not work as it should, at least it does not work as the pcsc-lite version on GNU/Linux.
If you start a new PC/SC transaction inside a PC/SC transaction you expect the card to be available to another application only after all the transactions have ended.

For example if, in one application, you do:
A. SCardBeginTransaction()
  B. SCardBeginTransaction()
  C. SCardEndTransaction()
D. SCardEndTransaction()
You may expect that the card is available again for another application after the transaction is ended in step D.

What happens on macOS Sierra is that the transaction is ended at step C instead. So all the card APDU exchanges between steps C and D are not protected by the PC/SC transaction and can be mixed with APDU exchanges from any other applications.

It looks like PC/SC on macOS does not include a nested transaction counter. The first call to SCardEndTransaction() will end the transaction.

The sample code above is very simple and may seem stupid. But maybe the steps B and C are executed in another function of your application and then more hidden.

The macOS Sierra behaviour may produce very bad side effects. Imagine you use transactions to restrict access to the card private key after verifying the card PIN code. Then another application could also use the private key (between steps C and D) before the PIN is unverified just before step D.

See also

Apple bug report #33940052 "Smart card PC/SC transactions can't be nested"
Closed by Apple as a duplicate of #33752531.

Sample code


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

#ifdef __APPLE__
#include <PCSC/winscard.h>
#include <PCSC/wintypes.h>
#else
#include <winscard.h>
#endif

#define pcsc_error(fct) printf(fct ": %s 0x%08lX\n", pcsc_stringify_error(err), (long)err)

int main(void)
{
    LPSTR mszReaders;
    DWORD err, cchReaders = 0;
    SCARDCONTEXT hContext = 0;
    SCARDHANDLE hCard = 0;

    err = SCardEstablishContext(SCARD_SCOPE_SYSTEM, NULL, NULL, &hContext);
    if (err != SCARD_S_SUCCESS) {
        pcsc_error("ScardEstablishedContext");
        return -1;
    }

    err = SCardListReaders(hContext, NULL, NULL, &cchReaders);
    if (err != 0) {
        pcsc_error("ScardListReaders");
        return -1;
    }
    mszReaders = calloc(cchReaders, sizeof(char));
    if (!mszReaders) {
        printf("calloc\n");
        return -1;
    }
    err = SCardListReaders(hContext,NULL, mszReaders, &cchReaders);
    if (err != SCARD_S_SUCCESS) {
        pcsc_error("ScardListReaders");
        return -1;
    }

    printf("Using reader: %s\n", mszReaders);

    DWORD dwActiveProtocol;
    printf("connect\n");
    err = SCardConnect(hContext, mszReaders, SCARD_SHARE_SHARED, SCARD_PROTOCOL_T0 | SCARD_PROTOCOL_T1, &hCard, &dwActiveProtocol);
    if (err != SCARD_S_SUCCESS) {
        pcsc_error("SCardConnect");
        return 0;
    }

    printf("transaction 1\n");
    err = SCardBeginTransaction(hCard);
    if (err != SCARD_S_SUCCESS)
        pcsc_error("SCardBeginTransaction");

    printf("transaction 2\n");
    err = SCardBeginTransaction(hCard);
    if (err != SCARD_S_SUCCESS)
        pcsc_error("SCardBeginTransaction");

    printf("release transaction 2\n");
    err = SCardEndTransaction(hCard, SCARD_LEAVE_CARD);
    if (err != SCARD_S_SUCCESS)
        pcsc_error("SCardBeginTransaction");

    printf("Start me again and press enter\n");
    getchar();

    printf("release transaction 1\n");
    err = SCardEndTransaction(hCard, SCARD_LEAVE_CARD);
    if (err != SCARD_S_SUCCESS)
        pcsc_error("SCardBeginTransaction");

    err = SCardDisconnect(hCard, SCARD_LEAVE_CARD);
    if (err != SCARD_S_SUCCESS)
        pcsc_error("SCardDisconnect");

    SCardReleaseContext(hContext);

    return 0;
}

Result (on Sierra)

$ CFLAGS="-framework PCSC" make main
cc -framework PCSC    main.c   -o main

First execution:
$ ./main_Mac
Using reader: Gemalto PC Twin Reader
connect
transaction 1
transaction 2
release transaction 2
Start me again and press enter

Second execution in another window:
$ ./main_Mac 
Using reader: Gemalto PC Twin Reader
connect
transaction 1
transaction 2
release transaction 2
Start me again and press enter

Expected result (on Debian)

$ CFLAGS=`pkg-config --cflags libpcsclite` LDFLAGS=`pkg-config --libs libpcsclite` make main
cc -pthread -I/usr/include/PCSC    -lpcsclite    main.c   -o main

First execution:
$ ./main_Linux 
Using reader: Gemalto PC Twin Reader 00 00
connect
transaction 1
transaction 2
release transaction 2
Start me again and press enter

Second execution in another window:
$ ./main_Linux 
Using reader: Gemalto PC Twin Reader 00 00
connect

Here you notice that the second execution is blocked at the "connect" step. Once you press "Enter" for the first execution then the transaction is released and the second execution can continue.

Known workaround

None. Do not use nested PC/SC transactions.

Update: 8th January 2018

The bug is fixed in macOS High Sierra 10.13.2.