Perché la mappatura MAP_GROWSDOWN non cresce?

Perché la mappatura MAP_GROWSDOWN non cresce?

So che l'OP ha già accettato una delle risposte, ma sfortunatamente non spiega perché MAP_GROWSDOWN sembra funzionare a volte. Poiché questa domanda sull'overflow dello stack è uno dei primi risultati nei motori di ricerca, consentitemi di aggiungere la mia risposta per gli altri.

La documentazione di MAP_GROWSDOWN necessita di aggiornamento. In particolare:

In realtà, il kernel non consente un MAP_GROWSDOWN mappatura per avvicinarsi a stack_guard_gap pagine distanti dalla mappatura precedente. Il valore predefinito è 256, ma può essere sovrascritto dalla riga di comando del kernel. Poiché il tuo codice non specifica alcun indirizzo desiderato per la mappatura, il kernel ne sceglie uno automaticamente, ma è molto probabile che finisca entro 256 pagine dalla fine di una mappatura esistente.

MODIFICA :

Inoltre, i kernel precedenti alla v5.0 negano l'accesso a un indirizzo che è più di 64k+256 byte sotto il puntatore dello stack. Vedi questo commit del kernel per i dettagli.

Questo programma funziona su x86 anche con kernel precedenti alla 5.0:

#include <sys/mman.h>
#include <stdint.h>
#include <stdio.h>

#define PAGE_SIZE   4096UL
#define GAP     512 * PAGE_SIZE

static void print_maps(void)
{
    FILE *f = fopen("/proc/self/maps", "r");
    if (f) {
        char buf[1024];
        size_t sz;
        while ( (sz = fread(buf, 1, sizeof buf, f)) > 0)
            fwrite(buf, 1, sz, stdout);
        fclose(f);
    }
}

int main()
{
    char *p;
    void *stack_ptr;

    /* Choose an address well below the default process stack. */
    asm volatile ("mov  %%rsp,%[sp]"
        : [sp] "=g" (stack_ptr));
    stack_ptr -= (intptr_t)stack_ptr & (PAGE_SIZE - 1);
    stack_ptr -= GAP;
    printf("Ask for a page at %p\n", stack_ptr);
    p = mmap(stack_ptr, PAGE_SIZE, PROT_READ | PROT_WRITE,
         MAP_PRIVATE | MAP_STACK | MAP_ANONYMOUS | MAP_GROWSDOWN,
         -1, 0);
    printf("Mapped at %p\n", p);
    print_maps();
    getchar();

    /* One page is already mapped: stack pointer does not matter. */
    *p = 'A';
    printf("Set content of that page to \"%s\"\n", p);
    print_maps();
    getchar();

    /* Expand down by one page. */
    asm volatile (
        "mov  %%rsp,%[sp]"  "\n\t"
        "mov  %[ptr],%%rsp" "\n\t"
        "movb $'B',-1(%%rsp)"   "\n\t"
        "mov  %[sp],%%rsp"
        : [sp] "+&g" (stack_ptr)
        : [ptr] "g" (p)
        : "memory");
    printf("Set end of guard page to \"%s\"\n", p - 1);
    print_maps();
    getchar();

    return 0;
}

Sostituisci:

volatile char *c_ptr_1 = mapped_ptr - 4096; //1 page below

Con

volatile char *c_ptr_1 = mapped_ptr;

Perché:

Nota che ho testato la soluzione e funziona come previsto sul kernel 4.15.0-45-generico.


Prima di tutto, non vuoi MAP_GROWSDOWN , e non è così che funziona lo stack di thread principale. Analisi della mappatura della memoria di un processo con pmap. [stack] Niente lo usa, e praticamente niente dovrebbe usalo. Le cose nella pagina man che dicono che è "usato per gli stack" sono sbagliate e dovrebbero essere corrette.

Sospetto che potrebbe essere difettoso (perché nulla lo usa, quindi di solito a nessuno importa o addirittura si accorge se si rompe.)

Il tuo codice funziona per me se cambio il mmap chiama per mappare più di 1 pagina. In particolare, ho provato 4096 * 100 . Sto eseguendo Linux 5.0.1 (Arch Linux) su bare metal (Skylake).

/proc/PID/smaps mostra un gd bandiera.

E poi (quando si esegue un singolo passaggio dell'asm) il maps la voce in realtà cambia in un indirizzo iniziale più basso ma lo stesso indirizzo finale, quindi sta letteralmente crescendo verso il basso quando inizio con una mappatura di 400k. Ciò fornisce un'allocazione iniziale di 400.000 sopra l'indirizzo di ritorno, che cresce fino a 404 kiB durante l'esecuzione del programma. (La dimensione per un _GROWSDOWN la mappatura non il limite di crescita o qualcosa del genere.)

https://bugs.centos.org/view.php?id=4767 potrebbe essere correlato; qualcosa è cambiato tra le versioni del kernel in CentOS 5.3 e 5.5. E/o aveva qualcosa a che fare con il lavoro in una VM (5.3) rispetto a non crescere e fare errori su bare metal (5.5).

Ho semplificato la C per usare ptr[-4095] ecc:

int main(void){
    volatile char *ptr = mmap(NULL, 4096*100,
                            PROT_READ | PROT_WRITE,
                            MAP_ANONYMOUS | MAP_PRIVATE | MAP_STACK | MAP_GROWSDOWN,
                            -1, 0);
    if(ptr == MAP_FAILED){
        int error_code = errno;
        fprintf(stderr, "Cannot do MAP_FIXED mapping."
                        "Error code = %d, details = %s\n", error_code, strerror(error_code));
                        exit(EXIT_FAILURE);
    }

    ptr[0] = 'a';      //address returned by mmap
    ptr[-4095] = 'b';  // grow by 1 page
}

Compilazione con gcc -Og dà un asm che è carino a un solo passaggio.

A proposito, varie voci sulla rimozione della bandiera da glibc sono ovviamente sbagliate. Questo sorgente viene compilato ed è chiaro che è supportato anche dal kernel, non ignorato silenziosamente. (Anche se il comportamento che vedo con la dimensione 4096 invece di 400 kiB è esattamente coerente con il flag ignorato silenziosamente. Tuttavia il gd VmFlag è ancora presente in smaps , quindi non viene ignorato in quella fase.)

Ho controllato e c'era spazio per farlo crescere senza avvicinarsi a un'altra mappatura. Quindi IDK perché non è cresciuto quando la mappatura GD era solo 1 pagina. Ho provato un paio di volte e ogni volta si è verificato un segfault. Con la mappatura iniziale più grande non ha mai sbagliato.

Entrambe le volte erano con un negozio al valore restituito mmap (la prima pagina della mappatura vera e propria), quindi un negozio 4095 byte al di sotto di quello.