Ich habe einen Byte-Puffer gefüllt mit Datensätzen variabler Länge, deren Länge durch das erste Byte des Datensatzes bestimmt wird. Eine reduzierte Version einer C Funktion einen einzelnen Datensatz zu lesenWarum erzeugt der Compiler 4-Byte-Ladevorgänge statt 1-Byte-Ladevorgänge, bei denen die breitere Last auf nicht zugeordnete Daten zugreifen kann?
void mach_parse_compressed(unsigned char* ptr, unsigned long int* val)
{
if (ptr[0] < 0xC0U) {
*val = ptr[0] + ptr[1];
return;
}
*val = ((unsigned long int)(ptr[0]) << 24)
| ((unsigned long int)(ptr[1]) << 16)
| ((unsigned long int)(ptr[2]) << 8)
| ptr[3];
}
erzeugt Baugruppe (GCC 5.4 -O2 -fPIC auf x86_64), die ersten vier Bytes bei PTR lädt, vergleicht das erste Byte mit 0xC0 und verarbeitet dann entweder zwei, entweder vier Bytes. Die undefinierten Bytes werden korrekt verworfen, aber warum denkt Compiler, dass es sicher ist, vier Byte an erster Stelle zu laden? Da es keine z.B. Ausrichtungsanforderung für ptr, es kann auf die letzten zwei Bytes einer Speicherseite zeigen, die neben einem nicht zugeordneten alle bekannten ist, was zu einem Absturz führt.
Sowohl -fPIC als auch -O2 oder höher müssen reproduziert werden.
Fehle ich hier etwas? Ist der Compiler korrekt und wie kann ich umgehen?
kann ich die oben zeigen Valgrind/AddressSanitiser Fehler erhalten oder einen Absturz mit mmap/mprotect:
//#define HEAP
#define MMAP
#ifdef MMAP
#include <unistd.h>
#include <sys/mman.h>
#include <stdio.h>
#elif HEAP
#include <stdlib.h>
#endif
void
mach_parse_compressed(unsigned char* ptr, unsigned long int* val)
{
if (ptr[0] < 0xC0U) {
*val = ptr[0] + ptr[1];
return;
}
*val = ((unsigned long int)(ptr[0]) << 24)
| ((unsigned long int)(ptr[1]) << 16)
| ((unsigned long int)(ptr[2]) << 8)
| ptr[3];
}
int main(void)
{
unsigned long int val;
#ifdef MMAP
int error;
long page_size = sysconf(_SC_PAGESIZE);
unsigned char *buf = mmap(NULL, page_size * 2, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
unsigned char *ptr = buf + page_size - 2;
if (buf == MAP_FAILED)
{
perror("mmap");
return 1;
}
error = mprotect(buf + page_size, page_size, PROT_NONE);
if (error != 0)
{
perror("mprotect");
return 2;
}
*ptr = 0xBF;
*(ptr + 1) = 0x10;
mach_parse_compressed(ptr, &val);
#elif HEAP
unsigned char *buf = malloc(16384);
unsigned char *ptr = buf + 16382;
buf[16382] = 0xBF;
buf[16383] = 0x10;
#else
unsigned char buf[2];
unsigned char *ptr = buf;
buf[0] = 0xBF;
buf[1] = 0x10;
#endif
mach_parse_compressed(ptr, &val);
}
MMAP Version:
Segmentation fault (core dumped)
Mit Valgrind:
==3540== Process terminating with default action of signal 11 (SIGSEGV)
==3540== Bad permissions for mapped region at address 0x4029000
==3540== at 0x400740: mach_parse_compressed (in /home/laurynas/gcc-too-wide-load/gcc-too-wide-load)
==3540== by 0x40060A: main (in /home/laurynas/gcc-too-wide-load/gcc-too-wide-load)
Mit ASan:
ASAN:SIGSEGV
=================================================================
==3548==ERROR: AddressSanitizer: SEGV on unknown address 0x7f8f4dc25000 (pc 0x000000400d8a bp 0x0fff884e56c6 sp 0x7ffc4272b620 T0)
#0 0x400d89 in mach_parse_compressed (/home/laurynas/gcc-too-wide-load/gcc-too-wide-load+0x400d89)
#1 0x400b92 in main (/home/laurynas/gcc-too-wide-load/gcc-too-wide-load+0x400b92)
#2 0x7f8f4c72082f in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2082f)
#3 0x400c58 in _start (/home/laurynas/gcc-too-wide-load/gcc-too-wide-load+0x400c58)
AddressSanitizer can not provide additional info.
SUMMARY: AddressSanitizer: SEGV ??:0 mach_parse_compressed
HEAP Version mit Valgrind:
==30498== Invalid read of size 4
==30498== at 0x400603: mach_parse_compressed (mach0data_reduced.c:9)
==30498== by 0x4004DE: main (mach0data_reduced.c:34)
==30498== Address 0x520703e is 16,382 bytes inside a block of size 16,384 alloc'd
==30498== at 0x4C2DB8F: malloc (vg_replace_malloc.c:299)
==30498== by 0x4004C0: main (mach0data_reduced.c:24)
Stack-Version mit Asan:
==30528==ERROR: AddressSanitizer: stack-buffer-overflow on address
0x7ffd50000440 at pc 0x000000400b63 bp 0x7ffd500003c0 sp
0x7ffd500003b0
READ of size 4 at 0x7ffd50000440 thread T0
#0 0x400b62 in mach_parse_compressed
CMakeFiles/innobase.dir/mach/mach0data_reduced.c:15
#1 0x40087e in main CMakeFiles/innobase.dir/mach/mach0data_reduced.c:34
#2 0x7f3be2ce282f in __libc_start_main
(/lib/x86_64-linux-gnu/libc.so.6+0x2082f)
#3 0x400948 in _start
(/home/laurynas/obj-percona-5.5-release/storage/innobase/CMakeFiles/innobase.dir/mach/mach0data_test+0x400948)
Dank
EDIT: hinzugefügt MMAP-Version, die tatsächlich abstürzt, geklärte Compiler-Optionen
EDIT 2: meldete es als https://gcc.gnu.org/bugzilla/show_bug.cgi?id=77673. Um das Problem zu umgehen, löst das Einfügen eines Compiler-Speicherbarrieren asm volatile("": : :"memory");
nach der if
-Anweisung das Problem. Danke allen!
Klingt wie ein Compiler-Fehler. Vielleicht möchten Sie einen Fehlerbericht einreichen. –
Möglich, aber wollte hier zuerst mit Sprache Anwälte/Compiler-Experten überprüfen, zu oft ein offensichtlicher Compiler Bug ist Benutzerfehler –
Der Compiler könnte wissen, dass diese 4-Byte-Last nie einen Absturz auf der Zielarchitektur (Valgrind-Bericht ungeachtet). Wenn Sie ein Beispiel erstellen könnten, das tatsächlich abstürzt, würde dies den Fall für einen Compiler-Fehler stärken. –