Yığın güvenlik için sıfır olarak başlatılırsa, yığın neden yalnızca başlatılmaz?


15

Debian GNU / Linux 9 sistemimde, bir ikili yürütüldüğünde,

  • yığın başlatılmadı ancak
  • yığın sıfır başlatılır.

Neden?

Sıfır başlatmanın güvenliği desteklediğini varsayıyorum, ancak yığın için ise neden yığın için olmasın? Yığın da güvenliğe ihtiyacı yok mu?

Sorum bildiğim kadarıyla Debian'a özgü değil.

Örnek C kodu:

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

Çıktı:

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

C standardı malloc()elbette ayırmadan önce hafızayı temizlemeyi istemez , ancak C programım sadece gösterim amaçlıdır. Soru, C veya C'nin standart kütüphanesi hakkında bir soru değildir. Daha ziyade soru, çekirdek ve / veya çalışma zamanı yükleyicisinin neden yığını değil, yığını sıfırladığı sorusudur.

BAŞKA BİR DENEY

Sorum standart belgelerin gerekliliklerinden ziyade gözlemlenebilir GNU / Linux davranışı ile ilgili. Ne demek istediğimden emin değilseniz, daha sonra tanımlanamayan davranışları ( tanımlanmamış, yani C standardı söz konusu olduğunda) çağıran bu kodu deneyin :

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

Makinemden çıktı:

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

C standardı ile ilgili olarak, davranış tanımsızdır, bu yüzden sorum C standardını dikkate almaz. malloc()Her seferinde aynı adresi döndürmesi gerekmeyen bir çağrı, ancak bu çağrı, malloc()her seferinde aynı adresi döndürdüğü için , yığıntaki belleğin her seferinde sıfırlandığını fark etmek ilginçtir .

Öte yandan, yığın sıfırlanmış gibi görünmüyordu.

GNU / Linux sisteminin hangi katmanının gözlenen davranışa neden olduğunu bilmediğim için, ikinci kodun makinenizde ne yapacağını bilmiyorum. Bunu deneyebilirsin.

GÜNCELLEME

@Kusalananda yorumlarda gözlemledi:

Değeri ne olursa olsun, OpenBSD'de çalıştırıldığında en son kodunuz farklı adresler ve (zaman zaman) başlatılmamış (sıfır olmayan) veriler döndürür. Bu açıkça Linux'ta tanık olduğunuz davranış hakkında hiçbir şey söylemiyor.

Sonucumun OpenBSD'deki sonuçtan farklı olması gerçekten ilginç. Görünüşe göre, deneylerim düşündüğüm gibi bir çekirdek (veya bağlayıcı) güvenlik protokolü değil, sadece bir uygulama artefaktı keşfetti.

Bu ışık altında, @mosvy, @StephenKitt ve @AndreasGrapentin'in aşağıdaki cevaplarının birlikte sorumu çözdüğüne inanıyorum.

Ayrıca bkz. Yığın Taşması: Malloc neden değerleri gcc cinsinden 0 olarak başlatır? (kredi: @bta).


2
Değeri ne olursa olsun, OpenBSD'de çalıştırıldığında en son kodunuz farklı adresler ve (zaman zaman) başlatılmamış (sıfır olmayan) veriler döndürür. Bu tabii ki yok değil Linux üzerinde şahit olduğumuzu davranışı hakkında bir şey söylemek.
Kusalananda

Lütfen sorunuzun kapsamını değiştirmeyin ve cevapları ve yorumları gereksiz hale getirmek için soruyu düzenlemeye çalışmayın. C'de "yığın", malloc () ve calloc () tarafından döndürülen bellekten başka bir şey değildir ve yalnızca ikincisi belleği sıfırlar; newC operatör ++ (aynı zamanda "yığın") Linux malloc'un için bir sargı () 'i; çekirdek "yığın" ne olduğunu bilmiyor ya da umursamıyor.
mosvy

3
İkinci örneğiniz glibc'deki malloc uygulamasının bir yapısını ortaya koymaktır; 8 bayttan daha büyük bir tamponla tekrarlanan malloc / free yaparsanız, sadece ilk 8 baytın sıfırlandığını açıkça göreceksiniz.
mosvy

@Kusalananda anlıyorum. Sonucumun OpenBSD'deki sonuçtan farklı olması gerçekten ilginç. Görünüşe göre, siz ve Mosvy, deneylerimin düşündüğüm gibi bir çekirdek (veya bağlayıcı) güvenlik protokolü değil, sadece bir uygulama artefaktı keşfettiğini gösterdiniz.
thb

@thb Bunun doğru bir gözlem olabileceğine inanıyorum, evet.
Kusalananda

Yanıtlar:


28

() Malloc'dan tarafından döndürülen depolama değildir sıfır başlatıldı. Asla öyle olduğunu varsaymayın.

Test programınızda, bu sadece bir şans: Sanırım malloc()yeni bir blok var mmap(), ama buna da güvenmeyin.

Örneğin, programınızı makinemde şu şekilde çalıştırırsam:

$ 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

İkinci örneğiniz mallocglibc'de uygulamanın bir yapaylığını ortaya koymaktır; Bunu tekrarlanan yaparsanız malloc/ freea tampon daha büyük 8 byte ile, açıkça aşağıdaki örnek kodda olduğu gibi, bu sadece ilk 8 byte Sıfırlı göreceksiniz.

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

Çıktı:

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

2
Evet, ama bu yüzden burada Stack Overflow yerine soruyu sordum. Benim sorum C standardı ile ilgili değil, modern GNU / Linux sistemlerinin tipik olarak ikili dosyaları bağlama ve yükleme biçimi hakkındaydı. LD_PRELOAD mizahi ama sormak istediğim sorudan başka bir soruya cevap veriyor.
thb

19
Seni güldürdüğüm için mutluyum, ama varsayımların ve önyargıların hiç komik değil. "Modern bir GNU / Linux sisteminde", ikili dosyalar genellikle programınızdan main () işlevine geçmeden önce dinamik kitaplıklardan yapıcılar çalıştıran dinamik bir bağlayıcı tarafından yüklenir. Debian GNU / Linux 9 sisteminizde, malloc () ve free (), önceden yüklenmiş kitaplıklar kullanılmasa bile, programınızdan main () işlevinden önce birden çok kez çağrılır.
mosvy

23

Yığının nasıl başlatıldığına bakılmaksızın, bozulmamış bir yığın görmüyorsunuz, çünkü C kütüphanesi aramadan önce birkaç şey yapıyor mainve yığına dokunuyorlar.

GNU C kütüphanesi ile x86-64 üzerinde, yürütme işlemi ayarlamak için çağrılan _start giriş noktasında başlar ve son çağrı çağrılır . Ancak aramadan önce , yığına çeşitli veri parçalarının yazılmasına neden olan bir dizi başka işlevi çağırır. Yığın içeriği işlev çağrıları arasında temizlenmez, bu nedenle içeri girdiğinizde yığınızda önceki işlev çağrılarından kalanlar bulunur.__libc_start_mainmainmainmain

Bu sadece yığından elde ettiğiniz sonuçları açıklar, genel yaklaşımınız ve varsayımlarınızla ilgili diğer cevaplara bakın.


Zaman main()çağrıldığında, başlatma yordamları tarafından döndürülen belleği çok iyi değiştirmiş olabilir malloc()- özellikle C ++ kitaplıkları bağlıysa.
Andrew Henle

Mosvy'nin yanıtı sorumu çözdü. Sistem maalesef bu ikisinden sadece birini kabul etmeme izin veriyor ; aksi halde ikisini de kabul ederdim.
thb

18

Her iki durumda da, başlatılmamış belleğe sahip olursunuz ve içeriği hakkında herhangi bir varsayımda bulunamazsınız.

İşletim Sistemi, işleminize yeni bir sayfa dağıtmak zorunda kaldığında (ister yığını için, isterse kullanılan arena için olsun malloc()), diğer işlemlerden veri göstermeyeceğini garanti eder; Bunu sağlamanın normal yolu sıfırlarla doldurmaktır (ancak bir sayfa bile dahil olmak üzere başka bir şeyin üzerine yazmak için eşit derecede geçerlidir /dev/urandom- aslında bazı hata ayıklama malloc()uygulamaları sizinki gibi yanlış varsayımları yakalamak için sıfır olmayan kalıplar yazar).

Eğer malloc()zaten bu işlem tarafından kullanılan ve serbest bellekten isteği tatmin edebilir, içeriği aslında, takas ile hiçbir alakası yoktur (temizlenmez malloc()ve olamaz - bu bellek içine eşlenir önce yapılmalıdır adres alanınız). Daha önce işleminiz / programınız tarafından yazılmış bellek (örneğin, daha önce main()) alabilirsiniz.

Örnek programınızda, malloc()bu işlem tarafından henüz yazılmamış bir bölge (yani doğrudan yeni bir sayfadan) ve ( main()programınızda ön kod ile) yazılan bir yığın görüyorsunuz . Yığının daha fazlasını incelerseniz, sıfırın daha aşağı dolu olduğunu görürsünüz (büyüme yönünde).

İşletim sistemi düzeyinde neler olduğunu gerçekten anlamak istiyorsanız, C Library katmanını atlamanızı brk()ve mmap()bunun yerine sistem çağrılarını kullanarak etkileşimde bulunmanızı öneririm .


1
Bir veya iki hafta önce, farklı bir deney yaptık çağıran malloc()ve free()tekrar tekrar. Hiçbir şey malloc()son zamanlarda serbest bırakılan aynı depolamayı yeniden kullanmak gerektirmemesine rağmen , deneyde malloc()bunu yaptı. Her seferinde aynı adresi döndürdü, ancak her seferinde beklemediğim belleği boş bıraktı. Bu benim için ilginçti. Daha sonraki deneyler bugünün sorusuna yol açmıştır.
16'da thb

1
@thb, Belki de yeterince net değilim - çoğu uygulama size verdikleri bellekle malloc()kesinlikle hiçbir şey yapmaz - ya daha önce kullanılmış ya da yeni atanmıştır (ve dolayısıyla işletim sistemi tarafından sıfırlanmıştır). Testinizde, açıkça ikincisini aldınız. Benzer şekilde, yığın belleği işleminize temizlenmiş durumda verilir, ancak işleminizin henüz dokunmadığı kısımları görecek kadar fazla incelemezsiniz. Yığın belleğiniz , işleminize verilmeden önce silinir.
Toby Speight

2
@TobySpeight: brk ve sbrk, mmap tarafından kullanılmıyor. pubs.opengroup.org/onlinepubs/7908799/xsh/brk.html LEGACY'nin hemen üstünde olduğunu söylüyor.
Joshua

2
Kullanıma callocmemset
başlamak

2
@ thb ve Toby: eğlenceli gerçek: çekirdekten yeni sayfalar genellikle tembel olarak ayrılır ve yalnızca paylaşılan sıfırlanmış bir sayfaya yazılırken kopyalanır. Bu mmap(MAP_ANONYMOUS), siz de kullanmadığınız sürece olur MAP_POPULATE. Yeni yığın sayfalar umarım yeni fiziksel sayfalarla desteklenir ve büyürken kablolanır (donanım sayfa tablolarında ve çekirdeğin işaretçi / uzunluk eşleme listesi), çünkü normalde ilk kez dokunulduğunda yeni yığın bellek yazılır . Ancak evet, çekirdek bir şekilde veri sızıntısından kaçınmalı ve sıfırlama en ucuz ve en yararlıdır.
Peter Cordes

9

Sizin öncülünüz yanlış.

'Güvenlik' olarak tanımladığınız şey gerçekten gizliliktir , yani bu bellek bu işlemler arasında açıkça paylaşılmadığı sürece hiçbir işlemin başka bir işlem belleğini okuyamayacağı anlamına gelir. Bir işletim sisteminde, bu, eşzamanlı faaliyetlerin veya süreçlerin izolasyonunun bir yönüdür .

İşletim sisteminin bu yalıtımı sağlamak için yaptığı şey, yığın veya yığın ayırma işlemi için bellek istendiğinde, bu bellek ya fiziksel bellekte sıfırlarla doldurulmuş bir bölgeden geliyorsa veya aynı süreçten geliyor .

Eğer sadece hiç gizliliği sağlanır, böylece sıfır veya kendi önemsiz görüyoruz ve bu Bu olmasını sağlar hem yığın ve mutlaka (sıfır) başlatıldı olsa yığını vardır, 'güvenli'.

Ölçümlerinizi çok fazla okuyorsunuz.


1
Sorunun Güncelleme bölümü şimdi aydınlatıcı cevabınıza açıkça atıfta bulunuyor.
thb
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.