Wenn der Heap aus Sicherheitsgründen auf Null gesetzt wurde, warum ist der Stack dann nur nicht initialisiert?

15

Auf meinem Debian GNU / Linux 9-System, wenn eine Binärdatei ausgeführt wird,

  • Der Stack ist aber nicht initialisiert
  • Der Heap wird auf Null gesetzt.

Warum?

Ich gehe davon aus, dass die Nullinitialisierung die Sicherheit fördert, aber wenn für den Heap, warum dann nicht auch für den Stack? Braucht der Stack auch keine Sicherheit?

Meine Frage ist meines Wissens nicht Debian-spezifisch.

Beispiel-C-Code:

#include <stddef.h>
#include <stdlib.h>
#include <stdio.h>

const size_t n = 8;

// --------------------------------------------------------------------
// UNINTERESTING CODE
// --------------------------------------------------------------------
static void print_array(
  const int *const p, const size_t size, const char *const name
)
{
    printf("%s at %p: ", name, p);
    for (size_t i = 0; i < size; ++i) printf("%d ", p[i]);
    printf("\n");
}

// --------------------------------------------------------------------
// INTERESTING CODE
// --------------------------------------------------------------------
int main()
{
    int a[n];
    int *const b = malloc(n*sizeof(int));
    print_array(a, n, "a");
    print_array(b, n, "b");
    free(b);
    return 0;
}

Ausgabe:

a at 0x7ffe118997e0: 194 0 294230047 32766 294230046 32766 -550453275 32713 
b at 0x561d4bbfe010: 0 0 0 0 0 0 0 0 

Der C-Standard fordert natürlich nicht malloc()zum Löschen des Speichers auf, bevor er zugewiesen wird, sondern mein C-Programm dient lediglich der Veranschaulichung. Die Frage ist keine Frage zu C oder zur Standardbibliothek von C. Die Frage ist vielmehr eine Frage, warum der Kernel und / oder der Laufzeit-Loader den Heap auf Null setzen, nicht aber den Stack.

Ein weiteres Experiment

Meine Frage bezieht sich eher auf ein beobachtbares GNU / Linux-Verhalten als auf die Anforderungen von Standarddokumenten. Wenn Sie sich nicht sicher sind, was ich meine, versuchen Sie diesen Code, der weiteres undefiniertes Verhalten aufruft ( undefiniert, das heißt, soweit der C-Standard betroffen ist), um den Punkt zu veranschaulichen:

#include <stddef.h>
#include <stdlib.h>
#include <stdio.h>

const size_t n = 4;

int main()
{
    for (size_t i = n; i; --i) {
        int *const p = malloc(sizeof(int));
        printf("%p %d ", p, *p);
        ++*p;
        printf("%d\n", *p);
        free(p);
    }
    return 0;
}

Ausgabe von meiner Maschine:

0x555e86696010 0 1
0x555e86696010 0 1
0x555e86696010 0 1
0x555e86696010 0 1

In Bezug auf den C-Standard ist das Verhalten undefiniert, sodass meine Frage den C-Standard nicht berücksichtigt. Ein Aufruf, der malloc()nicht jedes Mal dieselbe Adresse zurückgeben muss, aber da dieser Aufruf malloc()tatsächlich jedes Mal dieselbe Adresse zurückgibt, ist es interessant zu bemerken, dass der Speicher, der sich auf dem Heap befindet, jedes Mal auf Null gesetzt wird.

Im Gegensatz dazu schien der Stapel nicht auf Null gestellt zu sein.

Ich weiß nicht, was der letztere Code auf Ihrem Computer bewirken wird, da ich nicht weiß, welche Schicht des GNU / Linux-Systems das beobachtete Verhalten verursacht. Sie können es aber versuchen.

AKTUALISIEREN

@ Kusalananda hat in Kommentaren beobachtet:

Ihr neuester Code gibt verschiedene Adressen und (gelegentlich) nicht initialisierte (von Null verschiedene) Daten zurück, wenn er unter OpenBSD ausgeführt wird. Dies sagt offensichtlich nichts über das Verhalten aus, das Sie unter Linux beobachten.

Dass sich mein Ergebnis von dem unter OpenBSD unterscheidet, ist in der Tat interessant. Anscheinend entdeckten meine Experimente kein Kernel- (oder Linker-) Sicherheitsprotokoll, wie ich gedacht hatte, sondern nur ein Implementierungsartefakt.

In diesem Licht glaube ich, dass die folgenden Antworten von @mosvy, @StephenKitt und @AndreasGrapentin zusammen meine Frage regeln.

Siehe auch Stack Overflow: Warum initialisiert malloc die Werte in gcc auf 0? (Kredit: @bta).

thb
quelle
2
Ihr neuester Code gibt verschiedene Adressen und (gelegentlich) nicht initialisierte (von Null verschiedene) Daten zurück, wenn er unter OpenBSD ausgeführt wird. Dies gilt natürlich nicht sagen nichts über das Verhalten , dass Sie auf Linux erleben.
Kusalananda
Bitte ändern Sie den Umfang Ihrer Frage nicht und versuchen Sie nicht, sie zu bearbeiten, um Antworten und Kommentare überflüssig zu machen. In C ist der "Heap" nichts anderes als der von malloc () und calloc () zurückgegebene Speicher, und nur dieser löscht den Speicher auf Null; der newOperator in C ++ (auch "Heap") ist unter Linux nur ein Wrapper für malloc (); Dem Kernel ist es egal, was der "Haufen" ist.
Mosvy
3
In Ihrem zweiten Beispiel wird einfach ein Artefakt der Malloc-Implementierung in glibc verfügbar gemacht. Wenn Sie dies wiederholt mit einem Puffer von mehr als 8 Bytes tun, werden Sie deutlich sehen, dass nur die ersten 8 Bytes auf Null gesetzt werden.
Mosvy
@ Kusalananda Ich verstehe. Dass sich mein Ergebnis von dem unter OpenBSD unterscheidet, ist in der Tat interessant. Anscheinend haben Sie und Mosvy gezeigt, dass meine Experimente nicht, wie ich gedacht hatte, ein Kernel- (oder Linker-) Sicherheitsprotokoll, sondern lediglich ein Implementierungsartefakt enthielten.
28.
@thb Ich glaube, dass dies eine korrekte Beobachtung sein kann, ja.
Kusalananda

Antworten:

28

Der von malloc () zurückgegebene Speicher ist nicht null-initialisiert. Nimm niemals an, dass es so ist.

In Ihrem Testprogramm ist es nur ein Zufall: Ich denke, die haben malloc()gerade einen neuen Block entfernt mmap(), aber verlassen Sie sich auch nicht darauf.

Wenn ich zum Beispiel Ihr Programm auf meinem Computer so ausführe:

$ echo 'void __attribute__((constructor)) p(void){
    void *b = malloc(4444); memset(b, 4, 4444); free(b);
}' | cc -include stdlib.h -include string.h -xc - -shared -o pollute.so

$ LD_PRELOAD=./pollute.so ./your_program
a at 0x7ffd40d3aa60: 1256994848 21891 1256994464 21891 1087613792 32765 0 0
b at 0x55834c75d010: 67372036 67372036 67372036 67372036 67372036 67372036 67372036 67372036

Ihr zweites Beispiel besteht einfach darin, ein Artefakt der mallocImplementierung in glibc verfügbar zu machen. wenn man das wiederholt tun malloc/ freemit einem Puffer größer als 8 Bytes, werden Sie deutlich sehen , dass nur das erste 8 Bytes auf Null gesetzt werden, wie in dem folgenden Beispielcode.

#include <stddef.h>
#include <stdlib.h>
#include <stdio.h>

const size_t n = 4;
const size_t m = 0x10;

int main()
{
    for (size_t i = n; i; --i) {
        int *const p = malloc(m*sizeof(int));
        printf("%p ", p);
        for (size_t j = 0; j < m; ++j) {
            printf("%d:", p[j]);
            ++p[j];
            printf("%d ", p[j]);
        }
        free(p);
        printf("\n");
    }
    return 0;
}

Ausgabe:

0x55be12864010 0:1 0:1 0:1 0:1 0:1 0:1 0:1 0:1 0:1 0:1 0:1 0:1 0:1 0:1 0:1 0:1 
0x55be12864010 0:1 0:1 1:2 1:2 1:2 1:2 1:2 1:2 1:2 1:2 1:2 1:2 1:2 1:2 1:2 1:2 
0x55be12864010 0:1 0:1 2:3 2:3 2:3 2:3 2:3 2:3 2:3 2:3 2:3 2:3 2:3 2:3 2:3 2:3 
0x55be12864010 0:1 0:1 3:4 3:4 3:4 3:4 3:4 3:4 3:4 3:4 3:4 3:4 3:4 3:4 3:4 3:4
Mosvy
quelle
2
Nun ja, aber aus diesem Grund habe ich die Frage hier gestellt und nicht über Stack Overflow. Meine Frage betraf nicht den C-Standard, sondern die Art und Weise, wie moderne GNU / Linux-Systeme normalerweise Binärdateien verknüpfen und laden. Ihr LD_PRELOAD ist humorvoll, beantwortet aber eine andere Frage als die Frage, die ich stellen wollte.
28.
19
Ich bin froh, dass ich dich zum Lachen gebracht habe, aber deine Annahmen und Vorurteile sind überhaupt nicht lustig. Auf einem "modernen GNU / Linux-System" werden Binärdateien normalerweise von einem dynamischen Linker geladen, der Konstruktoren aus dynamischen Bibliotheken ausführt, bevor Sie von Ihrem Programm zur main () -Funktion gelangen. Auf Ihrem Debian GNU / Linux 9-System werden sowohl malloc () als auch free () mehrmals aufgerufen, bevor die main () -Funktion von Ihrem Programm ausgeführt wird, auch wenn keine vorinstallierten Bibliotheken verwendet werden.
Mosvy
23

Unabhängig davon, wie der Stapel initialisiert wird, sehen Sie keinen unberührten Stapel, da die C-Bibliothek eine Reihe von Vorgängen ausführt, bevor sie aufruft main, und sie berühren den Stapel.

Bei der GNU C-Bibliothek unter x86-64 beginnt die Ausführung am _start- Einstiegspunkt, der __libc_start_mainzum Einrichten der Dinge aufruft , und letzterer endet mit dem Aufrufen main. Vor dem Aufruf mainwerden jedoch eine Reihe anderer Funktionen aufgerufen, wodurch verschiedene Daten in den Stapel geschrieben werden. Der Inhalt des Stapels wird zwischen den Funktionsaufrufen nicht gelöscht. Wenn Sie in mainden Stapel eintreten, enthält der Stapel also Reste der vorherigen Funktionsaufrufe.

Dies erklärt nur die Ergebnisse, die Sie vom Stapel erhalten, und zeigt die anderen Antworten zu Ihrer allgemeinen Vorgehensweise und Ihren Annahmen.

Stephen Kitt
quelle
Beachten Sie, dass bei main()Aufruf der Zeit Initialisierungsroutinen möglicherweise den von zurückgegebenen modifizierten Speicher geändert haben malloc()- insbesondere, wenn C ++ - Bibliotheken eingebunden sind. Angenommen, der "Heap" ist auf irgendetwas initialisiert, ist eine wirklich, wirklich schlechte Annahme.
Andrew Henle
Ihre Antwort zusammen mit der der Mosvy regelt meine Frage. Das System erlaubt mir leider , nur einen der beiden zu akzeptieren ; ansonsten würde ich beides akzeptieren.
28.
18

In beiden Fällen erhalten Sie nicht initialisierten Speicher, und Sie können keine Annahmen über dessen Inhalt treffen.

Wenn das Betriebssystem Ihrem Prozess eine neue Seite zuweisen muss (sei es für den Stapel oder für die von verwendete Arena malloc()), stellt es sicher, dass keine Daten von anderen Prozessen verfügbar gemacht werden. Der übliche Weg, dies sicherzustellen, besteht darin, es mit Nullen zu füllen (es ist jedoch auch möglich, es mit etwas anderem zu überschreiben, einschließlich einer Seite im Wert von /dev/urandom- tatsächlich malloc()schreiben einige Debug- Implementierungen Muster ungleich Null, um fehlerhafte Annahmen wie Ihre zu erfassen).

Wenn malloc()die Anforderung aus dem Speicher, der bereits verwendet und durch diesen Prozess freigegeben wurde, erfüllt werden kann, wird der Inhalt nicht gelöscht (in der Tat hat das Löschen nichts damit zu tun malloc()und kann es nicht sein - es muss geschehen, bevor der Speicher zugeordnet wird Ihr Adressraum). Möglicherweise erhalten Sie Speicher, der zuvor von Ihrem Prozess / Programm geschrieben wurde (z main(). B. zuvor ).

In Ihrem Beispielprogramm sehen Sie eine malloc()Region, die durch diesen Prozess noch nicht geschrieben wurde (dh direkt von einer neuen Seite), und einen Stapel, in den geschrieben wurde (durch Vorcode main()in Ihrem Programm). Wenn Sie mehr von dem Stapel untersuchen, werden Sie feststellen, dass er weiter unten (in seiner Wachstumsrichtung) mit Null gefüllt ist.

Wenn Sie wirklich verstehen wollen , was auf OS - Ebene geschieht, empfehle ich Ihnen , Bypass - C - Bibliothek Schicht und interact System ruft wie brk()und mmap()stattdessen.

Toby Speight
quelle
1
Vor ein oder zwei Wochen habe ich ein anderes Experiment versucht, malloc()und zwar free()wiederholt. Obwohl es nicht erforderlich ist malloc(), denselben kürzlich freigegebenen Speicher erneut zu verwenden, wurde dies im Experiment malloc()tatsächlich durchgeführt. Es gab jedes Mal dieselbe Adresse zurück, löschte aber auch jedes Mal den Speicher, was ich nicht erwartet hatte. Das war interessant für mich. Weitere Experimente haben zur heutigen Frage geführt.
28.
1
@thb, Vielleicht bin ich mir nicht klar genug - die meisten Implementierungen malloc()machen absolut nichts mit dem Speicher, den sie Ihnen zur Verfügung stellen - es ist entweder zuvor verwendet oder frisch zugewiesen (und daher vom Betriebssystem auf Null gesetzt). In Ihrem Test haben Sie offensichtlich Letzteres erhalten. In ähnlicher Weise wird der Stapelspeicher im gelöschten Zustand an Ihren Prozess übergeben, aber Sie untersuchen ihn nicht weit genug, um festzustellen, welche Teile Ihr Prozess noch nicht berührt hat. Ihr Stapelspeicher wird gelöscht, bevor er Ihrem Prozess übergeben wird.
Toby Speight
2
@TobySpeight: brk und sbrk werden durch mmap ersetzt. pubs.opengroup.org/onlinepubs/7908799/xsh/brk.html sagt LEGACY ganz oben.
Joshua
2
Wenn Sie initialisierten Speicher benötigen, callockann dies eine Option sein (anstelle von memset)
siehe
2
@thb und Toby: Unterhaltsame Tatsache: Neue Seiten aus dem Kernel werden häufig träge zugewiesen und lediglich beim Schreiben kopiert und auf eine gemeinsam genutzte Seite mit Nullen abgebildet. Dies geschieht, mmap(MAP_ANONYMOUS)sofern Sie dies nicht ebenfalls verwenden MAP_POPULATE. Es ist zu hoffen, dass neue Stapelseiten durch neue physische Seiten gesichert und verkabelt werden (in den Hardwareseitentabellen sowie in der Liste der Zuordnungen für Zeiger und Länge des Kernels), wenn sie größer werden, da normalerweise beim ersten Berühren neuer Stapelspeicher geschrieben wird . Aber ja, der Kernel muss verhindern, dass Daten verloren gehen, und das Nullsetzen ist am billigsten und nützlichsten.
Peter Cordes
9

Ihre Prämisse ist falsch.

Was Sie als "Sicherheit" bezeichnen, ist Vertraulichkeit. Dies bedeutet, dass kein Prozess den Speicher eines anderen Prozesses lesen kann, es sei denn, dieser Speicher wird ausdrücklich von diesen Prozessen gemeinsam genutzt. In einem Betriebssystem ist dies ein Aspekt der Isolierung von gleichzeitigen Aktivitäten oder Prozessen.

Was das Betriebssystem tut, um diese Isolation sicherzustellen, ist, wann immer Speicher vom Prozess für Heap- oder Stapelzuweisungen angefordert wird, dieser Speicher kommt entweder aus einem Bereich im physischen Speicher, der mit Nullen gefüllt ist, oder der mit Junk gefüllt ist aus dem gleichen Prozess kommen .

Dies stellt sicher, dass Sie immer nur Nullen oder Ihren eigenen Müll sehen, sodass die Vertraulichkeit gewährleistet ist und sowohl der Heap als auch der Stack "sicher" sind, wenn auch nicht unbedingt (null-) initialisiert.

Sie lesen zu viel in Ihre Messungen.

Andreas Grapentin
quelle
1
Der Abschnitt "Aktualisieren" der Frage verweist jetzt explizit auf Ihre aufschlussreiche Antwort.
29.