Sé que el OP ya aceptó una de las respuestas, pero desafortunadamente no explica por qué MAP_GROWSDOWN
parece funcionar a veces. Dado que esta pregunta de desbordamiento de pila es uno de los primeros resultados en los motores de búsqueda, permítanme agregar mi respuesta para otras.
La documentación de MAP_GROWSDOWN
necesita actualización. En particular:
Este crecimiento se puede repetir hasta que el mapeo crezca dentro de una página del extremo superior del siguiente mapeo inferior, momento en el que tocar la página de "protección" dará como resultado una señal SIGSEGV.
En realidad, el kernel no permite un MAP_GROWSDOWN
mapeo para acercarse a stack_guard_gap
páginas lejos del mapeo anterior. El valor predeterminado es 256, pero se puede anular en la línea de comando del kernel. Dado que su código no especifica ninguna dirección deseada para la asignación, el kernel elige una automáticamente, pero es muy probable que termine dentro de las 256 páginas desde el final de una asignación existente.
EDITAR :
Además, los núcleos anteriores a v5.0 niegan el acceso a una dirección que está más de 64k+256 bytes por debajo del puntero de la pila. Consulte esta confirmación del núcleo para obtener más detalles.
Este programa funciona en x86 incluso con kernels anteriores a 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;
}
Reemplazar:
volatile char *c_ptr_1 = mapped_ptr - 4096; //1 page below
Con
volatile char *c_ptr_1 = mapped_ptr;
porque:
La dirección de retorno es una página más baja que el área de memoria que realmente se crea en el espacio de direcciones virtuales del proceso. Tocar una dirección en la página de "protección" debajo del mapeo hará que el mapeo crezca una página.
Tenga en cuenta que probé la solución y funciona como se esperaba en el kernel 4.15.0-45-generic.
En primer lugar, no quieres MAP_GROWSDOWN
, y no es así como funciona la pila de subprocesos principal. Analizando el mapeo de memoria de un proceso con pmap. [stack] Nada lo usa, y prácticamente nada debería úsalo Las cosas en la página de manual que dicen que "se usa para pilas" son incorrectas y deben corregirse.
Sospecho que podría tener errores (porque nada lo usa, por lo que a nadie le importa o incluso se da cuenta si se rompe).
Tu código me funciona si cambio el mmap
llame al mapa más de 1 página. Específicamente, probé 4096 * 100
. Estoy ejecutando Linux 5.0.1 (Arch Linux) en bare metal (Skylake).
/proc/PID/smaps
muestra un gd
bandera.
Y luego (al hacer un solo paso en el asm) el maps
la entrada en realidad cambia a una dirección de inicio más baja pero la misma dirección final, por lo que literalmente crece hacia abajo cuando empiezo con un mapeo de 400k. Esto da una asignación inicial de 400k superior la dirección de retorno, que crece a 404 kiB cuando se ejecuta el programa. (El tamaño de un _GROWSDOWN
el mapeo es no el límite de crecimiento o algo por el estilo.)
https://bugs.centos.org/view.php?id=4767 puede estar relacionado; algo cambió entre las versiones del kernel en CentOS 5.3 y 5.5. Y/o tuvo algo que ver con trabajar en una máquina virtual (5.3) frente a no crecer y fallar en el metal desnudo (5.5).
Simplifiqué la C para usar ptr[-4095]
etc:
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
}
Compilando con gcc -Og
da asm que es agradable a un solo paso.
Por cierto, varios rumores sobre la eliminación de la bandera de glibc son obviamente erróneos. Esta fuente se compila, y está claro que también es compatible con el kernel, no se ignora en silencio. (Aunque el comportamiento que veo con el tamaño 4096 en lugar de 400 kiB es exactamente consistente con la bandera que se ignora silenciosamente. Sin embargo, el gd
VmFlag todavía está allí en smaps
, por lo que no se ignora en esa etapa).
Revisé y había espacio para que creciera sin acercarse a otro mapeo. Entonces, IDK por qué no creció cuando el mapeo GD era solo 1 página. Lo intenté un par de veces y fallaba cada vez. Con el mapeo inicial más grande, nunca falló.
En ambas ocasiones se almacenó el valor de retorno de mmap (la primera página de la asignación propiamente dicha), luego se almacenó 4095 bytes por debajo de eso.