風柳メモ

ソフトウェア・プログラミング関連の覚書が中心

正常系・異常系共通の後処理を行うための手法検討と、「途中return禁止」「goto禁止」等の“一律禁止”問題

たいたい竹流さん @ガード節に関するツイートについて、

のようにリプライしたところ、

のようにコメント頂いた。


これに端を発した議論が有意義に思えたので、自分用の覚書として要点をまとめておく。

■ ガード節について

ガード節そのものについては、自分は最初から積極的に賛同している立場であるため、この記事では特記しない。


ガード節の有効性については、下記記事の引用部分に集約されているように思う。


これは、「ガード節による入れ子条件記述の置き換え」と呼ばれるもので、リファクタリング本では次のように説明されている。


「ガード節による入れ子条件記述の置き換え」のキーポイントを強調しておきましょう。if-then-else 構造が使われる時は、if 部にも else 部にも同じウエイトが置かれています。これは、プログラムの読み手に対して両方共等しく起こり得ること、等しく重要であることを伝えます。逆に、ガード節は『めったに起きないが、起きた時には、何もしないで出ていく』ことを伝えます。


リファクタリング - 第九章 条件記述の単純化 p.251

小野和俊のブログ:ガード節を用いた if-then-else 文の置き換え

■ 自分の発言の趣旨

上記であげた自分のリプライについてだが、もともと、複数のreturnを設けたり、Wrapper関数化するのに消極的だったのは、主にデバッグ上の要求から。

  • 複数 return があると、そのすべてにブレイクポイントやログ出力処理などを設ける必要が出てくるので、出口は一つにしたい
  • Wrapper化により異常系/正常系処理と共通の後処理とを切り離してしまうと、後処理内で異常系/正常系のローカル変数を参照できない
  • 後処理を共通化するためだけの単純なWrapperを設けるのは、余分にコールスタックが増えるということであり、デバッグ時にコールスタックを表示させた際、ソースコードにおいてネストが深くなるのと同等の見通しの悪さが生じる
  • Wrapperのために関数名が増えるのは面倒(苦笑)(言うまでもなく、これは副次的な理由)


なお、「ルールだから」という理由で途中 return を設けないわけではなく、デバッグ上の理由で消極的なだけなので、「必ず共通の後処理が実行される」保証があれば、途中で return することについては特に忌避感はない。

■ try-catch-finally で万全

実は、Java 等の try-catch-finally 相当の構文が使用できる言語であれば、次のように記述することで満足できる。

try {
    result = ABNORMAL;
    // [I] 共通の前処理(スコープ内で使用するローカル変数設定等)
    
    if ( /* 異常系判定条件 */ ) { // ガード節
        // [A] 異常系処理
        return result;
    }
    // ※状況に応じて複数のガード節を記述
    
    // [N] 正常系処理
    result = NORMAL;
    return result;
}
catch (Throwable errorObject) {
    // [E] 想定外の異常系(例外)処理
    throw errorObject;
}
finally {
    // [F] 共通の後処理
    // ※デバッグ用に、この部分にローカル変数を参照するログ出力処理等を入れたり
    //   条件付きブレイクポイントを設定したりできる
}

try-catch-finallyは、try節中等に return があっても必ず finally 節が実行されるため、ブレイクポイントやログ出力処理などもここにまとめることができ、Wrapperを用意する必要もなくなり、上記の要求は全て満たされる。


■ try-catch-finally の類が使えない場合の代替手段

問題は、try-catch-finally のような「途中の経過に関わりなく後処理が実行される」という機能を持たない言語の場合。
たいたい竹流さんが募集して下さった意見などを元に、いくつかの代替手段とその問題点等をあげる。

◇ 一度のみ実行するループを用いた方法
result = ABNORMAL
// [I] 共通の前処理(スコープ内で使用するローカル変数設定等)

<ループ> {
    if ( /* 異常系判定条件 */ ) {
        // [A] 異常系処理
        break
    }
    
    // [N] 正常系処理
    result = NORMAL
    break
}
// [F] 共通の後処理
return result

for(;;){…} や while(true){…}、do{…}while(false)を使う手法。


問題点

  • 初見の人には意図が判り辛い(do{…}while(false)であれば多少まし、ただし、言語によっては定数falseを使うことによる警告も)*1
  • 一度しか実行されないループ→ループとして機能していない→構造化構文の誤用
◇ goto を用いた方法
result = ABNORMAL
// [I] 共通の前処理(スコープ内で使用するローカル変数設定等)

if ( /* 異常系判定条件 */ ) {
    // [A] 異常系処理
    goto finally
}
    
// [N] 正常系処理
result = NORMAL

finally:
// [F] 共通の後処理
return result 


問題点

◇ 関数内関数を用いた方法
result = ABNORMAL
// [I] 共通の前処理(スコープ内で使用するローカル変数設定等)

(<関数>(){
    if ( /* 異常系判定条件 */ ) {
        // [A] 異常系処理
        return;
    }
    
    // [N] 正常系処理
    result = NORMAL
    return;
})()

// [F] 共通の後処理
return result


問題点

  • 一見してあえて関数内関数を使う意図が判り辛い
  • 本来スコープ内に閉じ込めるべきローカル変数を外出ししている(必要な変数をすべて引数として渡す、という方法もあるが、それならば最初から外部関数なりにするのと変わらず、内部関数にするメリットがない)
◇ classのデストラクタを利用した方法
<クラス定義> {
    result = ABNORMAL
    // [I] 共通の前処理(スコープ内で使用するローカル変数設定等)

    <メイン処理>(…) {
        if ( /* 異常系判定条件 */ ) {
            // [A] 異常系処理
            return result
        }
    
        // [N] 正常系処理
        result = NORMAL
        return result
    }

    <デストラクタ>(…) {
        // [F] 共通の後処理
    }
}


問題点

■ コーディングルールにおける「一律禁止」問題

上記の議論に付随して、「コーディングルールにおける、思考を停止した『一律禁止』は問題である」という話も出た。
知らず知らずのうちに思考停止に陥っていることが多いので、自戒を込めてまとめる。


ルールには、もちろんそれが制定された意味はある(場合がほとんどだ)けれども、盲目的に従うのではなく、歴史的な経緯を把握した上で、現状と照らし合わせてきちんと考えようということだと思う。
とはいえ、クライアントに要求されたルールに従わざるを得ない等、なかなか理想通りには行かないことも多いのだけれど……せめて吟味し批判する姿勢は保ちたいもの。


等々。

◇「途中return禁止」ルール

もともとのガード節に関するツイートに対し、「出口が複数あって却って見づらい」旨の否定的な意見受けて

ここで注意すべきなのは、構造化プログラミングの文脈では、「1つの入り口と1つの出口を持つ、順次・選択・反復の3つの論理構造の組み合わせ」というスタイルはプログラム実行時のフローをソースコードから追いやすくするために用いられている、という点だ。

MISRA-Cにおける「関数の末尾以外の return 禁止」の真意 - 新・日々録 by TRASH BOX@Eel

関数内での途中returnは、大抵がネストを浅くする――つまり静的構造自体が複雑化することを避ける目的で使用されることが多いはずだ。構造化プログラミングでは考慮されていない部分なのだ。

こうなってくると、基本的には「1つの入り口と1つの出口を持つ、順次・選択・反復の3つの論理構造の組み合わせ」というスタイルをとりつつも必要に応じてルールを破る、というスタイルに落ち着く。

MISRA-Cにおける「関数の末尾以外の return 禁止」の真意 - 新・日々録 by TRASH BOX@Eel
◇「goto禁止」ルール

上述した「一度のみ実行するループを用いた方法」に関して、そういった(自分でも自覚しているような)トリッキーな方法を使うのではなく、gotoを使うべき、という意見

タイムリーな記事もあった。

200万近いC言語のファイルと1万1千件を超えるプロジェクトからランダムに抽出した統計的に有効なサンプルを質的および量的に分析したところ、開発者はほとんどの場合gotoの使用を適切に制限しており、Dijkstra氏が懸念したような無制限な使用は行われていないことが判明した。これらのことから、実際にはgotoは有害でないものと考えられる。

C言語の開発者によるgoto文の使い方を対象とした実証研究の結果、「goto文は無害だと考えられる」 - エキサイトニュース

「goto一律禁止」のルールで作られているものはそもそもgoto文が現れないので抽出されないだろうし、そうでない場合は思考停止すること無くよく考えて使う開発者の割合が高くなるような気もするので、この結果を鵜呑みにして実際に解禁したら無制限な使用が増える、という可能性も否定できない気はするけれど。

*1:自分の場合は最初見た例がdo{…}while(false)だったために割とすんなり理解できたし、その土台があったため、警告対策でfor(;;){…}に切り替えた際も特に違和感を感じなかったという経緯がある

究極超人あ〜るネタを調べていたらFreeBSDのカレンダーコマンド(ncal)の不具合に行き当った件

ちょっと、2月のカレンダーと「14日の土曜日」ネタがらみで、

のようなことをつぶやきたいな、と思って、念のため調べていたら、いつの間にかFreeBSDのncalコマンドバグなのかも?と思われるものを見つけてしまった、という話。
いや、我ながら何を言っているのかわかりませんが…。

■ 経緯

◇ 13日の金曜日よりも14日の土曜日が重要なクラスタより
  • タイムライン上に「そういえば13日の金曜日」のようなツイートがちらちら
  • すると「14日は土曜日ではないか!」と反応しはじめる、偏った人々(ゆうきまさみフォロワーの方ね)
◇ 2月で「14日の土曜日」だった年を調べたところ…

『あ〜るのバレンタイン話は、確か1987年だったよなぁ』

$ cal 2 1987
      21987
日 月 火 水 木 金 土
 1  2  3  4  5  6  7
 8  9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28

『うんうん。えーと、今年はそれから何回目?ついでに西暦1年から調べてみるか』

$ python
>>> import calendar
>>> all, counter = 0, 0
>>> for year in range(1, 1+2015):
...   if calendar.weekday(year, 2, 14) == 5:
...     all += 1
...     if 1987 < year: counter += 1
...     print year
...
4
9
15
:(中略)
1750
1756
:(中略)
1981
1987
1998
2004
2009
2015
>>> print all, counter
288 4
>>>

『ほぉ、西暦4年もそうだったのか』

$ cal 2 4
        24
日 月 火 水 木 金 土
                1  2
 3  4  5  6  7  8  9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29

『あ、あれれ……?』

(中略)

$ cal 2 1750
      21750
日 月 火 水 木 金 土
             1  2  3
 4  5  6  7  8  9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28

『んん…?』


― 間 ―

$ cal 2 1756
      21756
日 月 火 水 木 金 土
 1  2  3  4  5  6  7
 8  9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29

『あ、これはちゃんと14日の土曜日になってるな。このあたりが境目、か……?』

◇ 原因は?
$ man cal
CAL(1)                  FreeBSD General Commands Manual                 CAL(1)

NAME
     cal, ncal ― displays a calendar and the date of Easter

(中略)

     -s country_code
             Assume the switch from Julian to Gregorian Calendar at the date
             associated with the country_code.  If not specified, ncal tries
             to guess the switch date from the local environment or falls back
             to September 2, 1752.  This was when Great Britain and her
             colonies switched to the Gregorian Calendar.

なる程、デフォルトだと、イギリスがグレゴリオ暦を採用した1752年9月14日以降がグレゴリオ暦表示で、それ以前はユリウス暦表示なのね。

$ cal 9 1752
      91752
日 月 火 水 木 金 土
       1  2 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30

ほうほう。グレゴリオ暦での1752年9月13日はユリウス暦1752年9月2日なので、こうなるのね。

◇ 国名コードを指定するとどうなる?

これ、他の国だとどうなるんだろう?

略号一覧

ISO 3166-1 - Wikipedia

コマンドの -s オプションで指定できる国名コード(country_code)は二字のもの、ただし、網羅されているわけではない。


そもそも、イタリア(1582年10月15日グレゴリオ暦制定)だと?

《参考資料》ユリウス暦からグレゴリオ暦への切り替え
・1582年にローマ法王グレゴリオ13世がユリウス暦からグレゴリオ暦への転換を宣言したことにより、この時期にユリウス暦からグレゴリオ暦への切り替えが行われた国があります。
・カトリック国のイタリア、スペインなどでは1582年10月に切り替えが行われ、1582年10月4日の翌日が10月15日になり、1582年10月5日から10月14日までは存在しません。

みんなの知識【ちょっと便利帳】 - その曜日は何日? 各年・各月の曜日を調べる - 西暦と月を設定し、各曜日の日付を一覧表示
$ cal -s IT 10 1582
Usage: cal [general options] [-hjy] [[month] year]
       cal [general options] [-hj] [-m month] [year]
       ncal [general options] [-hJjpwy] [-s country_code] [[month] year]
       ncal [general options] [-hJeo] [year]
General options: [-NC3] [-A months] [-B months]
For debug the highlighting: [-H yyyy-mm-dd] [-d yyyy-mm]

あ、-s オプションは ncal だけか。

$ ncal -s IT 10 1582
    1015821 18 252 19 263 20 274 21 2815 22 2916 23 3017 24 31

うん、確かに10/15を境に切り替わっているな。
じゃあ、日本だとどうなの?

・1873年1月1日に当たる明治5年12月3日(旧暦)を明治6年1月1日(新暦)とする太陽暦への改暦(明治改暦)。

グレゴリオ暦 - Wikipedia
$ ncal -s JP -A 1 12 1872
    121872          118734 11 18 25     1  8 15 22 295 12 19 26     2  9 16 23 306 13 20 27     3 10 17 24 317 14 21 28     4 11 18 251  8 15 22 29     5 12 19 262  9 16 23 30     6 13 20 273 10 17 24 31     7 14 21 28

ん?日付は連続しているし……

$ ncal -s IT -A 1 12 1872
    121872          118732  9 16 23 30     6 13 20 273 10 17 24 31     7 14 21 284 11 18 25     1  8 15 22 295 12 19 26     2  9 16 23 306 13 20 27     3 10 17 24 317 14 21 28     4 11 18 251  8 15 22 29     5 12 19 26

イタリアの暦とも違っている、ということは、このあたりはまだユリウス暦で表示されている、ということ?

◇ ncalでは、日本はいつグレゴリオ暦に切り替えたことになっているのか?
$ ncal -s JP -A 1 12 1918
    121918          119193 10 17           6 13 20 274 11 18           7 14 21 285 12           1  8 15 22 296 13           2  9 16 23 307 14           3 10 17 24 311  8 15           4 11 18 252  9 16           5 12 19 26

は、はい……?
1919年1月1日から切り替えたことになっているのか……なぜ???

結論(暫定)

結局、自分が調べた範囲では理由がわからず、単純なプログラム上の登録ミスではないか? と考えている。
実は深い理由があるのかも……ご存知の方、教えてください。


FreeBSDのncalコマンドソースコード中で、

[base] Contents of /head/usr.bin/ncal/ncal.c

69	/* The switches from Julian to Gregorian in some countries */
70	static struct djswitch {
71	        const char *cc; /* Country code according to ISO 3166 */
72	        const char *nm; /* Name of country */
73	        date dt;        /* Last day of Julian calendar */
74	} switches[] = {
75	        {"AL", "Albania",       {1912, 11, 30}},
76	        {"AT", "Austria",       {1583, 10,  5}},

のように、ISO 3166の国名コード(2文字)毎にユリウス暦→グレゴリオ暦に移り変わった日付の定義がなされているが、そのうち、日本(JP)用の定義が違っているのではないか、と。

$ diff -c ncal.c.r241737 ./ncal.c
*** ncal.c.r241737      2015-02-13 02:24:25.000000000 +0900
--- ./ncal.c    2015-02-13 02:26:03.000000000 +0900
***************
*** 91,97 ****
        {"HU", "Hungary",       {1587, 10, 21}},
        {"IS", "Iceland",       {1700, 11, 16}},
        {"IT", "Italy",         {1582, 10,  4}},
!       {"JP", "Japan",         {1918, 12, 18}},
        {"LI", "Lithuania",     {1918,  2,  1}},
        {"LN", "Latin",         {9999, 05, 31}},
        {"LU", "Luxembourg",    {1582, 12, 14}},
--- 91,97 ----
        {"HU", "Hungary",       {1587, 10, 21}},
        {"IS", "Iceland",       {1700, 11, 16}},
        {"IT", "Italy",         {1582, 10,  4}},
!       {"JP", "Japan",         {1872, 12, 19}},
        {"LI", "Lithuania",     {1918,  2,  1}},
        {"LN", "Latin",         {9999, 05, 31}},
        {"LU", "Luxembourg",    {1582, 12, 14}},
$ # 改修版の実行結果
$ ./ncal -s JP -A 1 12 1872
    121872          118734 11 18           6 13 20 275 12 19           7 14 21 286 13           1  8 15 22 297 14           2  9 16 23 301  8 15           3 10 17 24 312  9 16           4 11 18 253 10 17           5 12 19 26
◇ 注釈

そもそも日本の場合、べつにグレゴリオ暦の前にユリウス暦を使っていたわけではないので、定義するのはナンセンス、という話はある。
その上で、元のソースコード中で「グレゴリオ暦1919年1月1日」(厳密にはその前日に当たるユリウス暦1918年12月18日)が登録されている根拠もよくわからず、まだこれよりは、明治改暦が実施された「グレゴリオ暦1873年1月1日」(同1872年12月19日)を登録した方がもっともらしいのではないか、と考える次第。

■ 補足等

◇ ncal のバグについて

そもそもncal中の国別のユリウス暦→グレゴリオ暦切替日定義は、日本以外に関しても信頼できるかどうかは怪しい、のかも。
ncal(1)

BUGS
The assignment of Julian--Gregorian switching dates to country codes is
historically naive for many countries.

https://www.freebsd.org/cgi/man.cgi?query=ncal&sektion=1&manpath=FreeBSD+6.0-RELEASE
$ ncal -p
 AL Albania        1912-11-30      IT Italy          1582-10-04
 AT Austria        1583-10-05     *JP Japan          1918-12-18
 AU Australia      1752-09-02      LI Lithuania      1918-02-01
 BE Belgium        1582-12-14      LN Latin          9999-05-31
 BG Bulgaria       1916-03-18      LU Luxembourg     1582-12-14
 CA Canada         1752-09-02      LV Latvia         1918-02-01
 CH Switzerland    1655-02-28      NL Netherlands    1582-12-14
 CN China          1911-12-18      NO Norway         1700-02-18
 CZ Czech Republic 1584-01-06      PL Poland         1582-10-04
 DE Germany        1700-02-18      PT Portugal       1582-10-04
 DK Denmark        1700-02-18      RO Romania        1919-03-31
 ES Spain          1582-10-04      RU Russia         1918-01-31
 FI Finland        1753-02-17      SI Slovenia       1919-03-04
 FR France         1582-12-09      SW Sweden         1753-02-17
 GB United Kingdom 1752-09-02      TR Turkey         1926-12-18
 GR Greece         1924-03-09      US United States  1752-09-02
 HU Hungary        1587-10-21      YU Yugoslavia     1919-03-04
 IS Iceland        1700-11-16

※ラテン世界の9999年

$ ncal -s LN 9999
                                  9999
    1234月
月   1  8 15 22 29        5 12 19 26        5 12 19 26        2  9 16 23 302  9 16 23 30        6 13 20 27        6 13 20 27        3 10 17 243 10 17 24 31        7 14 21 28        7 14 21 28        4 11 18 254 11 18 25        1  8 15 22        1  8 15 22 29        5 12 19 265 12 19 26        2  9 16 23        2  9 16 23 30        6 13 20 276 13 20 27        3 10 17 24        3 10 17 24 31        7 14 21 287 14 21 28        4 11 18 25        4 11 18 25        1  8 15 22 29

    5678月
月      7 14 21 28                                           16 23 301  8 15 22 29                                           17 24 312  9 16 23 30                                           18 253 10 17 24 31                                           19 264 11 18 25                                           13 20 275 12 19 26                                           14 21 286 13 20 27                                           15 22 29

    9101112月
月      6 13 20 27        4 11 18 25     1  8 15 22 29        6 13 20 277 14 21 28        5 12 19 26     2  9 16 23 30        7 14 21 281  8 15 22 29        6 13 20 27     3 10 17 24        1  8 15 22 292  9 16 23 30        7 14 21 28     4 11 18 25        2  9 16 23 303 10 17 24        1  8 15 22 29     5 12 19 26        3 10 17 24 314 11 18 25        2  9 16 23 30     6 13 20 27        4 11 18 255 12 19 26        3 10 17 24 31     7 14 21 28        5 12 19 26
◇ 覚書
  • FreeBSD の cal コマンドは ncal コマンドのエイリアス
    バイナリレベルで同一。コマンド名で判別し、ncalの -C オプション相当の表示をしている模様。
◇ 愚痴

というかそもそも、「とある年月を境にしてユリウス暦とグレゴリオ暦が切り替えられ、かつ、一見して(表示結果では)それとわからない」なんて仕様のカレンダー自体、やめて欲しいと思うのは自分だけ?
「デフォルトでグレゴリオ暦換算のカレンダーを表示、オプションでユリウス暦換算でも表示可能だよ」でええやん…。

萌えるステータスコード 〜 生徒会長編 〜

無味乾燥なHTTPステータスコードに、潤いを。
麗しの生徒会長*1の台詞を通して、理解を深めよう!*2
何番煎じだ、とかいわない。
HTTPステータスコードの本来の意味に対して、台詞の内容に違和感がある(解釈がおかしい)等のコメントを緩募。

1xx Informational 情報

Code Reason Phrase 台詞
100 Continue ええ、そのままで続けて。
101 Switching Protocols 解ったわ、言われた手順に変えてみるわね。
102 Processing (WebDAV) その件なら処理中よ。

2xx Success 成功

Code Reason Phrase 台詞
200 OK いいわ。はい、これ。
201 Created うん、できたわ。これね。
202 Accepted ひとまず、受け取っておくわ。
203 Non-Authoritative Information 不確かな噂なのだけれど……。
204 No Content バインダーに何も挟まっていないわ。
205 Reset Content 解ったわ。あ、次の人のために、ホワイトボードは消しておいてね。
206 Partial Content その範囲だと……これだけかしら。
207 Multi-Status (WebDAV) いくつか頂いていた案件について、まとめておいたわ。
208 Already Reported (WebDAV) その件は、もう報告済みだけれど……ほら、ここに、ね?
226 IM Used 前回から加筆修正された箇所? そうね、この辺りかしら。

3xx Redirection リダイレクション

Code Reason Phrase 台詞
300 Multiple Choices どれかを選んでちょうだい。
301 Moved Permanently それなら、あちらの部屋に移したわ。
302 Found あ、あったわ。あちらを見てもらえるかしら。
303 See Other それはここじゃなくて、他をあたって。紹介状は書かせてもらうわ。
要件は承ったわ。次はこちらに行ってもらえる?
304 Not Modified 前に伝えた通りよ。
305 Use Proxy その件は委員会を通してちょうだい。
306 (Unused) (今はまだ、なにも言えない。)
307 Temporary Redirect あ、それは今だけ、そちらの棚に移してあるの。
308 Permanent Redirect この書類のままでいいから、あちらに提出しなおしてもらえる?

4xx Client Error クライアントエラー

Code Reason Phrase 台詞
400 Bad Request 不正は許されないわ。
401 Unauthorized あなたに、どんな権限があるというの?
402 Payment Required それには、予算の計上が必要なの。
403 Forbidden それは禁止されているでしょう?
404 Not Found 変ね、見つからないけれど?
405 Method Not Allowed そんな方法は認められません。
406 Not Acceptable 書類に不備があるから、受領はできないわ。
407 Proxy Authentication Required その件については、委員会の承認が必要よ。
408 Request Timeout 時間切れ、ね……。
409 Conflict そうじゃないでしょう、矛盾しているわよ。
410 Gone 困ったわ、どうやら紛失してしまったみたい……。
411 Length Required その書類、最初に何ページあるかを書いておいてね。
412 Precondition Failed そもそもの前提が間違っているわ。
413 Request Entity Too Large ごめんなさい、時間もないので、要点だけにしてもらえる?
414 Request-URI Too Long 枠からはみ出してしまっていて、よくわからないわ。
415 Unsupported Media Type あ……もうそのメディアでは受け付けられないのよ。
416 Requested Range Not Satisfiable その期間分のデータは無いのよね……。
417 Expectation Failed そ、それは生徒会の管轄外よ。
418 I'm a teapot (RFC 2324) 私、コーヒーはだめなのよ。紅茶にしてもらえるかしら。
422 Unprocessable Entity (WebDAV) 確かに書式はあっているのだけれど……この項目、おかしくないかしら?
423 Locked (RFC 2324) そこの鍵は預かっていないのよ。
424 Failed Dependency (RFC 2324) この前の議題が否決されてしまっているから……ごめんなさい、これも駄目ね。
426 Upgrade Required 申し訳ないのだけれど、新しい書式に則ってもらえるかしら。
428 Precondition Required 少し事情が変わってしまったの。悪いのだけれど、こんな感じに調整して再提出お願いできるかしら。
429 Too Many Requests 同じ人からの相談を続けて受けるというのは問題になってしまうの。日を改めてもらえるかしら。
431 Request Header Fields Too Large 序章の注釈が多すぎて読みづらくなってしまっているの。もう少し整理してもらえないかしら。

5xx Server Error サーバエラー

Code Reason Phrase 台詞
500 Internal Server Error ごめんなさい、こちらに落ち度があったようね……。
501 Not Implemented その件は、まだ体制が整っていないのよ。
502 Bad Gateway その件だけれど、先方に断られてしまったわ。
503 Service Unavailable 今は少し手を離せないの、後でまたお願いできるかしら。
504 Gateway Timeout 出ないわね……ごめんなさい、担当の方がご不在みたい。
505 HTTP Version Not Supported その校則は草案はあるのだけれど、まだ適用されてはいないのよ。
506 Variant Also Negotiates え、ええ……こちらのミスで、交渉先が判らなくなってしまっているようなの……。
507 Insufficient Storage (WebDAV) ごめんなさい、どうやら保管場所が無くなってしまったようなの……。
508 Loop Detected (WebDAV) 堂々巡りで、結局何も決まらなかったわ……。
509 Bandwidth Limit Exceeded この量は、一度に運びきれないわ。
510 Not Extended 今のままだと、そこまでは認められないわ。こんな感じでやり直してもらえるかしら。
511 Network Authentication Required 先に、こちらで委員会の登録手続きをすませてもらえる?

*1:イメージは七草真由美@魔法科高校の劣等生(CV:花澤香菜さん)とか、望月真帆@大図書館の羊飼い(CV:中島沙樹さん)とか

*2:いやほんとに、いけないと思いながらも、ついつい手を抜いた実装にしてしまうんですよね……(サーバー・クライアント共に)

脆弱性案件はかわいいGHOSTの夢を見るか?

glibcのgethostbyname系関数にバッファオーバーフローを引き起こす重大な脆弱性(CVE-2015-0235)が発見されたとして騒ぎになっている。

The Laws of Vulnerabilities: The GHOST Vulnerab... | Qualys Community
Qualys Security Advisory CVE-2015-0235
glibc の脆弱性 CVE-2015-0235(通称:GHOST)についてまとめてみた - piyolog
glibcのgethostbyname関数に存在するCVE-2015-0235(GHOST)脆弱性について - ブログ - ワルブリックス株式会社
JVNVU#99234709: glibc ライブラリにバッファオーバーフローの脆弱性
glibc の脆弱性対策について(CVE-2015-0235):IPA 独立行政法人 情報処理推進機構

こちらの記事には、RedHat Enterprise Linux・CentOS・Debian GNU/Linux等の対処方法も書かれている

脆弱性の概要

glibcは libcのGNUバージョンです。libcはアプリケーションではなく、事実上全てのアプリケーションが利用しているライブラリです。OSの中ではカーネルに次いで重要な部分と言えます。Linuxシステムでは例外なく glibcが使われています。


この glibcに含まれる gethostbyname系関数の実装に 2000年頃から存在したバグが今になって発見され、CVE-2015-0235 通称 GHOSTと命名されました。ネットワークで何らかの通信を行うアプリケーションは必ずこの関数を使用します。

glibcのgethostbyname関数に存在するCVE-2015-0235(GHOST)脆弱性について - ブログ - ワルブリックス株式会社

今回発見されたglibcの問題は、攻撃者がリモートから細工されたホスト名文字列を(例えばURLのホスト名部分などに埋め込んで)与えることによって、アプリケーション側のメモリを数バイト上書きできてしまうことです。

実際に、Exim(メールサーバ)に対して攻撃を行い任意コードの実行が出来る実証コードを作成できたそうです。32bit/64bitの両方が攻略可能で、CPUの不正命令実行防止機構(ASLR, PIE, and NX)は役に立たないとのこと。

glibcのgethostbyname関数に存在するCVE-2015-0235(GHOST)脆弱性について - ブログ - ワルブリックス株式会社

ということで、少なくともLinuxを用いている場合には*1早々に確認の上、必要に応じて対応しなければならない。


……のだが。


え、これは……?

「GHOST」という通称(GetHOSTbynameに由来)もさることながら

GHOST vulnerability

The Laws of Vulnerabilities: The GHOST Vulnerab... | Qualys Community

用意されたこのアイコンのせいで、事態の深刻さと比較して、どうにも危機感が薄まってしまうのは否めない。


これは孔明の罠か?……いやいや、惑わされてはならない。きちんと確認の上、対応しなければ。

脆弱性の確認方法

基本的には、

Qualys Security Advisory CVE-2015-0235

中にある、GHOST.cをコンパイル・実行すると、"vulnerable"か"not vulnerable"か(実行したシステムが脆弱か否か)が表示されるので、"vulnerable"だった場合には対応が必要となる。


ただ、自分のような英弱には、"vulnerable"という単語を見て、即座に「危険だ!」と判断することは困難である。
そこで、脆弱性があった場合の結果を目立たせるように簡単なパッチをあててみた。

ghost.c
#include <netdb.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>

#define CANARY "in_the_coal_mine"

struct {
  char buffer[1024];
  char canary[sizeof(CANARY)];
} temp = { "buffer", CANARY };

void ghost(void) {
  int a, b=0, c=10; for (; (a="!nBACAAUBJBPAOANAQALAFBFBCAJAFDDDBCHATBIACAFMGAESFAKCAAHACCEGADBDBAEBLMGCRANCBHBBE"[b++]);) for(;64 < a--;) putchar((++c == 42) ? (c-=32) : (32^(b&1)));
  puts("GHOST IN THE SHELL\n");
}

int main(void) {
  struct hostent resbuf;
  struct hostent *result;
  int herrno;
  int retval;

  /*** strlen (name) = size_needed - sizeof (*host_addr) - sizeof (*h_addr_ptrs) - 1; ***/
  size_t len = sizeof(temp.buffer) - 16*sizeof(unsigned char) - 2*sizeof(char *) - 1;
  char name[sizeof(temp.buffer)];
  memset(name, '0', len);
  name[len] = '\0';

  retval = gethostbyname_r(name, &resbuf, temp.buffer, sizeof(temp.buffer), &result, &herrno);

  if (strcmp(temp.canary, CANARY) != 0) {
    //puts("vulnerable");
    ghost();
    exit(EXIT_SUCCESS);
  }
  if (retval == ERANGE) {
    puts("not vulnerable");
    exit(EXIT_SUCCESS);
  }
  puts("should not happen");
  exit(EXIT_FAILURE);
}
CVE-2015-0235_GHOST/ghost.c at develop · furyutei/CVE-2015-0235_GHOST · GitHub

これを使用しているシステムでコンパイルして

$ gcc -o ghost ghost.c

実行すると、該当する脆弱性がある場合には、

$ ./ghost

              !! !!! !
           !!          !!
         !               !
        !                 !
       !      !!      !!   !
      !      !!!!    !!!!  !!!
      !                    !!
      !   !      !!!!!!!!!!!!!
     !     !!!!!!!!!!!!!!!!!!!
    !           !!! !        !
 !!!     !!!!!!! !!!!  !!!!  !
   !!            !!!!!!!!!!!!!
     !!!                  !
         !!!  !!!!!!!!  !!     GHOST IN THE SHELL

$ 

このように一目でわかるので、迅速な対応が可能になると思われる。適宜活用されたし。

などと、まじめに対応するつもりでもついついネタに走りたくなってしまうので、脆弱性案件にかわいい通称やアイコンはよくない。

追記:GitHubにも登録

mholzinger/CVE-2015-0235_GHOST · GitHub

をForkさせてもらい、

furyutei/CVE-2015-0235_GHOST at develop · GitHub

上のファイルを読み込んで実行できるようになった。

wget で実行
$ wget -q -O - "https://raw.githubusercontent.com/furyutei/CVE-2015-0235_GHOST/develop/build_aa.sh" | bash
not vulnerable
curl で実行
$ curl -skL "https://raw.githubusercontent.com/furyutei/CVE-2015-0235_GHOST/develop/build_aa.sh" | bash
not vulnerable

*1:GNU glibcの問題なので、Linux以外でも影響する場合はある

mountしたWindows7上のPHPファイルが実行できない?

Linux (CentOS 6.6) で、Windows 7 上のフォルダを mount したとき、Linux 上からそのフォルダ下にある PHP ファイルを実行しようとすると、エラーになってしまう……。
同じフォルダに置いた Python や Ruby 等のスクリプトについては、問題なく実行できる。


今のところ原因が全くわからないので、ご存じの方は教えてほしい。



解決編

改めて検索してみたら、解決策があった。

Add 'noserverino' option, and the problem is settled.

windows - PHP crazy behavior on mount - Super User
[user@linuxhost ~]$ sudo mount -t cifs "//win7host/src" /mnt/src -o username=user,password=pass,uid=500,gid=500,file_mode=0755,dir_mode=0755,noserverino

This doesn't sound like a PHP problem at all. My best guess is that you have a 32/64-bit mismatch here. And PHP doesn't currently support 64-bit inodes.

PHP :: Bug #50150 :: stat() / lstat() does not work on a mounted cifs

使用しているPHPのバージョンが64ビット inode をサポートしていないので、ホスト側(Windows側)が64ビット版の場合にこの問題が起こるらしい。

Inode Numbers

When Unix Extensions are enabled, we use the actual inode number provided by the server in response to the POSIX calls as an inode number.

When Unix Extensions are disabled and "serverino" mount option is enabled there is no way to get the server inode number. The client typically maps the server-assigned "UniqueID" onto an inode number.

Note that the UniqueID is a different value from the server inode number. The UniqueID value is unique over the scope of the entire server and is often greater than 2 power 32. This value often makes programs that are not compiled with LFS (Large File Support), to trigger a glibc EOVERFLOW error as this won't fit in the target structure field. It is strongly recommended to compile your programs with LFS support (i.e. with -D_FILE_OFFSET_BITS=64) to prevent this problem. You can also use "noserverino" mount option to generate inode numbers smaller than 2 power 32 on the client. But you may not be able to detect hardlinks properly.

mount.cifs(8) - Linux man page


なお、64ビット環境のLinuxからmountした場合には、noserverino オプションをつけなくても動作した。



環境

[user@linuxhost ~]$ # === OS バージョン確認
[user@linuxhost ~]$ uname -a
Linux linuxhost.localdomain 2.6.32-504.el6.i686 #1 SMP Wed Oct 15 03:02:07 UTC 2014 i686 i686 i386 GNU/Linux
[user@linuxhost ~]$ cat /etc/redhat-release
CentOS release 6.6 (Final)
[user@linuxhost ~]$ # === PHP バージョン確認
[user@linuxhost ~]$ php -v
PHP 5.4.28 (cli) (built: May  2 2014 18:40:44)
Copyright (c) 1997-2014 The PHP Group
Zend Engine v2.4.0, Copyright (c) 1998-2014 Zend Technologies

不具合発生手順

[user@linuxhost ~]$ # === Windows7 上のフォルダを /mnt/src に mount
[user@linuxhost ~]$ sudo mount -t cifs "//win7host/src" /mnt/src -o username=user,password=pass,uid=500,gid=500,file_mode=0755,dir_mode=0755
[user@linuxhost ~]$ # === /mnt/src に移動
[user@linuxhost ~]$ cd /mnt/src
[user@linuxhost src]$ # === 簡単なPHPファイル作成
[user@linuxhost src]$ echo "<?php echo(\"TEST\n\");" > ./test.php
[user@linuxhost src]$ cat ./test.php
<?php echo("TEST\n");
[user@linuxhost src]$ # === 実行するとエラー
[user@linuxhost src]$ php ./test.php
PHP Fatal error:  Unknown: Failed opening required './test.php' (include_path='.:/usr/share/pear:/usr/share/php') in Unknown on line 0
[user@linuxhost src]$ # === リントでも Warning とエラー
[user@linuxhost src]$ php -l ./test.php
PHP Warning:  Unknown: Failed opening './test.php' for inclusion (include_path='.:/usr/share/pear:/usr/share/php') in Unknown on line 0
Errors parsing ./test.php


なお、上記で作成した /mnt/src/test.php を include() することは出来るので、ますます謎。

[user@linuxhost ~]$ # === mount ディレクトリ以外に移動
[user@linuxhost ~]$ cd ~
[user@linuxhost ~]$ # === /mnt/src/test.php を include() するPHPファイル作成
[user@linuxhost ~]$ echo "<?php include('/mnt/src/test.php');" > ./call_test.php
[user@linuxhost ~]$ cat ./call_test.php
<?php include('/mnt/src/test.php');
[user@linuxhost ~]$ # === ちゃんと実行される
[user@linuxhost ~]$ php ./call_test.php
TEST