Alım işlevi kullanılmaması için neden bu kadar tehlikelidir?


229

gets()GCC ile işlevi kullanan C kodunu derlemeye çalıştığımda , bu uyarıyı alıyorum:

(.text + 0x34): uyarı: `` gets '' işlevi tehlikelidir ve kullanılmamalıdır.

Bunun yığın koruması ve güvenliği ile ilgili bir şey olduğunu hatırlıyorum, ama tam olarak neden olduğundan emin değilim.

Bu uyarıyı nasıl kaldırabilirim ve neden kullanmayla ilgili böyle bir uyarı var gets()?

Bu gets()kadar tehlikeliyse neden kaldıramıyoruz?



Yanıtlar:


179

getsGüvenli bir şekilde kullanmak için , tam olarak kaç karakter okuyacağınızı bilmeniz gerekir, böylece ara belleğinizi yeterince büyük hale getirebilirsiniz. Yalnızca tam olarak hangi verileri okuyacağınızı bileceksiniz.

Kullanmak yerine , imzası olan getskullanmak istiyorsunuzfgets

char* fgets(char *string, int length, FILE * stream);

( fgetstüm satırı okursa '\n', dizede bırakılır; bununla uğraşmanız gerekir.)

1999 ISO C standardına kadar dilin resmi bir parçası olarak kaldı, ancak 2011 standardı tarafından resmi olarak kaldırıldı. Çoğu C uygulaması hala bunu desteklemektedir, ancak en azından gcc onu kullanan herhangi bir kod için bir uyarı verir.


79
Aslında uyaran gcc değil gets(), derleyicinin kullanıldığında uyarı vermesine neden olan bir pragma veya özellik içeren glibc .
fuz

@fuz aslında, uyaran sadece derleyici değil: OP'de belirtilen uyarı bağlayıcı tarafından yazdırıldı!
Ruslan

163

Neden gets()tehlikeli

İlk internet solucanı ( Morris İnternet Solucanı ) yaklaşık 30 yıl önce (1988-11-02) kaçtı ve gets()sistemden sisteme yayılma yöntemlerinden biri olarak bir tampon taşması kullandı . Temel problem, fonksiyonun tamponun ne kadar büyük olduğunu bilmemesidir, bu nedenle yeni bir satır bulana veya EOF ile karşılaşana kadar okumaya devam eder ve verilen tamponun sınırlarını aşabilir.

Var olduğunu duyduğunuzu unutmalısınız gets().

C11 standardı ISO / IEC 9899: 2011 gets()standart bir işlev olarak ortadan kaldırıldı , bu da A Good Thing ™ (resmi olarak ISO / IEC 9899: 1999 / Cor.3: 2007'de 'eskimiş' ve 'kullanımdan kaldırıldı' olarak işaretlendi) C99 için 3'tür ve daha sonra C11'de çıkarılır). Ne yazık ki, geriye dönük uyumluluk nedeniyle kütüphanelerde uzun yıllar ('on yıllar' anlamına gelir) kalacaktır. Bana kalmış gets()olsaydı , uygulaması şöyle olurdu:

char *gets(char *buffer)
{
    assert(buffer != 0);
    abort();
    return 0;
}

Kodunuzun zaten er ya da geç çökeceği göz önüne alındığında, sorunu daha sonra değil, daha erken halletmek daha iyidir. Bir hata mesajı eklemeye hazır olurum:

fputs("obsolete and dangerous function gets() called\n", stderr);

Linux derleme sisteminin modern sürümleri, bağlantı kurarsanız gets()ve ayrıca güvenlik sorunları olan bazı diğer işlevler için uyarılar üretir ( mktemp(),…).

Alternatifleri gets()

fgets ()

Herkesin dediği gibi, kanonik alternatif etmek gets()olduğunu fgets()belirterek stdindosya akışı olarak.

char buffer[BUFSIZ];

while (fgets(buffer, sizeof(buffer), stdin) != 0)
{
    ...process line of data...
}

Daha önce kimsenin bahsetmediği şey gets(), yeni satırı içermediği, ancak fgets()içermediği. Bu nedenle, fgets()yeni satırı silen bir sarmalayıcı kullanmanız gerekebilir :

char *fgets_wrapper(char *buffer, size_t buflen, FILE *fp)
{
    if (fgets(buffer, buflen, fp) != 0)
    {
        size_t len = strlen(buffer);
        if (len > 0 && buffer[len-1] == '\n')
            buffer[len-1] = '\0';
        return buffer;
    }
    return 0;
}

Ya da daha iyisi:

char *fgets_wrapper(char *buffer, size_t buflen, FILE *fp)
{
    if (fgets(buffer, buflen, fp) != 0)
    {
        buffer[strcspn(buffer, "\n")] = '\0';
        return buffer;
    }
    return 0;
}

Ayrıca, caf bir yorumda belirttiği ve paxdiablo'nun cevabında gösterdiği gibi fgets(), bir satırda kalan veriler olabilir. Sarıcı kodum bu verileri bir dahaki sefere okunacak şekilde bırakır; isterseniz, veri satırının geri kalanını silip süpürmek için kolayca değiştirebilirsiniz:

        if (len > 0 && buffer[len-1] == '\n')
            buffer[len-1] = '\0';
        else
        {
             int ch;
             while ((ch = getc(fp)) != EOF && ch != '\n')
                 ;
        }

Kalan sorun, üç farklı sonuç durumunun nasıl rapor edileceğidir - EOF veya hata, satır okuma ve kesilmemiş ve kısmi satır okuma ancak veriler kesildi.

Bu sorun, gets()ara belleğinizin nerede sona erdiğini ve nihayetinde nihayetinde ezildiğini bilmediği için, güzel bir şekilde ayrılmış bellek düzeninize zarar verir, arabellek tahsis edilirse genellikle dönüş yığınını ( Yığın Taşması ) bozar. yığını dinamik olarak tahsis edilmişse kontrol bilgisi üzerinden ya da arabelleğe statik olarak tahsis edilmişse diğer değerli global (veya modül) değişkenlere veri kopyalanması. Bunların hiçbiri iyi bir fikir değildir - 'tanımsız davranış' ifadesini özetlerler.


Ayrıca aşağıdakiler de dahil olmak üzere çeşitli işlevlere daha güvenli alternatifler sunan TR 24731-1 (C Standart Komitesinden Teknik Rapor) bulunmaktadır gets():

§6.5.4.1 gets_sİşlev

özet

#define __STDC_WANT_LIB_EXT1__ 1
#include <stdio.h>
char *gets_s(char *s, rsize_t n);

Süre-kısıtları

sboş bir işaretçi olamaz. nsıfıra eşit veya RSIZE_MAX değerinden büyük olamaz. Adlı n-1karakterleri okurken yeni satır karakteri, dosya sonu veya okuma hatası oluşacaktır stdin. 25)

3 Çalışma zamanı kısıtlaması ihlali varsa, s[0]boş karakter olarak ayarlanır ve stdinyeni satır karakteri okunana veya dosya sonu veya okuma hatası oluşana kadar karakterler okunur ve atılır .

Açıklama

4 gets_sİşlev n , işaret ettiği akıştan, işaret stdinettiği diziye kadar belirtilen karakter sayısından en az birini daha az okur s. Yeni bir karakterden (atılan) veya dosya sonundan sonra başka hiçbir karakter okunmaz. Atılan yeni satır karakteri okunan karakter sayısına dahil edilmez. Diziye son karakter okunduktan hemen sonra boş bir karakter yazılır.

5 Dosya sonu ile karşılaşılırsa ve diziye hiçbir karakter okunmazsa veya işlem sırasında bir okuma hatası oluşursa s[0], boş karakter olarak ayarlanır ve diğer öğeler sbelirtilmemiş değerleri alır.

Önerilen uygulama

6 fgetsİşlev, düzgün yazılan programların, sonuç dizisinde depolanamayacak kadar uzun giriş satırlarını güvenli bir şekilde işlemesini sağlar. Genel olarak bu, arayanların fgetssonuç dizisinde yeni satır karakterinin varlığına veya yokluğuna dikkat etmesini gerektirir . fgetsBunun yerine (yeni satır karakterlerine dayalı gerekli işlemlerle birlikte) kullanmayı düşünün gets_s.

25)gets_s farklı olarak işlev, getsbu saklamak bellek taşmasına girdinin bir hat için bir çalışma zamanı-kısıtlaması ihlali yapar. Bunun aksine fgets, gets_sgiriş hatları ile başarılı çağrılar arasında bire bir ilişki sürdürür gets_s. Kullanan programlar getsböyle bir ilişki bekler.

Microsoft Visual Studio derleyicileri, TR 24731-1 standardına bir yaklaşım uygular, ancak Microsoft tarafından TR'de imzalanan imzalar arasında farklılıklar vardır.

C11 standardı ISO / IEC 9899-2011, kütüphanenin isteğe bağlı bir parçası olarak Ek K'da TR24731 içerir. Ne yazık ki, nadiren Unix benzeri sistemlerde uygulanır.


getline() - POSIX

POSIX 2008 de güvenli bir alternatif sunmaktadır gets()denir getline(). Çizgi için dinamik olarak yer ayırır, böylece onu serbest bırakmanız gerekir. Bu nedenle, hat uzunluğu sınırlamasını kaldırır. Ayrıca okunan verinin uzunluğunu döndürür -1( ya da değil EOF!). Ayrıca 'kendi tek karakterli sınırlayıcınızı seçin' varyasyonu vardır getdelim(); örneğin find -print0, dosya adlarının uçlarının ASCII NUL '\0'karakteriyle işaretlendiği çıktıyla ilgileniyorsanız bu yararlı olabilir .


8
Ayrıca fgets(), fgets_wrapper()sürümünüzün, bir sonraki giriş işlevi tarafından okunmak için giriş arabelleğindeki uzun bir çizginin arka kısmını bırakacağını belirtmek gerekir. Çoğu durumda, bu karakterleri okumak ve silmek istersiniz.
caf

5
Neden bir aptalca strlen çağrı yapmak zorunda kalmadan işlevselliğini kullanmak için izin veren bir fgets () alternatif eklemedim merak ediyorum. Örneğin, dizeye okunan bayt sayısını döndüren bir fgets varyantı, kodun son okunan baytın yeni satır olup olmadığını görmesini kolaylaştıracaktır. Tampon için bir boş gösterici geçirme davranışı "bir sonraki satırsonuna kadar n-1 bayta kadar okuma ve atma" olarak tanımlandıysa, bu, kodun fazla uzunluktaki kuyrukları kolayca atmasına izin verir.
supercat

2
@supercat: Evet, katılıyorum - çok yazık. Buna en yakın yaklaşım muhtemelen POSIX getline()ve göreli getdelim()olup, komutlar tarafından okunan 'satırın' uzunluğunu döndürür ve tüm satırı saklayabilmek için gereken alanı ayırır. Boyutu birden çok gigabayt olan tek satırlı bir JSON dosyası ile sonuçlanırsanız bile bu sorunlara neden olabilir; tüm bu hafızayı karşılayabilir misin? (Biz bunu yaparken, biz olabilir strcpy()ve strcat()sonunda boş byte için bir işaretçi döndürür varyantları Vb?)
Jonathan Leffler

4
@supercat: diğer sorun fgets(), dosya bir boş bayt içeriyorsa, boş bayttan sonra satır sonuna kadar (veya EOF) ne kadar veri olduğunu söyleyemezsiniz. strlen()yalnızca verilerdeki boş bayta kadar rapor verebilir; Bundan sonra, tahmin ve bu nedenle neredeyse kesinlikle yanlıştır.
Jonathan Leffler

7
" gets()var olduğunu duyduğunu unutma ." Bunu yaptığımda tekrar karşılaşıp buraya geliyorum. Oy almak için stackoverflow'u hackliyor musunuz?
candied_orange

21

Çünkü stdin'dengets bayt alırken ve onları bir yere koyarken hiçbir kontrol yapmaz . Basit bir örnek:

char array1[] = "12345";
char array2[] = "67890";

gets(array1);

Şimdi, her şeyden önce, kaç karakter istediğinizi girmenize izin verilir gets, bununla ilgilenmezsiniz. İkincisi, onları yerleştirdiğiniz dizinin boyutu üzerindeki baytlar (bu durumda array1), bellekte buldukları her şeyin üzerine yazacaktır çünkü getsonları yazacaktır. Önceki örnekte bu "abcdefghijklmnopqrts", belki de tahmin edilemez şekilde girerseniz , bunun üzerine array2veya başka bir şeyin üzerine yazılacağı anlamına gelir .

Tutarsız girdi varsaydığı için işlev güvenli değildir. ASLA KULLANMAYIN!


3
getsAçıkça kullanılamaz yapan şey, aldığı bir dizi uzunluğu / sayısı parametresine sahip olmamasıdır; orada olsaydı, sadece sıradan bir C standart işlevi olurdu.
legends2k

@ legends2k: Amaçlanan kullanımın ne olduğunu getsve yeni satırın girdinin bir parçası olarak istenmediği kullanım durumları için neden hiçbir standart fgets varyantı yapılmadığını merak ediyorum ?
supercat

1
@supercat gets, adından da anlaşılacağı gibi, bir dize almak için tasarlanmıştı stdin, ancak bir boyut parametresine sahip olmamanın mantığı C : Programcıya güven ruhundan olabilir . Bu işlev C11'de kaldırıldı ve verilen değiştirme gets_sgiriş tamponunun boyutunu alır. fgetsParça hakkında hiçbir fikrim yok .
legends2k

@ legends2k: Mümkün olabileceğini görebildiğim tek bağlam gets, fiziksel olarak belirli bir uzunlukta bir çizgi ve programın kullanım ömrü boyunca gönderilemeyen bir donanım hattı tamponlu G / Ç sistemi kullanıyor olsaydı olurdu. donanımın ömründen daha kısaydı. Bu durumda, donanım 127 bayt uzunluğundaki satırları getsgönderemezse, 128 baytlık bir arabelleğe haklı olabilir , ancak daha küçük bir girdi beklerken daha kısa bir tampon belirleyebilmenin avantajlarını maliyet.
supercat

@ legends2k: Aslında, ideal olabilecek şey, birkaç farklı dize / arabellek / arabellek bilgisi biçimi arasından seçim yapacak bir baytı tanımlamak için bir "dize işaretçisi" olması ve içerilen bir yapıyı gösteren bir önek bayt değeri olması olurdu. önek baytı [artı dolgusu], artı arabellek boyutu, kullanılan boyut ve gerçek metnin adresi. Böyle bir model kodu şey kopyalamak zorunda kalmadan başka dize keyfi bir alt dize (sadece kuyruk) geçmesi mümkün kılacak ve benzeri yöntemler sağlayacak getsve strcatgüvenli bir şekilde çok olarak uyacak şekilde kabul ediyoruz.
supercat

16

getsBir arabellek taşmasını durdurmanın bir yolu olmadığından kullanmamalısınız . Kullanıcı arabelleğe sığabileceğinden daha fazla veri yazarsa, büyük olasılıkla bozulma veya daha kötüsü ile karşılaşırsınız.

Aslında, ISO aslında aşamasını almış çıkarılması gets son derece geriye doğru uyumluluk nasıl oranı göz önüne alındığında, fonksiyonu olduğu kadar kötü bir göstergesi olması gereken, (o C99 kaldırıldı da, C11 gibi) Cı standarttan.

Yapılacak doğru şey, fgetsişlevi stdindosya tanıtıcısı ile kullanmaktır, çünkü kullanıcıdan okunan karakterleri sınırlandırabilirsiniz.

Ancak bunun da sorunları var:

  • kullanıcı tarafından girilen ekstra karakterler bir sonraki sefer alınacaktır.
  • kullanıcının çok fazla veri girdiğine dair hızlı bir bildirim yoktur.

Bu amaçla, kariyerlerinin bir noktasında hemen hemen her C kodlayıcısı da daha yararlı bir paket yazacaktır fgets. Benimki burada:

#include <stdio.h>
#include <string.h>

#define OK       0
#define NO_INPUT 1
#define TOO_LONG 2
static int getLine (char *prmpt, char *buff, size_t sz) {
    int ch, extra;

    // Get line with buffer overrun protection.
    if (prmpt != NULL) {
        printf ("%s", prmpt);
        fflush (stdout);
    }
    if (fgets (buff, sz, stdin) == NULL)
        return NO_INPUT;

    // If it was too long, there'll be no newline. In that case, we flush
    // to end of line so that excess doesn't affect the next call.
    if (buff[strlen(buff)-1] != '\n') {
        extra = 0;
        while (((ch = getchar()) != '\n') && (ch != EOF))
            extra = 1;
        return (extra == 1) ? TOO_LONG : OK;
    }

    // Otherwise remove newline and give string back to caller.
    buff[strlen(buff)-1] = '\0';
    return OK;
}

bazı test kodlarıyla:

// Test program for getLine().

int main (void) {
    int rc;
    char buff[10];

    rc = getLine ("Enter string> ", buff, sizeof(buff));
    if (rc == NO_INPUT) {
        printf ("No input\n");
        return 1;
    }

    if (rc == TOO_LONG) {
        printf ("Input too long\n");
        return 1;
    }

    printf ("OK [%s]\n", buff);

    return 0;
}

fgetsArabellek taşmalarını önlediği ile aynı korumaları sağlar, ancak arayana ne olduğunu bildirir ve fazla karakteri temizler, böylece bir sonraki giriş işleminizi etkilemezler.

İstediğiniz gibi kullanmaktan çekinmeyin, burada "iyi olmasını istediğiniz şeyi yapın" lisansı altında serbest bırakıyorum :-)


Aslında, orijinal C99 standardı, gets()ya tanımlandığı 7.19.7.7 bölümünde ya da 7.26.9 Gelecek kütüphane yönleri ve alt bölümünde açıkça tasfiye edilmemiştir <stdio.h>. Tehlikeli olduğuna dair bir dipnot bile yok. (Ben "Bu 9899 ISO / IEC önerilmemektedir: 1999 / Cor.3: 2007 (E) bakınız, söyledikten)" bölümünde cevabını tarafından Yu Hao ) Ama C11 standart çıkarın vermedi - değil saatinden önce.!
Jonathan Leffler

int getLine (char *prmpt, char *buff, size_t sz) { ... if (fgets (buff, sz, stdin) == NULL)size_tiçin intdönüşümü gizler sz. sz > INT_MAX || sz < 2tuhaf değerleri yakalayacaktı sz.
chux - Monica'yı geri yükle

if (buff[strlen(buff)-1] != '\n') {kötü kullanıcının girdiği ilk karakter buff[strlen(buff)-1]UB oluşturma gömülü bir boş karakter olabilir gibi bir hacker istismar . while (((ch = getchar())...bir kullanıcı boş karakter girerse sorun yaşar.
chux - Monica'yı geri yükle

12

fgets .

Stdin'den okumak için:

char string[512];

fgets(string, sizeof(string), stdin); /* no buffer overflows here, you're safe! */

6

API işlevlerini bozmadan API işlevlerini kaldıramazsınız. İsterseniz, birçok uygulama artık derlenmeyecek veya çalışmayacaktır.

Bir referansın vermesinin nedeni budur :

S ile gösterilen diziyi taşan bir satırın okunması, tanımsız davranışa neden olur. Fgets () kullanılması önerilir.


4

Geçenlerde USENET gönderisindecomp.lang.c , bu gets()Standarttan kaldırılıyor okudum . Woohoo

Komitenin taslaktan get () 'i kaldırmak için oybirliğiyle (oybirliğiyle, oy kullandığını) bilmek mutlu olacaksınız.


3
Standarttan çıkarılması mükemmeldir. Bununla birlikte, çoğu uygulama, geriye dönük uyumluluk nedeniyle, en azından önümüzdeki 20 yıl boyunca 'artık standart olmayan bir uzantı' olarak sağlayacaktır.
Jonathan Leffler

1
Evet, doğru, ama ile derlediğinizde gcc -std=c2012 -pedantic ...gets () elde edemezsiniz. ( -stdParametreyi yeni oluşturdum)
pmg

4

C11'de (ISO / IEC 9899: 201x) gets()kaldırıldı. (ISO / IEC 9899: 1999 / Cor.3: 2007 (E) 'de kullanımdan kaldırılmıştır)

Buna ek olarak fgets(), C11 yeni bir güvenli alternatif sunuyor gets_s():

C11 K.3.5.4.1 gets_sİşlev

#define __STDC_WANT_LIB_EXT1__ 1
#include <stdio.h>
char *gets_s(char *s, rsize_t n);

Bununla birlikte, Önerilen uygulama bölümünde fgets()hala tercih edilmektedir.

fgetsFonksiyon sonuç dizide mağazaya çok uzun güvenle işlem giriş hatlarına programları düzgün yazılmış verir. Genel olarak bu, arayanların fgetssonuç dizisinde yeni satır karakterinin varlığına veya yokluğuna dikkat etmesini gerektirir . fgetsBunun yerine (yeni satır karakterlerine dayalı gerekli işlemlerle birlikte) kullanmayı düşünün gets_s.


3

gets()tehlikelidir, çünkü kullanıcının bilgi istemine çok fazla yazarak programı çökmesi mümkündür. Kullanılabilir belleğin sonunu algılayamaz, bu nedenle amaç için çok küçük bir bellek ayırırsanız, bir seg hatasına ve çökmeye neden olabilir. Bazen bir kullanıcının bir kişinin adı için bir isteme 1000 harf yazması pek olası görünmez, ancak programcılar olarak programlarımızı kurşun geçirmez hale getirmeliyiz. (bir kullanıcının bir sistem programını çok fazla veri göndererek çökebilmesi de bir güvenlik riski olabilir).

fgets() standart girdi arabelleğinden kaç karakter alınacağını belirlemenize olanak tanır, böylece değişkeni aşmazlar.


Gerçek tehlikenin programınızı çökertmek değil, rastgele kod çalıştırmasını sağlamak olduğunu unutmayın . (Genel olarak, tanımsız davranışlardan yararlanma .)
Tanz87

2

gets"Hala herkesin buna bağlı olması durumunda" kitaplıklarına dahil olan herhangi bir C kitaplığı koruyucusuna ciddi bir davet sunmak istiyorum : Lütfen uygulamanızı

char *gets(char *str)
{
    strcpy(str, "Never use gets!");
    return str;
}

Bu, kimsenin hala ona bağlı olmadığından emin olmaya yardımcı olacaktır. Teşekkür ederim.


2

C'nin işlevi tehlikelidir ve çok maliyetli bir hata olmuştur. Tony Hoare, "Null Referanslar: Milyar Dolarlık Hata" konulu konuşmasında özel olarak anılacaktır:

http://www.infoq.com/presentations/Null-References-The-Billion-Dollar-Mistake-Tony-Hoare

Bütün saat izlemeye değer ama onun yorum için 30 dakikadan itibaren belirli 39 dakika hakkında eleştiri alır.

Umarım bu, tüm konuşma için iştahınızı artırır, bu da dillerde daha resmi doğruluk kanıtlarına nasıl ihtiyacımız olduğuna ve dil tasarımcılarının programcıya değil, dillerindeki hatalardan nasıl sorumlu tutulması gerektiğine dikkat çeker. Kötü dillerin tasarımcılarının suçu programcılara 'programcı özgürlük' kisvesi altında itmeleri için şüpheli bir neden bu gibi görünüyor.

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.