We figured out what was happening and in case anyone is interested…
When NTFS is completing a notification IRP it may, under some circumstances (high IRQL?), decide to allocate another buffer to put the results into rather than put them into the original buffer allocated by the caller. It stores the pointer to this buffer in AssociatedIrp.SystemBuffer. It also modifies the irp flags to signal this. Under normal circumstances, when the irp is completed the io manager notices this, copies the data from the NTFS allocated buffer into the original user buffer and frees the NTFS allocation.
So, if you roll a notification irp, in completion if AssociatedIrp.SystemBuffer is not NULL then your results will be in AssociatedIrp.SystemBuffer and you are responsible to free it.