28 votes

Pourquoi le compilateur génère-t-il une charge de 4 octets au lieu d'une charge de 1 octet où la charge plus large peut accéder aux données non mappées?

J'ai un tampon d'octets rempli avec des enregistrements de longueur variable, dont la durée est déterminée par le premier octet de l'enregistrement. Une version réduite d'une fonction C pour lire un enregistrement unique

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];
}

génère de l'assemblée (GCC 5.4 -O2 -fPIC sur x86_64) qui se charge de quatre octets au ptr tout d'abord, on compare le premier octet avec 0xC0, puis les processus, soit deux, soit quatre octets. L'indéfini octets sont jetés correctement, mais pourquoi ne compilateur pense qu'il est sûr de charger quatre octets en premier lieu? Comme il n'existe pas par exemple l'alignement exigence pour le ptr, il peut pointer vers les deux derniers octets de la mémoire d'une page qui est à côté de l'unmapped un pour tout ce que nous savons, résultant dans un accident.

Les deux -fPIC et-O2 ou plus sont nécessaires à la reproduction.

Suis-je manqué quelque chose? Compilateur est correct de faire cela et comment puis-je contourner ce problème?

Je peux obtenir les ci-dessus montrent que Valgrind/AddressSanitiser des erreurs ou d'une collision avec 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)

Avec 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)

Avec 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

TAS version avec 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)

Pile version avec 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)

Merci

EDIT: ajouté MMAP version qui en fait se bloque, a précisé options du compilateur

EDIT 2: signalé comme https://gcc.gnu.org/bugzilla/show_bug.cgi?id=77673. Pour cette solution, l'insertion d'un compilateur mémoire barrière asm volatile("": : :"memory"); après l' if déclaration résout le problème. Merci à tous!

2voto

chqrlie Points 17105

Félicitations! Vous avez trouvé un véritable compilateur bug!

Vous pouvez utiliser http://gcc.godbolt.org pour explorer l'assemblée de sortie à partir de différents compilateurs et les options.

Avec gcc version 6.2 pour les architectures x86 64 bits de linux, à l'aide de gcc -fPIC -O2, votre fonction n'est compiler en incorrecte code:

mach_parse_compressed(unsigned char*, unsigned long*):
    movzbl  (%rdi), %edx
    movl    (%rdi), %eax   ; potentially incorrect load of 4 bytes
    bswap   %eax
    cmpb    $-65, %dl
    jbe     .L5
    movl    %eax, %eax
    movq    %rax, (%rsi)
    ret
.L5:
    movzbl  1(%rdi), %eax
    addl    %eax, %edx
    movslq  %edx, %rdx
    movq    %rdx, (%rsi)
    ret

Vous avez correctement diagnostiqué le problème et l' mmap exemple fournit un bon test de régression. gcc est d'essayer trop dur pour optimiser cette fonction et le code résultant est certainement incorrect: la lecture de 4 octets à partir d'un non alignés adresse est OK pour la plupart des X86 environnements d'exploitation, mais la lecture de passé la fin d'un tableau n'est pas.

Le compilateur peut faire l'hypothèse que lit au-delà de la fin d'un tableau sont OK si ils ne passent pas un 32 bits ou même 64 bit de la frontière, mais cette hypothèse est incorrecte pour votre exemple. Vous pourriez être en mesure d'obtenir un crash d'un bloc alloué avec malloc , si vous le faites assez grand. malloc utilise mmap pour les très gros blocs (>= 128 KO par défaut de l'IRCC).

Notez que ce bug a été introduit avec la version 5.1 du compilateur.

clang d'autre part n'ont pas ce problème, mais le code semble moins efficace dans le cas général:

#    @mach_parse_compressed(unsigned char*, unsigned long*)
mach_parse_compressed(unsigned char*, unsigned long*):         
    movzbl  (%rdi), %ecx
    cmpq    $191, %rcx
    movzbl  1(%rdi), %eax
    ja      .LBB0_2
    addq    %rcx, %rax
    movq    %rax, (%rsi)
    retq
.LBB0_2:
    shlq    $24, %rcx
    shlq    $16, %rax
    orq     %rcx, %rax
    movzbl  2(%rdi), %ecx
    shlq    $8, %rcx
    orq     %rax, %rcx
    movzbl  3(%rdi), %eax
    orq     %rcx, %rax
    movq    %rax, (%rsi)
    retq

1voto

brovko Points 38

Il semble que le compilateur optimise l'accès à ptr. Il est possible de désactiver l'optimisation pour accéder à ptr en ajoutant simplement un mot clé volatile. Dans ce cas, il n'y a pas de plantage pour la variante MMAP.

 //#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(volatile 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 = (unsigned char *) 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);
}
 

1voto

barak manos Points 10969

Sur certaines architectures (par exemple STM32), 4 octets de charge/store opération est appliquée sur les 4 octets segment dans lequel l'opérande est "situé".

Par exemple, un 4 octets de la charge à partir de l'adresse 0x80000003 sera appliquée sur 0x80000000.

En outre, le bus mémoire des cartes d'un espace d'adressage qui commence à un 4 octets adresse alignée et contient un nombre entier de 4 octets segments.

Par exemple, l'adresse de l'espace commence à 0 (inclus) et se termine à 0x80000000 (exclusif).

Maintenant, supposons que nous prenons de l'architecture, et de configurer le bus pour permettre la lecture (de la charge) sur l'ensemble de l'espace d'adressage.

Par la suite, un de 4 octets de charge de l'opération sera effectuée avec succès (sans provoquer d'erreur de bus) n'importe où dans le cadre de l'espace d'adressage.


Ceci dit, ce n'est pas le cas sur x86/x64 pour autant que je suis conscient de...

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X