プロが教えるわが家の防犯対策術!

下記のセキュアコーディングのページの解説が理解し切れず、疑問点についてご教示頂けると幸いです。
宜しくお願いします。
https://www.jpcert.or.jp/sc-rules/c-exp35-c.html

⒈関数の引数で、他の関数呼び出しをすると、その関数の戻り値は代入式で変数に格納しないと消失する、ということなのでしょうか?
下の方の説明に「引数として渡されるすべてのオブジェクトに対してコピーが作られる。」とあるので戻り値もコピーが作られるなら問題ないのではと感じていますが、説明文だけでは理解できず。
2. 1は構造体が配列だけであった場合に起きるのか?ポインタ変数がメンバーであったら問題は起きない?
3.「C では、返り値の生存期間は次の副作用完了点で終了」の次の副作用完了点とは不適合コードのどこを指しているのでしょうか?

A 回答 (8件)

No4です。


> printfの後ではなく呼び出す際に、なぜ消失するのか?がよく分かっておりません。副作用が引数の呼び出し内で発生しても、その計算結果は一時的にはメモリ上に存在し、それをprintf関数の第二引数としてコピーされれば消失しないのでは?と疑問に思っております。

回答に書いたように、printfの引数としてコピーされるのはaddresseeの返り値ではなくてそれに対して .a[0] と ++ を行った結果の整数値です。整数値を取り出してコピーが終わった時点でaddresseeの返り値は不要なので、生存終了です。「生存終了」というのは、その値の入っていたレジスタあるいはメモリ場所がコンパイラによって別目的に再利用可能になるという意味で、あなたの言う「消失」と同じイメージかどうかは分かりません。

> 返される時点での a の値はどういう想定ですかね?
> →自動変数resultのアドレスと思っています。アドレス先は有効範囲のスコープから戻るので不定値になっている理解です。

??
「返される時点でのaの値」というのは、
struct X { int *a; };
struct X addressee(void) {
struct X result;
result.a = ここに何を書く?;
return result;
}
ということですが。

> 表示が目的のコードにみえるのでprintfの仮引数に渡せさえすれば生存が終了しても良いのでは?と思っています。

C99のNG例は、引数は addressee().a なので、これは「関数のローカル変数result の &(result.a)」です。これは実行終了した関数のローカル変数のアドレスなので、printf側で渡されたアドレスを使ってアクセスすることは出来ません。
struct X { char *a; };
struct X addressee(void) {
struct X result;
result.a = malloc(sizeof(char)*6);
result.a[0] = ・・・・・・・;
return result;
}
であればaddressee().aはmallocの返り値なのでそれを使ってのアクセスはOKです。
あるいは、外側の関数への実引数が addressee().a でなくて addressee() であれば、構造体の全体のコピーが作られるのでOKです。

誤解があるのは、「呼び出し時に実引数が仮引数にコピーされる」を考える時に、「具体的に何がコピーされるのか」をきちんと考えていないからかと思います。
実引数がaddressee() ⇒ 構造体全部がコピーされる(コピー元は存在終了だが同じものが仮引数に残っているのでOK)
実引数がaddressee().a ⇒ aの先頭アドレスがコピーされる(aの実体や構造体全体は存在終了)
実引数がaddressee().a[0] ⇒ 整数値がコピーされる(構造体は存在終了)
    • good
    • 0
この回答へのお礼

解決しました

ありがとうございます。
以下を誤解していたので、なかなか理解に至らなかったと思いました。

誤解があるのは、「呼び出し時に実引数が仮引数にコピーされる」を考える時に、「具体的に何がコピーされるのか」をきちんと考えていないからかと思います。

お礼日時:2024/03/01 09:39

>2例目のc99の違反コードでは、インクリメントもないので、変数に入れなくとも成り立っているように感じておりますが、printf関数第二引数に渡す前に消失しているという事でしょうか?



なぜそう思うのか分かりません。
"消失"するなら、消失するという仕様が定まっているので、それを"未定義"とは言いません。
消失するかもしれないし、しないかもしれないし、何が起きるかを仕様として定めてないですよというのが未定義です。
仮に、消失すると言えたり、消失しないと言えたりするのであればそれはそう定義されてるのだから未定義では無いです。

例えばC言語ではポインタを指定すれば好きなアドレスの値を好きに参照できます。しかしこれはプログラムで使うと宣言した領域ではありません。
生存期間が終了しているのに参照するというのは正にこういう事です。

インクリメントとか関係無いです。

※もっと単純な話です。未定義の意味の理解と危険なポインタの参照の理解が足りてません。
C言語では他のプロセスで利用中のメモリに不正アクセスすることも出来ます。それはリンク先で「生存期間」と称されています。

例えば、宣言した値を初期化せずに参照した時の値は未定義です。ランダムかもしれないし一定かもしれないし0かもしれないです。
そういう基本的な部分の理解が大切です。
    • good
    • 0
この回答へのお礼

助かりました

ご見解ありがとうございます。
語彙力がなく、消失という表現を使用していたのは、
顧みて改めたいと思います。

他の方の回答も踏まえて、自身の疑問について解決致しました。ご助言下さりありがとうございました。

お礼日時:2024/03/01 09:43

1. 一般に関数呼び出しの返値を利用して、別の関数の引数を作る場合


返値のメンバの値を取り出して別の関数の引数を作った時点で元の返値は必要なくなるので廃棄される可能性がありますね。
違反コードの例は、++(addressee().a[0]) を (addressee().a[0])+1 にすれば良いと思いますが、なんでわざわざすぐ破棄する一時オブジェクト(addressee()の返値)を更新するのでしょうね。
C99違反コードの方が分かりやすいですね。addressee().aは一時オブジェクト内の配列のアドレスを指していますが、printfが呼び出された時点では一時オブジェクトの寿命が尽きていますので懸垂参照です。何を表示するかわかったものではありません。
2. 「一時的な生存期間を持つオブジェクトを変更すると未定義の動作」からすると配列やポインタ変数を変更するのはダメだけど、ポインタ変数の参照先については何も言っていないかと。参照先が十分な寿命を持つオブジェクティビティーなら言語仕様上は問題ないはず。よほど気を付けた実装をしないとメモリリークしそうな怪しげなコードではあるけど。
3. 言語仕様を確認しないとハッキリしないけど、最短だと次の引数の評価を始める時点で返値の寿命は尽きるかも。

サンプルコードでは関数の返値自体を外側の関数の引数にしているのではなく、その一部の値を引数にしているのがミソでしょう。返値の構造体自体を引数に渡すなら構造体のコピーが作られ、それは外側の関数から返るまで有効なオブジェクトなので問題ないと思われます。
    • good
    • 0
この回答へのお礼

ありがとうございます。
他の方の回答も踏まえて、解決致しました。
誠に有難うございます。

お礼日時:2024/03/01 09:41

補足


未定義動作っていうのはどういう実装でもいいよということで、もしも「消失する」という「仕様」なのであればそれは未定義では無いです。
消失するかもしれないし、しないかもしれないから未定義で何が起きるか定義されてないから未定義です。
    • good
    • 0

これが未定義動作になっているのは、


printf("%x %x\n", ++(addressee().a[0]),++(addressee().a[1]));
のようなコードに対して、addressee() の呼び出しを一回で済ませる最適化を可能とするためかもしれません。addressee は副作用の無い関数ですので。

gcc -std=c90だと、「addressee().a は左辺値じゃないので添え字を付けられない ISO C90 forbids subscripting non-lvalue array」という警告が出ますね。

re: 1
消失するというか、printfの後では残っていません。そういう質問ですか?

> 「引数として渡されるすべてのオブジェクトに対してコピーが作られる。」とあるので戻り値もコピーが作られるなら問題ないのでは

printf関数の内部で扱う仮引数に渡された整数値はコピーですが、printfの呼び出し側(このmain上の記述)で扱う配列はコピーでは無いです。addresseeの返り値を直接加工しようとしています。

re: 2
struct X { char *a; }; ということですかね? 返される時点での a の値はどういう想定ですかね? addresseeのローカルなauto変数でなく、ヒープ上やグローバルやstaticの変数を指すなら、その指された変数は多分書き換え可能だと思います。返り値はポインターのaだけなので。

re: 3
副作用点は、printfの引数が全て評価された直後で、printfが呼び出される直前です。printfの仮引数にa[0]を+1した値がコピーされた時点で、コピー元の「addresseeの返り値」は生存を終了します。
    • good
    • 0
この回答へのお礼

見解ありがとうございます。

消失するというか、printfの後では残っていません。そういう質問ですか?
→printfの後ではなく呼び出す際に、なぜ消失するのか?がよく分かっておりません。副作用が引数の呼び出し内で発生しても、その計算結果は一時的にはメモリ上に存在し、それをprintf関数の第二引数としてコピーされれば消失しないのでは?と疑問に思っております。

返される時点での a の値はどういう想定ですかね?
→自動変数resultのアドレスと思っています。アドレス先は有効範囲のスコープから戻るので不定値になっている理解です。

printfの仮引数にa[0]を+1した値がコピーされた時点で、コピー元の「addresseeの返り値」は生存を終了します
→上記と同じ疑問で、表示が目的のコードにみえるのでprintfの仮引数に渡せさえすれば生存が終了しても良いのでは?と思っています。また、2例目のc99の違反コードでは、インクリメントもないのでさらに疑問を感じている次第です

お礼日時:2024/02/28 09:15

多分もっと単純な話かと?間違っていたらすみませんお断りを入れておきます。



1,リンク先の説明が正しければ、次の副作用が起きる際に戻り値のアドレスは不定になるので、変数宣言をして格納しましょうと書いてある(生存期間を伝えるために)。
ポインタのコピーをしたあとに、アドレスの値が自由になってしまう(生存期間が終わる)ので、自由になる前に変数に入れて、という話かと。

2,リンク先の説明が正しければポインタ全般で起こり得る、変数であっても。

3,どこかは多分気にしなくてよくて、変数宣言をして生存期間を伝える以外のことを先にするな、という趣旨の話。
print関数に引数として渡すことかもしれないし、インクリメントすることかもしれないし。ある意味そのどちらでもあるかもしれないし。
ま、あくまでも「定義されていない挙動」であるだけで、何も「全くありえないこと」が起きる訳では無い。


蛇足
正直質問文だけじゃどう言う前提を読み飛ばしているのか?があんまり分からないし、そもそも前提を理解した上でこういう質問をしている?ような気もします。

C言語においてデータの塊は全部ポインタですし、データの塊をまるまるコピーすることは、基本文法上は無い、参照渡しを行う
ということくらいは念の為書いておく

※間違ってたらメンゴ^^
返信ウェルカム
    • good
    • 0
この回答へのお礼

ご見解ありがとうございます。

ポインタのコピーをしたあとに、アドレスの値が自由になってしまう(生存期間が終わる)ので、自由になる前に変数に入れて

2例目のc99の違反コードでは、インクリメントもないので、変数に入れなくとも成り立っているように感じておりますが、printf関数第二引数に渡す前に消失しているという事でしょうか?

データの塊をまるまるコピーすることは、基本文法上は無い、参照渡しを行う
→文法のお作法としてこのあたりは理解していますが、前提知識の記載がなく失礼しました。

お礼日時:2024/02/28 09:29

う〜ん、サッパリ分からん(笑)!


まぁ、Cの専門じゃないから当然だけど(と逃げる)。

ただ、実は貴方の参照してるページのこれまた参照してる根拠(ISO/IEC TR 24772:2013)ってのがこれまた古いんだよ(笑)。調べてみたら失効してる(笑)。
今だと

ISO/IEC TR 24772-1:2019 | ISO/IEC TR 24772-2:2020 | ISO/IEC TR 24772-3:2020 | ISO/IEC WD TR 24772-4 | ISO/IEC WD TR 24772-6 | ISO/IEC AWI TR 24772-8.2

の4つを根拠にせなアカンの?つまり、文書として古い。

加えると、これの意味が分からんのだよ。

> C 標準には、一時的な生存期間を持つオブジェクトを変更すると未定義の動作となると記載されている。この定義は C99 標準の仕様とは異なる。

ここで言う「C標準」ってのが何か、って話で。
恐らく、日本のフツーの人(Cプログラマ)が思い浮かべる「ANSI C」ではないんだ。
恐らく、時期的には、ISOのC11を指してるんじゃなかろうか。
つまり、意図してるのは、

> C11 には、一時的な生存期間を持つオブジェクトを変更すると未定義の動作となると記載されている。この定義は C99 標準の仕様とは異なる。

って事なんじゃないか。
すなわち、貴方が仮に、Microsoftの処理系を「そのまま」使ってるとしたら、ANSI Cだろうから、ここで言う「セキュアなプログラム」はどのみち書けない、って事になる。セキュアなプログラムを書こうと思えば書けるだろうけど、ここに記載されている方法論ではない、って考えておいた方がいい。
仮に

Visual Studio に C11 および C17 サポートをインストールする:
https://learn.microsoft.com/ja-jp/cpp/overview/i …

に従っていたとしたら、C99は「使えない」けど、一応、C11にしてたらその文書の言ってる/やってる事が「結果として」見れるんじゃないか。
一方、GCCとかClangとかだと多分現時点ではデフォ動作がC17になってるだろうから、-Wall入れても特に文句が出てこないんだよな。その場合はオブションで-std=c99とか-std=c11とかにしてみて、Warningが出るかどうか調べていけばいい・・・んだけど、こっちでやった以上特に出なかったんだよな(笑)。
ってぇこたぁ、仕様の不具合、って事でC17辺りで改良されてんじゃね?読んでねぇけど(笑)。また、だからこそ2019〜2020年に渡って、ISOの方で、この元ネタの「セキュアなコードの書き方」の指針が変わった、とかそういう事じゃないかしらん。

ちなみに、日本の場合、JIS規格のC言語、ってのは依然C99だ。

さて、まず用語の問題で言うと、「副作用」なんだけど、単純な言い方をすると、「プログラム上、計算と関係ない全て」だ。代表的なトコに「入出力」、そして「代入」がある。
ビックリするかもしんないけど、実は「代入」は「計算」じゃないんだ。具体的に言うと、特にCだと「メモリに値を登録する事」になるんで、「あ、確かにそれは計算じゃないな」って分かるだろう。従って、インクリメントやデクリメントは「計算」ではない。副作用だ。

とすると、だ。

「C では、返り値の生存期間は次の副作用完了点で終了する。したがって、printf() が呼び出された時点では、addressee() 呼び出しが返した構造体が有効ではなくなっており、データが上書きされているかもしれない。」

これは一体何を意味してるのか。

ちなみに、printf()は「副作用目的の関数」だ。実はどうやらあまり知られてないんだけど、printf()自体には返り値が存在する。つまり「printf()の結果を」変数に代入可能だ。しかし、printf()を使う理由、っつーか、人々は返り値の方には全く興味がなくて、「副作用」である、印字に興味があるわけだよな。

さて、問題は「副作用完了点」と言う用語だ。一体何だこれは、って事なんだけど。
JISのC仕様書

X3010-2003:
https://kikakurui.com/x3/X3010-2003-01.html

では付属書Cで「副作用完了点」に付いて解説してる、的な事を目次で書いてんだけど、その「附属書が付いてる」筈の286ページでは、「ISO/IEC 9899:1999のAnnex Cを見ろ」って書いてある(笑)。クソ、役に立たねぇじゃねぇか(笑)。
当然、ISOは「仕様書を売ってる」ワケで、しかも英語だ(笑)。何だこれ(怒

しかし、用語的には「副作用完了点」は英語の「Sequence Point」の訳語だ、って事が分かる・・・・・・酷い訳語だよな、ヲイ。
んでまぁ、色々調べてみたら、こういうのを見つけた。

"A sequence point is a point in the program execution sequence at which all previous side effects of execution are to have taken place ad at which no subsequent side effects will have occurred."

日本語に訳せばこんなカンジだろう。

「副作用完了点とは、プログラム実行シーケンスにおいて、それまでのすべての実行の副作用が発生し、その後の副作用が発生しないポイントです。」

つまりだな、端的に言うと、関数に与える「引数」ってのは副作用完了点の一つだ、って事だ。
関数は原理的に引数として、「関数の演算結果」を取る事が出来る。

例: printf("%x\n", foo(何とやら));

当然その引数になる「関数」は副作用目的の場合もあり得るんだよ。上の例だと、fooが中で何やってるか分からん。当然例のような「構造体でのデータ定義」に関して中で副作用、言っちゃえば「代入」で値が変わってるかもしんない。そしてそれは仕様上では「未定義だ」って事だ。
未定義を引数に与える、ってのは当然おかしいんだけど、実装によってはO.K.だったりそうじゃない場合もある、と。そういう「結果が実装によって変わるようなプログラムは安全じゃねぇだろ?」って事なんじゃないか、言ってる事は。

まぁ、副作用完了点、ってのをザーッと調べたカンジ、多分こんな事を言ってるんだろうな、って事を書いてるだけなんで、見当違いかもしんない。

いずれにせよ、「副作用完了点」を調べたい、って思うのなら、多分次の本に丁寧に解説が書いてると思う。上の英文も、結果「間接的に」この本から探し出してきた結果だ。

Cリファレンスマニュアル 第5版:
https://www.amazon.co.jp/S%E3%83%BBP%E3%83%BB%E3 …

今だと、少なくとも日本では、K&Rを買うよかコッチの本を買うべきだ。
仕様書を買えない、とか言うんでも、詳細を解説してるリファレンスマニュアルとかないと、多分このテの「動作」は想像付かないんじゃないか、って思う。
    • good
    • 1
この回答へのお礼

色々調べて下さったのが伝わって参りました。
ありがとうございます。参考にさせて頂きます

お礼日時:2024/02/28 09:16

関数の返値は一時オブジェクトなのでメンバを変更できない。

C99ではメンバへの参照もできない。しかし関数の返値を変数に代入したら、それは一時オブジェクトではないので普通にメンバにアクセスできる。
不適合コードは関数の返値のメンバに++していますので、一時オブジェクトたる構造体を書き換えていて違反ですね。それをprintfの引数にしていることが違反なのではないです。
    • good
    • 0
この回答へのお礼

ありがとうございます。
つまり決まり事として理解しておくしかないということでしょうかね?
一時オブジェクトを変更しても、また一時オブジェクトとして作られて…となれば消失しないように感じており、なぜそうはならないのか仕組みが可能であれば知りたいです

お礼日時:2024/02/28 09:20

お探しのQ&Aが見つからない時は、教えて!gooで質問しましょう!

このQ&Aを見た人はこんなQ&Aも見ています


このQ&Aを見た人がよく見るQ&A