giovedì 15 agosto 2019

08 - Supporto all'accesso in memoria: Buffered I/O

Fino ad ora la comunicazione tra client e driver è stata rappresentata ed implementata come un semplice accesso diretto alla memoria in user space da parte del driver. Questo, purtroppo, non sempre è conveniente o, addirittura, possibile. Nelle lezioni precedenti si è visto, infatti, che si è dovuti ricorrere ad un blocco __try\__exception durante l'accesso al buffer per evitare eventuali eccezioni e possibili crash nel caso in cui il client fornisse un indirizzo non valido per il buffer. Il blocco __try\__exception, benché non elegante, risolve questo problema ma, purtroppo, questa non è tutta la storia. Se il nostro codice in kernelmode è eseguito come DPC, ad un IRQL $= 2$, sappiamo che il contesto del thread è arbitrario (si veda [3]) e quindi non è garantito che il thread che si occupa di eseguire la DPC sia lo stesso che ha effettuato la richiesta al device in usermode (in questo modo non è possibile accedere al buffer in user space da kernelmode perché il thread non "vedrà" tale buffer dato che probabilmente è in esecuzione con associato uno spazio di indirizzamento virtuale diverso). Inoltre, per IRQL $\ge 2$ non è possibile fare riferimento a memoria Paged e questo è un problema poiché non c'è alcuna garanzia che la memoria in user space sia in RAM.
Per evitare tutti questi problemi è possibile chiedere supporto all'I/O Manager riguardo l'accesso dei buffer forniti dal client. Come visto nella lezione precedente (si veda [1]) con il codice di controllo è possibile indicare il tipo di trasferimento dei dati da client a driver per una data operazione. Con METHOD_NEITHER si afferma che non si vuole alcun supporto da parte dell'I/O Manager e si vuole accedere ai buffer del client direttamente. Atri valori permettono di attivare tale supporto per DeviceIoControl e relativa dispatch routine. Per quanto riguarda ReadFile\WriteFile e relative dispatch routine il discorso è simile, cambia solo modo ti attivare il supporto da parte dell'I/O Manager. Un'altra buona notizia è che tra del codice che non implementa tale supporto e lo stesso che lo implementa la differenza si riduce a poche righe di codice. Ma prima di vedere degli esempi pratici è necessaria un po' di teoria per analizzare e chiarire come funziona il supporto dell'I/O Manager, in particolare quello che utilizza il trasferimento dei dati noto come Buffered I/O (argomento di questa lezione), sia per ReadFile\WriteFile che per DeviceIoControl.




Il client ha un buffer di input (richiesta in scrittura: WriteFile) o di output (richiesta in lettura: ReadFile) o entrambi (uno di input e uno di output oppure uno solo che sia di input e di output allo stesso tempo: DeviceIoControl).




Se il supporto dell'I/O Manager è attivo ed il tipo di trasferimento dati è Buffered I/O, l'I/O manager alloca memoria nonPaged in kernel space e salva il puntatore a tale buffer di sistema in AssociatedIrp.SystemBuffer della struttura IRP. Con ReadFile\WriteFile la dimensione di tale memoria è uguale a quella del buffer in user space. Inoltre, se il buffer è di input (WriteFile), l'I/O Manager copia il buffer in user space nello spazio allocato in kernel space.
Per DeviceIoControl la dimesione è uguale al quella massima tra il buffer di input e quello di output. Inoltre, l'I/O Manager copia il buffer di input in user space nello spazio allocato in kernel space.




A questo punto il driver può leggere o scrivere il buffer di sistema che, essendo in kernel space e condiviso dagli spazi virtuali, può essere consumato a prescindere da quale sia il thread che gestisce la richiesta. Questo risolve sia il problema del codice eseguito come DPC a IRQL $= 2$ che il problema del buffer liberato prematuramente da un altro thread del client (eventuali eccezioni per dati corrotti o robe simili verranno lanciate in usermode al momento opportuno; per quanto riguarda il driver, il suo lavoro lo fa sul buffer di sistema che è sempre valido e non può essere liberato).




Se la richiesta è stata inoltrata attraverso ReadFile il buffer del client è di output quindi l'I/O Manager copia il buffer di sistema nel buffer in user space. Per DeviceIoControl, l'I/O Manager copia il buffer di sistema nel buffer di output in user space. Il numero di byte copiati, in entrambi i casi, è uguale al valore che si trova nel campo IoStatus.Information della struttura IRP.




Infine l'I/O Manager libera la memoria di sistema allocata ed il client può continuare a leggere o scrivere il suo buffer. Fatta questa doverosa premessa teorica sul Buffered I/O, è arrivato il momento di analizzare quali sono le (poche) modifiche necessarie al codice delle lezioni precedenti.



ReadFile\WriteFile

Per attivare il Buffered I/O è necessario solo effettuare un OR tra il campo Flags dell'oggetto Device in DriverEntry ed il valore DO_BUFFERED_IO

NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObjectPUNICODE_STRING RegistryPath)
{
 UNREFERENCED_PARAMETER(RegistryPath);
 
 // ...
 
 DeviceObject->Flags |= DO_BUFFERED_IO;
 
 // ...
 
}

Nelle dispatch routine relative agli indici IRP_MJ_READ e IRP_MJ_WRITE le uniche due modifiche riguardano il recupero del buffer e la rimozione del blocco __try\__exception

NTSTATUS RWDriverRead(PDEVICE_OBJECT pDOPIRP pIrp)
{
 UNREFERENCED_PARAMETER(pDO);
 
 KeWaitForMutexObject(&mutex, ExecutiveKernelModeFALSENULL);
 NTSTATUS status = STATUS_INVALID_USER_BUFFER;
 PIO_STACK_LOCATION stack;
 ULONG len = 0;
 
 __try
 {
  stack = IoGetCurrentIrpStackLocation(pIrp);
  len = stack->Parameters.Read.Length;
  if (len == 0)
   return CompleteIrp(pIrpSTATUS_INVALID_BUFFER_SIZE, 0);
 
  //PVOID buffer = pIrp->UserBuffer;
  PVOID buffer = pIrp->AssociatedIrp.SystemBuffer;
  if (!buffer)
   return CompleteIrp(pIrpSTATUS_INSUFFICIENT_RESOURCES, 0);
 
  /*__try
  {
   RtlCopyMemory(buffer, pBuffer, len);
   status = STATUS_SUCCESS;
  }
  __except (EXCEPTION_EXECUTE_HANDLER)
  {
   status = STATUS_ACCESS_VIOLATION;
  }*/
  RtlCopyMemory(buffer, pBuffer, len);
  status = STATUS_SUCCESS;
 }
 __finally
 {
  KeReleaseMutex(&mutex, FALSE);
  return CompleteIrp(pIrp, status, len);
 }
}

NTSTATUS RWDriverWrite(PDEVICE_OBJECT pDOPIRP pIrp)
{
 UNREFERENCED_PARAMETER(pDO);
 
 KeWaitForMutexObject(&mutex, ExecutiveKernelModeFALSENULL);
 NTSTATUS status = STATUS_INVALID_USER_BUFFER;
 PIO_STACK_LOCATION stack;
 ULONG len = 0;
 
 __try
 {
  stack = IoGetCurrentIrpStackLocation(pIrp);
  len = stack->Parameters.Write.Length;
  if (len == 0)
   return CompleteIrp(pIrpSTATUS_INVALID_BUFFER_SIZE, 0);
 
  //PVOID buffer = pIrp->UserBuffer;
  PVOID buffer = pIrp->AssociatedIrp.SystemBuffer;
  if (!buffer)
   return CompleteIrp(pIrpSTATUS_INSUFFICIENT_RESOURCES, 0);
 
  /*__try
  {
   RtlCopyMemory(pBuffer, buffer, len);
   status = STATUS_SUCCESS;
  }
  __except (EXCEPTION_EXECUTE_HANDLER)
  {
   status = STATUS_ACCESS_VIOLATION;
  }*/
  RtlCopyMemory(pBuffer, buffer, len);
  status = STATUS_SUCCESS;
 }
 __finally
 {
  KeReleaseMutex(&mutex, FALSE);
  return CompleteIrp(pIrp, status, len);
 }
}

Il blocco __try/__catch non serve più perché è stato l' I/O Manager a creare lo spazio per il buffer di sistema in kernel space e quindi avrà sempre un indirizzo valido. Il resto è esattamente uguale al codice della lezione precedente (si veda [2]) che non usava il supporto dell'I/O Manager. In particolare, il codice del client resta totalmente invariato.



DeviceIoControl

Per attivare il Buffered I/O è necessario specificare il valore METHOD_BUFFERED come terzo argomento della macro CTL_CODE

#define IOCTL_DICDRIVER_INC_VALUE CTL_CODE(DICDRIVER_DEVICE, 0x800, METHOD_BUFFEREDFILE_ANY_ACCESS)

Nella dispatch routine relativa all'indice IRP_MJ_DEVICE_CONTROL le uniche due modifiche riguardano il recupero dei buffer di input e output e la rimozione del blocco __try\__exception. In particolare, si noti che non si lavora più con due buffer separati: il buffer di sistema è trattato come di input fino alla prima scrittura, dopodiché si considera di output.

NTSTATUS DICDriverDeviceIoControl(PDEVICE_OBJECT pDOPIRP Irp)
{
 UNREFERENCED_PARAMETER(pDO);
 
 PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp);
 
 NTSTATUS status = STATUS_SUCCESS;
 ULONG len = 0;
 
 switch (stack->Parameters.DeviceIoControl.IoControlCode)
 {
 case IOCTL_DICDRIVER_INC_VALUE:
 {
  if (stack->Parameters.DeviceIoControl.InputBufferLength < sizeof(ULONG) ||
   (len = stack->Parameters.DeviceIoControl.OutputBufferLength) < sizeof(ULONG))
  {
   status = STATUS_BUFFER_TOO_SMALL;
   break;
  }
 
  //PULONG iBuffer = (PULONG)stack->Parameters.DeviceIoControl.Type3InputBuffer;
  PULONG iBuffer = (PULONG)Irp->AssociatedIrp.SystemBuffer;
  if (iBuffer == NULL) {
   status = STATUS_INVALID_PARAMETER;
   break;
  }
 
  /*PULONG oBuffer = (PULONG)Irp->UserBuffer;
  if (oBuffer == NULL) {
   status = STATUS_INVALID_PARAMETER;
   break;
  }*/
 
  /*__try
  {
   if (*iBuffer < (ULONG)(-1))
   {
    *oBuffer = ++(*iBuffer);
    break;
   }
   else
    status = STATUS_INVALID_USER_BUFFER;
  }
  __except (EXCEPTION_EXECUTE_HANDLER)
  {
   // something wrong with the buffer
   status = STATUS_ACCESS_VIOLATION;
  }*/
  if (*iBuffer < (ULONG)(-1))
  {
   ++(*iBuffer);
   break;
  }
  else
   status = STATUS_INVALID_USER_BUFFER;
 
  break;
 }
 default:
 {
  status = STATUS_INVALID_DEVICE_REQUEST;
  break;
 }
 }
 
 // Irp->IoStatus.Information ritornato da I/O Manager in lpBytesReturned fornito da client in DeviceIoControl
 return CompleteIrp(Irp, status, len);
}

Il resto è esattamente uguale al codice della lezione precedente (si veda [1]) che non usava il supporto dell'I/O Manager. In particolare, il codice del client resta totalmente invariato.



Codice sorgente:
RWBIODriver.zip
DICBIODriver.zip



Riferimenti:
[1] 07 - Comunicazione Client / Driver: DeviceIoControl
[2] 06 - Comunicazione Client \ Driver: ReadFile e WriteFile
[3] 01 - Cenni preliminari
[4] Windows Kernel Programming - Pavel Yosifovich

Nessun commento:

Posta un commento