web-dev-qa-db-ja.com

scanfの欠点

scanf()の欠点を知りたい。

多くのサイトで、scanfを使用するとバッファオーバーフローが発生する可能性があることを読んでいます。この理由は何ですか? scanfに他の欠点はありますか?

56
karthi_ms

Scanfの問題は(少なくとも)です。

  • %sを使用してユーザーから文字列を取得すると、文字列がバッファよりも長くなり、オーバーフローが発生する可能性があります。
  • スキャンが失敗して、ファイルポインターが不定の場所に残る可能性。

読み取りデータ量を制限できるように、fgetsを使用して行全体を読み取ることを非常に好みます。 1Kのバッファーがあり、fgetsを使用してその行を読み込むと、終了改行文字がないという事実によって行が長すぎたかどうかを確認できます(改行のないファイルの最後の行)。

その後、ユーザーに文句を言うか、残りの行にさらにスペースを割り当てます(必要に応じて、十分なスペースができるまで継続的に)。どちらの場合でも、バッファオーバーフローのリスクはありません。

行を読み取ったら、次の行に位置していることをknowしているので、問題はありません。その後、再読み込みのためにファイルポインタを保存および復元することなく、文字列をsscanf思いのままに保存できます。

これは、ユーザーに情報を要求するときにバッファオーバーフローが発生しないようにするために頻繁に使用するコードのスニペットです。

必要に応じて、標準入力以外のファイルを使用するように簡単に調整できます。また、呼び出し元に返す前に、独自のバッファを割り当てて(十分に大きくなるまで増加させ続ける)ことができます(ただし、呼び出し元が責任を持ちます)もちろん、それを解放します)。

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

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

    // Size zero or one cannot store enough, so don't even
    // try - we need space for at least newline and terminator.
    if (sz < 2)
        return SMALL_BUFF;

    // Output Prompt.
    if (prmpt != NULL) {
        printf ("%s", prmpt);
        fflush (stdout);
    }

    // Get line with buffer overrun protection.
    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.
    size_t lastPos = strlen(buff) - 1;
    if (buff[lastPos] != '\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[lastPos] = '\0';
    return OK;
}

そして、そのためのテストドライバー:

// Test program for getLine().

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

    rc = getLine ("Enter string> ", buff, sizeof(buff));
    if (rc == NO_INPUT) {
        // Extra NL since my system doesn't output that on EOF.
        printf ("\nNo input\n");
        return 1;
    }

    if (rc == TOO_LONG) {
        printf ("Input too long [%s]\n", buff);
        return 1;
    }

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

    return 0;
}

最後に、テストを実行して動作を確認します。

$ ./tstprg
Enter string>[CTRL-D]
No input

$ ./tstprg
Enter string> a
OK [a]

$ ./tstprg
Enter string> hello
OK [hello]

$ ./tstprg
Enter string> hello there
Input too long [hello the]

$ ./tstprg
Enter string> i am pax
OK [i am pax]
52
paxdiablo

これまでの回答のほとんどは、文字列バッファオーバーフローの問題に焦点を当てているようです。実際には、scanf関数で使用できる形式指定子は、明示的なフィールド幅設定をサポートします。これにより、入力の最大サイズが制限され、バッファーオーバーフローが防止されます。これにより、scanfに存在する文字列バッファオーバーフローの危険性に関する一般的な非難が事実上ベースレスになります。 scanfgetsと何らかの点で類似しているという主張は、完全に間違っています。 scanfgetsには大きな質的な違いがあります:scanfは文字列バッファーオーバーフロー防止機能をユーザーに提供しますが、getsは提供しません。

フィールド幅をフォーマット文字列に埋め込む必要があるため、これらのscanf機能の使用は困難であると主張できます(printf)。それは実際に本当です。 scanfは、その点で実際にはかなり不十分に設計されています。しかし、それでもscanfが文字列バッファーオーバーフローの安全性に関して何らかの形で絶望的に壊れているという主張は完全に偽であり、通常は怠zyなプログラマーによって作られています。

scanfの実際の問題は、オーバーフローにも関わらず、まったく異なる性質を持っています。 scanf関数を使用して、数値の10進表現を算術型の値に変換する場合、算術オーバーフローから保護されません。オーバーフローが発生すると、scanfは未定義の動作を引き起こします。このため、C標準ライブラリで変換を実行する唯一の適切な方法は、strto... 家族。

したがって、上記を要約すると、scanfの問題は、文字列バッファーで適切かつ安全に使用することが(可能ですが)難しいことです。そして、算術入力に安全に使用することは不可能です。後者が本当の問題です。前者は単に不便です。

追伸上記は、scanf関数のファミリー全体(fscanfおよびsscanfも含む)を対象としています。具体的にはscanfに関して、明らかな問題は、潜在的にinteractive入力を読み取るために厳密にフォーマットされた関数を使用するというアイデアがかなり疑わしいということです。

53
AnT

Comp.lang.c FAQから: なぜ、誰もがscanfを使用しないと言うのですか?代わりに何を使用すればよいですか?

scanfには多くの問題があります。質問を参照してください 12.1712.18a 、および 12.19 。また、その_%s_形式にはgets()と同じ問題があります(質問 12.2 を参照)。受信バッファーがオーバーフローしないことを保証するのは困難です。 [脚注]

より一般的には、scanfは比較的構造化され、フォーマットされた入力用に設計されています(その名前は実際には「スキャンフォーマット済み」から派生しています)。注意を払えば、成功したか失敗したかがわかりますが、失敗したおおよその場所のみがわかり、方法や理由はわかりません。エラー回復を行う機会はほとんどありません。

しかし、インタラクティブなユーザー入力は、最も構造化されていない入力です。適切に設計されたユーザーインターフェイスにより、数字を入力するときに文字や句読点だけでなく、予想よりも多いまたは少ない文字を入力したり、まったく入力しない(ie、RETURNキーのみ)、またはEOFの早期、またはその他。 scanfを使用する場合、これらの潜在的な問題をすべてうまく処理することはほぼ不可能です。行全体を(fgetsなどで)読み、sscanfまたはその他の手法を使用して解釈する方がはるかに簡単です。 (strtolstrtokatoiなどの関数が役立つことがよくあります。質問 12.16 および 13.6 も参照してください。) scanfバリアントを使用する場合は、戻り値をチェックして、予想されるアイテム数が見つかったことを確認してください。また、_%s_を使用する場合は、必ずバッファオーバーフローから保護してください。

ところで、scanfに対する批判は、必ずしもfscanfおよびsscanfの告発ではないことに注意してください。 scanfstdinから読み取ります。これは通常、対話型キーボードであるため、最も制約が少なく、ほとんどの問題につながります。一方、データファイルに既知の形式がある場合、fscanfを使用して読み取ることが適切な場合があります。 sscanfを使用して文字列を解析することは完全に適切です(戻り値がチェックされる限り)。制御を取り戻し、スキャンを再開し、一致しなかった場合は入力を破棄するなど、非常に簡単です。

追加のリンク:

参照:K&R2 Sec。 7.4頁159

12
jamesdlin

はい、あなたは正しいです。 scanf family(scanfsscanffscanf .. etc)には、文字列を読み取るときに重大なセキュリティ上の欠陥があります。バッファの長さ(読み込み先)を考慮してください。

例:

char buf[3];
sscanf("abcdef","%s",buf);

明らかにバッファbufはMAX 3 char。しかし、sscanf"abcdef"がバッファオーバーフローを引き起こします。

5
codaddict

scanfを取得して目的のことを実行するのは非常に困難です。もちろん、できますが、scanf("%s", buf);のようなものは、誰もが言っているように、gets(buf);と同じくらい危険です。

例として、paxdiabloが読み取り機能で行っていることは、次のような方法で実行できます。

scanf("%10[^\n]%*[^\n]", buf));
getchar();

上記は行を読み、bufに最初の10個の非改行文字を保存し、改行まで(を含む)すべてを破棄します。したがって、paxdiabloの関数はscanfを使用して次のように記述できます。

#include <stdio.h>

enum read_status {
    OK,
    NO_INPUT,
    TOO_LONG
};

static int get_line(const char *Prompt, char *buf, size_t sz)
{
    char fmt[40];
    int i;
    int nscanned;

    printf("%s", Prompt);
    fflush(stdout);

    sprintf(fmt, "%%%zu[^\n]%%*[^\n]%%n", sz-1);
    /* read at most sz-1 characters on, discarding the rest */
    i = scanf(fmt, buf, &nscanned);
    if (i > 0) {
        getchar();
        if (nscanned >= sz) {
            return TOO_LONG;
        } else {
            return OK;
        }
    } else {
        return NO_INPUT;
    }
}

int main(void)
{
    char buf[10+1];
    int rc;

    while ((rc = get_line("Enter string> ", buf, sizeof buf)) != NO_INPUT) {
        if (rc == TOO_LONG) {
            printf("Input too long: ");
        }
        printf("->%s<-\n", buf);
    }
    return 0;
}

scanfのその他の問題の1つは、オーバーフローの場合の動作です。たとえば、intを読み取る場合:

int i;
scanf("%d", &i);

オーバーフローが発生した場合、上記は安全に使用できません。最初の場合でも、文字列の読み取りは、fgetsよりもscanfの方がはるかに簡単です。

5
Alok Singhal

ここでの多くの回答は、scanf("%s", buf)を使用することで発生する可能性のあるオーバーフローの問題について説明していますが、最新のPOSIX仕様では、フォーマット指定子で使用できるm割り当て割り当て文字を提供することでこの問題を解決していますcs、および[形式の場合。これにより、scanfmallocで必要なだけメモリを割り当てることができます(したがって、freeで後で解放する必要があります)。

その使用例:

char *buf;
scanf("%ms", &buf); // with 'm', scanf expects a pointer to pointer to char.

// use buf

free(buf);

here を参照してください。このアプローチの欠点は、POSIX仕様への比較的最近の追加であり、C仕様でまったく指定されていないため、現時点では移植性が低いことです。

3
dreamlax

scanf- like関数には1つの大きな問題があります。anyタイプセーフティの欠如です。つまり、これをコーディングできます。

int i;
scanf("%10s", &i);

地獄、これでも「素晴らしい」:

scanf("%10s", i);

printfはポインタを期待しているため、scanfのような関数よりも悪いので、クラッシュする可能性が高くなります。

確かに、いくつかの形式指定子チェッカーがありますが、それらは完璧ではなく、言語や標準ライブラリの一部でもありません。

3

scanfの利点は、Cで常に行うべきツールの使用方法を学ぶと、非常に有用なユースケースがあることです。マニュアル を読んで理解することで、scanfと友人の使い方を学ぶことができます。あなたが深刻な理解の問題なしでそのマニュアルを手に入れることができないなら、これはおそらくあなたがCをあまりよく知らないことを示しているでしょう。


scanfと友人は、ドキュメントを読まずに正しく使用することを困難にした(そして時には不可能にした)不幸な設計選択に苦しみました。答えが示されています。これは残念ながらC全体で発生するため、scanfを使用しないようにアドバイスする場合は、Cを使用することをお勧めします。

最大の不利な点の1つは、純粋に未経験者の間で得られた評判にあるようです; Cの多くの便利な機能と同様に、使用する前に十分な情報を得る必要があります。重要なのは、Cの残りの部分と同様に、簡潔で慣用的なように見えるが、それは微妙に誤解を招く可能性があることを認識することです。これはCに広まっています。初心者にとっては理にかなっていると思われるコードを書くことは簡単で、最初はうまくいくかもしれませんが、意味がなく、破滅的に失敗する可能性があります。

たとえば、初心者は一般的に_%s_デリゲートによってa行が読み取られることを期待しますが、直感的に思えるかもしれませんが、必ずしもそうではありません。 a Wordとして読み取られるフィールドを記述する方が適切です。すべての機能について、マニュアルを読むことを強くお勧めします。

安全性の欠如とバッファオーバーフローのリスクは言うまでもなく、この質問への回答は何でしょうか?すでに説明したように、Cは安全な言語ではないため、正確性を犠牲にして最適化を適用する可能性があります。したがって、システムが固定バイト数を超える文字列を決して受け取らないことがわかっている場合、サイズをチェックして境界チェックを行わない配列を宣言することができます。私はこれを本当に落ち込みとは思わない。それはオプションです。繰り返しますが、マニュアルを読むことを強くお勧めします。

怠zyなプログラマーだけがscanfに刺されません。たとえば_%d_を使用してfloatまたはdoubleの値を読み取ろうとしている人を見るのは珍しいことではありません。彼らは通常、実装が舞台裏で何らかの変換を実行すると信じるのを間違えています。これは、言語の残りの部分で同様の変換が行われるので理にかなっていますが、そうではありません。前に言ったように、scanfと友人(そして実際にはCの残り)は欺are的です。簡潔で慣用的なように見えますが、そうではありません。

経験の浅いプログラマーは、操作の成功を考慮することを強制されませんscanfに_%d_を使用して10進数のシーケンスを読み取って変換するように指示したときに、ユーザーが完全に非数値を入力したとします。このような誤ったデータをインターセプトできる唯一の方法は、戻り値をチェックすることであり、どのくらいの頻度で戻り値をチェックする必要がありますか?

fgetsと同様に、scanfと友人が読むように言われたものを読むことができない場合、ストリームは異常な状態のままになります;-fgetsの場合、完全な行を格納するのに十分なスペースがない場合、未読のままになっている行の残りは、それが新しい行であるかのように誤って扱われる可能性がありますそうでないとき。 -scanfとその友人の場合、上記のように変換が失敗し、エラーデータはストリーム上で未読のままになり、別のフィールドの一部であるかのように誤って処理される可能性があります。

scanfと友人を使用するのは、fgetsを使用するより簡単ではありません。 fgetsを使用しているときに_'\n'_を検索するか、scanfと友人を使用するときに戻り値を調べることで成功を確認すると、 fgetsを使用して不完全な行を読み取るか、scanfを使用してフィールドを読み取れなかった場合、同じ現実に直面します。discard input(通常は次の改行まで、次の改行を含む)!ゆうううううう!

残念ながら、scanfは同時に、この方法で入力を破棄するのを難しく(直感的ではない)かつ簡単にします(最も少ないキーストローク)。ユーザー入力を破棄するというこの現実に直面して、いくつかは試みました scanf("%*[^\n]%*c");、_%*[^\n]_デリゲートは改行のみに遭遇すると失敗することを認識していないため、改行はストリームに残ったままになります。

2つの形式のデリゲートを分離することによるわずかな適応。ここでいくつかの成功が見られます:scanf("%*[^\n]"); getchar();。他のツールを使用して、非常に少ないキーストロークでそれを試してみてください;)

3
autistic

*scanf()ファミリーに関する問題:

  • %sおよび%[変換指定子によるバッファオーバーフローの可能性。はい。最大フィールド幅を指定できますが、printf()とは異なり、scanf()呼び出しで引数にすることはできません。変換指定子にハードコーディングする必要があります。
  • %d、%iなどによる算術オーバーフローの可能性.
  • 不正な形式の入力を検出および拒否する制限された機能。たとえば、「12w4」は有効な整数ではありませんが、scanf("%d", &value);は正常に変換して12をvalueに割り当て、入力ストリームに「w4」を残して将来の読み取りをファウルします。理想的には、入力文字列全体を拒否する必要がありますが、scanf()は簡単なメカニズムを提供しません。

入力が常に固定長の文字列とオーバーフローを発生させない数値で整形式になることがわかっている場合、scanf()は優れたツールです。インタラクティブな入力または整形式であることが保証されていない入力を処理している場合は、別のものを使用してください。

3
John Bode