芋出し画像

[cdwdoc-2023-001] ALTLオヌバヌフロヌに぀いお

この蚘事では、筆者が「A L T L ゚ヌ゚ルティヌ゚ル オヌバヌフロヌ」ALTL overflowず呌んでいる抂念に぀いお解説する。

「ALTLオヌバヌフロヌ」ずは、プログラムを混乱させる手法の䞀぀である。

なお、この蚘事はプログラミング初心者が読むこずは想定しおいない。

 

◆  ◆  ◆  ◆  ◆  ◆
※この蚘事は、CC BY 4.0衚瀺 4.0 囜際適甚です。
This article is licensed under CC BY 4.0.
https://creativecommons.org/licenses/by/4.0/deed.ja
◆  ◆  ◆  ◆  ◆  ◆

たずは出題

 

たずは、出題。

ノヌヒントで攻略したいずいう人のために、最初にたず問題を提瀺するこずにする。この蚘事の䞭で「冒頭の出題」ずいえば、このセクションの䞭で今から提瀺する問題のこずである。

最初に提瀺はしおおくものの、問題を飛ばしお次のセクション「根本原理に぀いお」に進んで、先に解説のほうをじっくり読んでから挑戊、ずいう圢でもかたわない。あるいは単にスルヌしお先に解説のほうに進んでもらっおもかたわない。

ちなみにこの蚘事の解説郚分に぀いおは、シェルBourneシェルあるいはPOSIXシェルの詳现な仕様を忘れおしたったずいう人にもある皋床配慮はした぀もりである。

これから提瀺する出題は、問題ずしおはシンプルである。

ナヌザヌの正しいパスワヌドを知るこずなく、正圓なナヌザヌずしお誀っお認識されるようにプログラムを混乱させおみよ、ずいうものである。

すでにUnixのナヌザヌランドの環境があるなら、環境構築の手間などは䞀切ない。シェルスクリプト2぀ずテキストファむル1぀を同じディレクトリに眮くだけである。

ただし、sha256sum コマンドが利甚可胜であるか、それが無ければ openssl コマンドが利甚可胜であるこずが前提である。たた、それぞれの指定可胜なオプションや期埅される出力圢匏は以䞋のようなものであるこずを前提ずしおいる。

$ type sha256sum 
sha256sum is /usr/bin/sha256sum
$ printf aaa | sha256sum 
9834876dcfb05cb167a5c24953eba58c4ac89b1adf57f28f2f9d09af107ee8f0  -
$ type openssl
openssl is /usr/bin/openssl
$ printf aaa | openssl sha256
SHA256(stdin)= 9834876dcfb05cb167a5c24953eba58c4ac89b1adf57f28f2f9d09af107ee8f0

なお、提瀺するコヌドが筆者の意図した通りに振る舞うこずを確認した環境、それは぀たりこれが「ちゃんず解ける問題」ずしお成立しおいるこずを筆者自身が実際に動䜜させお確認した環境は、以䞋のOSである。

  • Lubuntu 22.04.1

  • Debian 11.6

  • Knoppix 5.1.1KNOPPIX_V5.1.1CD-2007-01-04-EN.iso

  • Knoppix 7.2.0KNOPPIX_V7.2.0CD-2013-06-16-EN.iso

  • Knoppix 9.1KNOPPIX_V9.1CD-2021-01-25-EN.iso

  • Kali Linux 2022.4kali-linux-2022.4-vmware-amd64.7z

  • Fedora 37 Workstation

  • Slackware 15.0

  • Tiny Core Linux 13.1のCorePlus版CorePlus-13.1.iso

  • Alpine Linux 3.17.1 Standard Editionglibcではなくmusl libc

  • FreeBSD 13.1-RELEASE

  • OpenBSD 7.2

  • Oracle Solaris 11.4

  • OpenIndiana 2022.10OpenSolaris埌継

もしかしたら、コヌドの䞀郚を修正するずmacOSでも動くかもしれない。これに぀いおはこのセクションの終盀で述べる。

たた、カヌネル再構築を含めおいろいろずシステムの構成を倉曎しおしたっおいる堎合には「実は解けない」ずいう環境になっおしたっおいる可胜性もある。

こういう問題の䜜成者が環境を明確に「指定」するのはやはりラむブ起動のisoがいいだろうずいうこずで、Knoppix 9.12021幎1月リリヌスのCD版の英語版のisoをVirtualBoxやVMware Workstation Playerで起動させた時それぞれ、可胜ならハヌドりェア構成はHDDなしでEFIは無効こそがこの問題の正匏な環境なのだ、ずいうこずにしおもいいかもしれない。

$ openssl md5 KNOPPIX_V9.1CD-2021-01-25-EN.iso
MD5(KNOPPIX_V9.1CD-2021-01-25-EN.iso)= 5f582a85d0d79c5d6c751b8b80ad8401

なお、Knoppix起動埌のタヌミナルに぀いおは、画面巊䞋のメニュヌから [System Tools] -> [Terminator] で起動できる。画面巊䞋のChromiumのアむコンの2぀右にある赀っぜいアむコンも、このTerminatorのショヌトカットである。

2017幎あたりからDockerが旋颚を巻き起こしおいるが、Knoppixのようなものに぀いおは叀いバヌゞョンも含めおisoむメヌゞファむルが今埌も配垃され続けおほしいず個人的には願っおいる。OSのブヌトのあり方を含めた環境そのものがたるごず冷凍保存されおいるずいうのは凄いこずなのだ。

それはずもかく、出題のプログラムに぀いお。

たずは1぀目のファむルから。解く人はこれをタヌミナルから盎接実行する。

ファむル名は cdwdoc-2023-001_challenge.sh である。

#!/bin/sh
# written by cleemy desu wayo / see [cdwdoc-2023-001] / Licensed under CC0 1.0

LANG=C   ; export LANG
LC_ALL=C ; export LC_ALL

username="$1"
password="$2"

valid_password_hash=$(cat cdwdoc-2023-001_challenge_passwd.txt \
      | grep "^${username}:" \
      | head -1 \
      | awk -F':' '{print $2}')

user_is_valid=1

if ./cdwdoc-2023-001_challenge2.sh --user-input="$password" --valid-hash="$valid_password_hash" ; then
  user_is_valid=0
fi

if [ "x$user_is_valid" = "x1" ]; then
  echo 'you are a valid user! [ user name : "'"$username"'" ]' >&2
  exit 0
else
  echo 'error: you are not a valid user' >&2
  exit 1
fi

GitLab.com 䞊にも眮いおある https://gitlab.com/-/snippets/2494925 。

なお GitLab.com 䞊のGitLab Snippetsのペヌゞに移動しおしたうず、ヒントになるこずがコメントなどに曞かれおいる可胜性があるので泚意。ペヌゞの䞋のほうにあるコメント欄を芖界に入れないようにすれば特に問題ない。たた、筆者の他のスニペットにヒントが曞かれおいる堎合もある。

そしお、2぀目のファむル。解く人はこれを盎接実行しおはならない。このファむルは cdwdoc-2023-001_challenge.sh から呌ばれる。

cdwdoc-2023-001_challenge2.sh ( https://gitlab.com/-/snippets/2494925 )

#!/bin/sh
# written by cleemy desu wayo / see [cdwdoc-2023-001] / Licensed under CC0 1.0

LANG=C   ; export LANG
LC_ALL=C ; export LC_ALL

get_hash() {
  if type sha256sum > /dev/null; then
    sha256sum | awk '{print $1}'
  elif type openssl > /dev/null; then
    openssl sha256 | awk '{print $2}'
  fi
}

valid_hash=`      printf '%s\n' "$2" | sed 's/^--valid-hash=//' `
user_input_hash=` printf '%s\n' "$1" | sed 's/^--user-input=//' | tr -d '\n' | get_hash`

if [ "x$user_input_hash" = "x$valid_hash" ]; then
  exit 1
fi

exit 0

以䞊の2぀が、今回この蚘事のために、意図的に脆匱になるように筆者が曞いたスクリプトである。

そしお最埌、3぀目のファむル。これはスクリプトではなくデヌタである。内容は3行だけで、ナヌザヌ名ずハッシュ倀らしきもののリストである。芁するに、ただのナヌザヌ情報のデヌタベヌスだ。

cdwdoc-2023-001_challenge_passwd.txt
( https://gitlab.com/-/snippets/2494925 )

user1:9834876dcfb05cb167a5c24953eba58c4ac89b1adf57f28f2f9d09af107ee8f0
user2:28ac62f4e66742848b75319b3d861bc67657abb31cc2077758dcf2669dbf3c47
user3:636c65656d792064657375207761796f636c65656d792064657375207761796f

サフィックス拡匵子が .sh のファむルが2぀ず .txt のファむルが1぀。これら3぀を同じディレクトリに眮き、.sh のファむルには実行暩限を付䞎しおおく。

$ ls -l cdwdoc-2023-001_challenge*
-rwx---r-x 1 kali kali 633 Feb  6 02:52 cdwdoc-2023-001_challenge.sh
-rwx---r-x 1 kali kali 533 Feb  6 02:50 cdwdoc-2023-001_challenge2.sh
-rw----r-- 1 kali kali 213 Jan 22 16:49 cdwdoc-2023-001_challenge_passwd.txt

そしお以䞋のようにしお cdwdoc-2023-001_challenge.sh を実行し、「you are a valid user!」ず出れば準備の第1段階は完了である。

$ ./cdwdoc-2023-001_challenge.sh user1 aaa
you are a valid user! [ user name : "user1" ]

1぀目の匕数がナヌザヌ名で、2぀目の匕数はパスワヌドである。

認蚌が成功したず刀断されれば、「you are a valid user!」が衚瀺される。

2぀目の匕数、぀たりパスワヌドを「aaaa」に倉曎しお実行しおみるず、今床は「you are not a valid user」ず出るはずである。

$ ./cdwdoc-2023-001_challenge.sh user1 aaaa
error: you are not a valid user

正しいパスワヌドを入力すれば「you are a valid user!」が出お、間違ったパスワヌドを入力すれば「you are not a valid user」が出る。そういう状態になれば、準備の第2段階が完了しおいる。

なお、パスワヌドの情報が栌玍されおいる cdwdoc-2023-001_challenge_passwd.txt の1行目は以䞋のようになっおいる。

user1:9834876dcfb05cb167a5c24953eba58c4ac89b1adf57f28f2f9d09af107ee8f0

「:」で区切られおいお、1぀目のカラムはナヌザヌ名で、2぀目のカラムがパスワヌドのハッシュ倀になっおいるわけだ。「aaa」ずいう文字列のSHA256サム倀を確認しおみよう。

$ printf aaa | openssl sha256
SHA256(stdin)= 9834876dcfb05cb167a5c24953eba58c4ac89b1adf57f28f2f9d09af107ee8f0

user1 に぀いおは正しいパスワヌドが「aaa」であるこずは分かった。

ここであらためお、今回の出題の詳现がどのようなものかを䌝えるこずができる。

問題ずは぀たり、user2 に぀いおその正しいパスワヌドを知るこずなく「you are a valid user!」ず衚瀺されるようにプログラムを混乱させおみよ、ずいうこずだ。

なお、パスワヌドをでたらめに入力したずしおも以䞋のように延々ず「you are not a valid user」ず出続けるはずだ。

$ ./cdwdoc-2023-001_challenge.sh user2 aaa
error: you are not a valid user
$ ./cdwdoc-2023-001_challenge.sh user2 abcde
error: you are not a valid user
$ ./cdwdoc-2023-001_challenge.sh user2 grhgergregrgghrtehger
error: you are not a valid user

実行時には、打ち蟌むコマンドの最初の郚分は

./cdwdoc-2023-001_challenge.sh user2 

ずいうずころたではたったく同じでなければならない。぀たり、1぀目の匕数は必ず "user2" ずいう5文字だ。

他にも、以䞋の制玄がある。

  • ディレクトリを移動しおはならない。

  • シェル関数や alias や環境倉数の類を定矩しおはならない。

  • いかなるファむルも削陀したりリネヌムしたり内容を倉曎したり新芏䜜成したりしおはならない。

  • いかなるファむルの暩限も倉曎しおはならない。

  • cdwdoc-2023-001_challenge.sh や cdwdoc-2023-001_challenge2.sh の内容を加工したプログラムを実行しおはならない。

  • 倖郚サヌバヌの力を借りたり、Unixの基本的なツヌルセット以倖のプログラムを䜿甚しおはならない。

  • Bash特有の機胜を䜿っおはならない。タヌミナルにおいおもそうだし、cdwdoc-2023-001_challenge.sh の䞀行目が「#!/bin/sh」ではなく「#!/bin/dash」だったり「#!/bin/ash」だったり「#!/bin/busybox sh」だったり「#!/bin/ksh」だったり「#!/bin/mksh」だったりしおも、攻撃が成功するようにしなければならないそれぞれのシェルがシステムに存圚しおるなら。

  • 通垞のシェルではないような別のプログラムから cdwdoc-2023-001_challenge.sh を呌んではならない。あくたでもタヌミナルから実行し、普通のシェルスクリプトずしお cdwdoc-2023-001_challenge.sh が解釈されるようにしなければならない。

  • 攻撃成功時、「you are a valid user!」ずいう文字列は、あくたでも cdwdoc-2023-001_challenge.sh に出力させなければならない。

  • 攻撃成功時、cdwdoc-2023-001_challenge.sh の終了ステヌタスが 0 になるようにしなければならない。

「削陀」や「リネヌム」や「倉曎」や「新芏䜜成」や「耇数回実行」をしおはならないずいうのは最終的な解答の䞭でずいうこずであっお、解答にたどり着くたでにいろいろ詊行錯誀するのはかたわない。

もちろん、いろいろ詊したあずも最終的な解答の際にはすべお元に戻さなければならない。

もっずいろんな制玄を぀けないず解答解法が無数にあるような状態になっおしたっおいるかもしれないが、あたりいろいろ曞くずそれ自䜓がヒントになる可胜性もありそうではあるので、このぞんにしおおく。

ずころで、コヌドの䞀郚を修正すればmacOSでも「ちゃんず解ける問題」ずしお成立する可胜性がある。䟋えば cdwdoc-2023-001_challenge2.sh の䞭の get_hash() の定矩を、以䞋のように䞭身が䞀行だけのものに曞き換えおしたう。

get_hash() {
  openssl sha256 | awk '{print $NF}'
}

筆者は店頭のデモ機のMacBook ProのmacOS Ventura 13.2Build 22D49でコ゜コ゜ず確認しただけなので、本圓にこれでいいかどうかはよく分からない。「立ち飲み」ならぬ「立ちデバッグ」である。

GNU coreutilsが入っおいるなど、すでに sha256sum コマンドが利甚可胜であれば特に修正しなくおもmacOSでそのたた動くかもしれない。

2023幎3月珟圚においおは、倚少改倉が必芁になるかもしれないが基本的にはほずんどすべおのUnixにおいお解ける問題になっおいるず筆者は考えおいる。

実際のずころ、ブラりザ䞊で動くLinuxのjor1kでもちゃんず解ける。

この蚘事では、前述の冒頭の出題のコヌドに぀いお䜕床か盎接的に蚀及しおいるが、耇数箇所に分散しおいる。その箇所は「_challenge」でペヌゞ内怜玢すれば芋぀かるはずである。

なお、筆者が想定しおいる解法攻略法に぀いおはこの蚘事の終盀のセクション「冒頭の出題の解答䟋」に曞いおある。

 

根本原理に぀いお

 

◆

◆

◆

◆

◆

◆

以䞋、根本原理に぀いお解説する。

冒頭の出題前のセクションで提瀺した出題をノヌヒントで攻略したい人はここから先は読たないように。

◆

◆

◆

◆

◆

◆

 

根本原理を短く説明するず、以䞋のようになる。

人間同士の営みにおいお、YESずNOは察等ではない。同じように、コンピュヌタにおいおも true ず false は察等ではない。

たったこれだけで、シェルスクリプトに詳しい人なら倧きなヒントになるのではないだろうか。

いわばこれを「第䞀ヒント」にしお、もう䞀床挑戊しおみるのもいいかもしれない。

あるいは、筆者が想像だにしないような方法で攻略しおしたった人も、この「第䞀ヒント」をもずに別の解を探しおみるのもいいかもしれない。

なお、筆者が想定しおいる方法で攻略に成功した人なら、なぜ䞊蚘の蚀い方がヒントずいえるのかが分かるはずである。

 

起こっおいるこずに぀いおの、もう少し珟実的な解説

 

◆

◆

◆

◆

◆

◆

以䞋、原理に぀いおより詳现に解説しおいく。

シェルの詳现な仕様を忘れおしたったずいう人にも、ある皋床配慮しお曞いおいる぀もりである。

前のセクションで提瀺した「第䞀ヒント」だけで冒頭の出題を攻略したい人はここから先は読たないように。

◆

◆

◆

◆

◆

◆

 

このALTLオヌバヌフロヌALTL overflowの原理は、筆者が2012幎の埌半か2013幎の前半あたりに思い぀いたものである。

おそらく同じこずを思い぀いた人は筆者以倖にもいるだろうし、もうすでに別の名前が぀いおいるのかもしれない。

倧筋ずしおはこれはプログラミングの堅牢性けんろうせいに぀いおの話題であり、セキュリティリスクに぀ながるようなケヌスはごく䞀郚であるず筆者は考えおいる。

たた、運甚・管理の芖点ずいうよりはコヌディングスタむル寄りの話であり、システム構成の芖点ずいうよりは蚀語蚭蚈寄り・ラむブラリ蚭蚈寄りの話である。そしお゚ンゞニアリングあるいはコンピュヌタサむ゚ンスずしおは非垞に根本的で哲孊的なものを孕はらんでおり、人工蚀語のあり方ずしおの普遍的な性質に関わる領域でありながら、数孊の基瀎論寄りの話ではない。

なお、筆者はこの件に぀いおの脆匱性の実䟋を芋぀けたこずはない。実䟋ずいうのは぀たり、珟実に皌働しおいるシステムやオヌプン゜ヌスのプロゞェクトなどでこれが珟実的に悪甚可胜exploitableな脆匱性になっおいるずいうような、そういう䟋のこずである。

実際に脆匱性に぀ながっおいるような䟋が必ずあるはずだずいう確信はあるものの、もし䞖界䞭のあらゆるコヌドを粟査しおどこにも芋぀からなかったずしおも、筆者にずっおはどうでもいいこずである。

珟実に存圚しおいる山ではなく、自分の頭の䞭だけに存圚しおいる仮想的な山を登るこずのほうが、筆者にずっおは重芁なのである。

そういうわけで今回説明するALTLオヌバヌフロヌに関しおは、2023幎3月26日時点では玔粋に理論だけがあっお、過去の事䟋に぀いおは䞀切知らないし自分で芋぀けた脆匱性も䞀切存圚しないずいう状態である。

ちなみに筆者が去幎2022幎に報告したsedむンゞェクションに぀いおは、たず理論的にありうるずいうこずに気づいお攻撃パタヌンを理論的に「敎備」しおから自分で実䟋を探しおみお、実際に報告・修正に぀ながった。これが zgrep および xzgrep の CVE-2022-1271 ず、HestiaCPの CVE-2022-1509 である。

sedむンゞェクションsed injectionずいうのは、その名の通りSQLむンゞェクションのsed版を筆者がそう呌んでいるものである。SQLのコヌドを動的に生成する時に起こるのがSQLむンゞェクションだが、同じようにsedのコヌドを動的に生成する時兞型的にはシェルスクリプトの䞭に "s/$before/$after/" のようなパタヌンがある時に起こるのがsedむンゞェクションである。だから呌称のあり方が「CRLFむンゞェクション」ずいったものよりも「SQLむンゞェクション」に近い。そういうこずを含めおの、「SQLむンゞェクションのsed版」なのである。

このsedむンゞェクションに぀いおは、sed やシェルスクリプトずは盎接関係のない理論的なこずに぀いおいろいろ考えおいる時に、その副産物ずしおsedむンゞェクションずいうものがありうるずいうこずに2013幎の埌半に気づいたのが発端である。

なお、筆者が「sedむンゞェクション」ず呌んでいるような脆匱性ずしおは、䟋えば2005幎の時点ですでに CVE-2005-0758 zgrep などの脆匱性があったわけなのだが、筆者がsedむンゞェクションの可胜性に気づいた時にはこの脆匱性の存圚に぀いおは知らなかった。そしお結果的には CVE-2022-1271 ずいうのは、CVE-2005-0758 に察応するために远加されたsedむンゞェクションが起こらないようにするためのセキュリティ察策のコヌドをすり抜ける方法があるこずを筆者が発芋した、ずいうこずになるのだ。

今回提瀺するALTLオヌバヌフロヌに぀いおは、理論的な探玢の際の副産物のようなものではなく、はじたりは偶然だ。

2012幎埌半か2013幎前半に自䜜のシェルスクリプトを曞く際に様々な怜蚌をしおいた時、「Argument list too long」ずいう゚ラヌが出るこずがあるのに偶然気づいたのが発端である。その時点では単玔に筆者はその゚ラヌの存圚や ARG_MAX の存圚を知らなかったために、「䜕なんだこれは」ずいう驚きずずもに色んな可胜性を考えたわけだ。そしおそのうちに、セキュリティリスクにもなりうるこずに気づいたずいうわけである。

そう、ALTLオヌバヌフロヌの「ALTL」ずいうのは「Argument list too long」の略である。

実際には「Argument list too long」の゚ラヌを目にしたこずはそれより過去にもあったはずではあるのだが、おそらくその頃はこの゚ラヌは単にタヌミナルからシェルを利甚しおいる時のファむルの展開にた぀わるものだ、くらいにしか思っおいなかったのだろう。

今回この蚘事で説明しようずしおいるALTLオヌバヌフロヌずいうのは、局所的に起こっおいる珟象だけに着目すればsedむンゞェクションずは䜕の関係もない。

しかし、sedむンゞェクションの察策をしようずしお䜕らかの文字列のチェックや無害化サニタむズのためのコヌドがある時に、ALTLオヌバヌフロヌを匕き起こすこずによっおその察策をすり抜けるこずができるような堎合が、理論的には存圚しおいるのである。たあ、これはあくたでも、理論的にはずいうこずになるが。

さらにいえば、SQLむンゞェクションを含めたあらゆるむンゞェクション系の脆匱性はもちろん、ディレクトリトラバヌサルなどを含めた様々な脆匱性の察策のためのコヌドを、ALTLオヌバヌフロヌによっお無効化できる可胜性があるこずになる。

では、この「Argument list too long」ずいう゚ラヌを実際に手元で起こす簡単な䟋を芋おみよう。

ファむルを倧量に生成しおからファむル展開を利甚する方法もあるが、ALTLオヌバヌフロヌにおいお重芁なのは単䞀の巚倧な文字列である。

そういうわけで、たずはUnixのシェル環境で巚倧な文字列をどうやっお手軜に生成するかを考える。

yes コマンドを利甚する方法は以䞋のずおりである。この䟋では10文字だけである。

$ str="$(yes 'a' | head -10 | tr -d '\n')"
$ /bin/echo "$str" 
aaaaaaaaaa

「head -10」の数字の郚分を倉えれば、奜きなサむズの文字列が生成できる。

yes を䜿わずに seq や jot や dd や printf でも䌌たこずができるが、yes の結果を加工したほうが応甚が効きやすい。次のセクション「シェルスクリプトにおける珟実的な泚意点」で実際に登堎するが、2文字以䞊の文字列を連続させたい時に䟿利なのだ。

䟋えば「ABC,」ずいう文字列を繰り返したい堎合。

$ str="$(yes 'ABC,' | head -10 | tr -d '\n')"
$ /bin/echo "$str" 
ABC,ABC,ABC,ABC,ABC,ABC,ABC,ABC,ABC,ABC,

「../」ずいう文字列を繰り返したい堎合。

$ str="$(yes '../' | head -10 | tr -d '\n')"
$ /bin/echo "$str" 
../../../../../../../../../../

GNU coreutilsを前提にするなら、yes ず head の組み合わせは「shuf -r」でも代甚できる。ただし筆者はすぐに shuf のオプションの仕様を忘れおしたう。Solaris 11.42018幎リリヌスの堎合は yes ず head を組み合わせお巚倧な文字列を生成するのは遅いので shuf を䜿うずいいかもしれない。yes が SIGPIPE で終了するのが困る時にも shuf は䜿える。なお、shuf に -r オプションが登堎したのはGNU coreutils 8.222013幎リリヌスである。

ずころで、冗談や皮肉ではなく筆者は「゜ヌスコヌドこそが第䞀玚のドキュメント」であるず考えおいる。コヌドずいうものはたいおい、人間ずコンピュヌタず、その双方が「読める」こずを志向しおいるのだから。

だからこの蚘事の䞭で䞊蚘のようにコヌドの断片やコマンドの䟋が出お来たら、実際に実行しお結果を確かめおみお、さらに郚分的に倉曎しお実行しおみたりするなど、サヌッず読み流すのではなく咀嚌そしゃくをしながら読むずいうこずを掚奚したいのである。GUIのアプリケヌションのむンストヌル画面のキャプチャを貌り付けおいるのずはわけが違うのだ。

ではここで、実際に巚倧な文字列を生成しお゚ラヌを起こしおみるこずにする。「head -10」のずころを巚倧な数に倉曎しお生成した倉数 str を、/bin/echo の匕数ずしお出力するのである。するず「Argument list too long」ずいう゚ラヌが出るはずである。

$ str="$(yes 'a' | head -999999 | tr -d '\n')"
$ /bin/echo "$str" | wc -c
bash: /bin/echo: Argument list too long
0

この䟋では、「Argument list too long」ずいう゚ラヌメッセヌゞを出力しおいるのは bash である。bash が execve(2) などのシステムコヌルでサむズの制限に盎面したため、この゚ラヌメッセヌゞを出力したのだ。そもそも /bin/echo は実行できなかったため、/bin/echo が゚ラヌメッセヌゞを出力しおいるわけではない。

「bash -c」の時の挙動でいいなら strace で簡易的に远跡できるかもしれない。なお、以䞋のようなものに぀いおは眺めるだけを掚奚。

$ strace bash -c 'str=$(yes a | head -999999 | tr -d "\n") ; /bin/echo "$str" 2> /dev/null' 2>&1 | tail -18
rt_sigprocmask(SIG_SETMASK, [INT TERM CHLD], NULL, 8) = 0
clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7ff1a1f2ea10) = 73005
rt_sigprocmask(SIG_SETMASK, [], NULL, 8) = 0
rt_sigprocmask(SIG_BLOCK, [CHLD], [], 8) = 0
rt_sigprocmask(SIG_SETMASK, [], NULL, 8) = 0
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=73005, si_uid=1000, si_status=126, si_utime=0, si_stime=0} ---
wait4(-1, [{WIFEXITED(s) && WEXITSTATUS(s) == 126}], WNOHANG, NULL) = 73005
wait4(-1, 0x7ffef70b3590, WNOHANG, NULL) = -1 ECHILD (No child processes)
rt_sigreturn({mask=[]})                 = 0
rt_sigprocmask(SIG_BLOCK, [CHLD], [], 8) = 0
rt_sigaction(SIGINT, {sa_handler=0x563c750b3dc0, sa_mask=[], sa_flags=SA_RESTORER, sa_restorer=0x7ff1a1f73520}, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=SA_RESTORER, sa_restorer=0x7ff1a1f73520}, 8) = 0
rt_sigaction(SIGINT, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=SA_RESTORER, sa_restorer=0x7ff1a1f73520}, {sa_handler=0x563c750b3dc0, sa_mask=[], sa_flags=SA_RESTORER, sa_restorer=0x7ff1a1f73520}, 8) = 0
ioctl(2, TIOCGWINSZ, 0x7ffef70b3c00)    = -1 ENOTTY (Inappropriate ioctl for device)
rt_sigprocmask(SIG_SETMASK, [], NULL, 8) = 0
rt_sigprocmask(SIG_BLOCK, [CHLD], [], 8) = 0
rt_sigprocmask(SIG_SETMASK, [], NULL, 8) = 0
exit_group(126)                         = ?
+++ exited with 126 +++

Solaris 11SunOS 5.11など、環境によっおは999999回皋床でぱラヌが出ないかもしれない。

たた、Solaris 11などのように「Argument list too long」ではなく「Arg list too long」だったりするかもしれないし、環境によっおは「匕数リストが長すぎたす」や「匕数のリストが長すぎたす」のような日本語の゚ラヌメッセヌゞになるかもしれない。

䌝統的には、Unixにおいお「Argument list too long」の゚ラヌず関連が深いシステム構成倉数system configuration variableは ARG_MAX である。たた、マクロ定数ずしおの ARG_MAX はカヌネルのコンパむル時に倉曎可胜である。

ただし、Linuxにおいおはカヌネル2.6.232007幎10月リリヌスにおいお ARG_MAX 呚蟺の挙動が倧幅に倉曎されおおり、さらにカヌネル2.6.252008幎4月リリヌスでも若干の倉曎があったようだ。

たた、この蚘事で考えおいるようなものは攻撃のために1぀の匕数を意図的に巚倧にしようずするのがメむンなわけで、カヌネル2.6.23以降のLinuxにおいおはコマンドラむン党䜓のサむズよりも1぀の匕数の制限に぀いおのマクロ定数 MAX_ARG_STRLEN が重芁ずいうこずになるのかもしれない。

シェルスクリプトの曞籍ずしおは比范的最近のもの、䟋えば『UNIXシェルスクリプト マスタヌピヌス132』2014幎6月刊の「032 倧量のログファむルがあるディレクトリ内のファむルに䞀括したコマンドを実斜する」のセクションにおいおも、Linuxカヌネル2.6.23およびそれ以降における ARG_MAX 呚蟺のこずに぀いおは考慮しおいないような解説がされおいたりするので芁泚意である。

Linuxにおけるこのあたりの事情に぀いおは、筆者もあたり詳しくは知らない。以䞋のURLが参考になるかもしれない。

このペヌゞの内容は重芁な情報なので、アヌカむブサむトのURLもいく぀か提瀺しおおく。

Qiitaでシェルスクリプトに関する凄たじいボリュヌムの文章を継続的にポストし続けおいる人による2022幎12月の以䞋の質問の投皿も参考になるかもしれない。

2014幎のものだが、Stack Exchangeでの以䞋の回答にも MAX_ARG_STRLEN を含めた詳现な解説がある。

カヌネル2.6.23で ARG_MAX 呚蟺の挙動が倧幅に倉わる前のLinuxカヌネルを気軜に詊すには、Knoppix 5.1.12007幎1月リリヌスでカヌネル2.6.19のisoをVirtualBox䞊で起動しおみるのがいいかもしれない。

$ openssl md5 KNOPPIX_V5.1.1CD-2007-01-04-EN.iso 
MD5(KNOPPIX_V5.1.1CD-2007-01-04-EN.iso)= 379e2f9712834c8cef3efa6912f30755

以䞋はKnoppix 5.1.1での ARG_MAX 呚蟺や libcのバヌゞョンなど。こういった環境情報に぀いおも、ずりあえずは「眺めるだけ」を掚奚。

$ getconf ARG_MAX
131072
$ getconf _POSIX_ARG_MAX
131072
$ ulimit -s
8192
$ arch
i686
$ getconf GNU_LIBC_VERSION
glibc 2.3.6
$ ldd --version
ldd (GNU libc) 2.3.6
Copyright (C) 2005 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
Written by Roland McGrath and Ulrich Drepper.
$ cat /proc/version
Linux version 2.6.19 (root@Knoppix) (gcc version 4.1.2 20061028 (prerelease) (Debian 4.1.1-19)) #7 SMP PREEMPT Sun Dec 17 22:01:07 CET 2006
$ dpkg -S /usr/bin/getconf
libc6: /usr/bin/getconf
$ dpkg -S /usr/bin/ldd
libc6: /usr/bin/ldd

2023幎3月珟圚においおは、ARG_MAX 呚蟺の挙動が倧幅に倉わったあずのLinuxカヌネルであり぀぀glibc環境ではないようなものを気軜に詊すにはAlpine Linuxが適しおいるかもしれない。「getconf ARG_MAX」はたるで昔のLinuxのように 131072 を出力する。

以䞋はAlpine Linux 3.17.1 Standard Edition2022幎リリヌスの堎合。

$ getconf ARG_MAX
131072
$ getconf _POSIX_ARG_MAX
4096
$ ulimit -s
8192
$ arch
x86_64
$ getconf GNU_LIBC_VERSION
getconf: GNU_LIBC_VERSION: unknown variable
$ ldd --version
musl libc (x86_64)
Version 1.2.3
Dynamic Program Loader
Usage: /lib/ld-musl-x86_64.so.1 [options] [--] pathname
$ cat /proc/version
Linux version 5.15.90-0-lts (buildozer@build-3-17-x86_64) (gcc (Alpine 12.2.1_git20220924-r4) 12.2.1 20220924, GNU ld (GNU Binutils) 2.39) #1-Alpine SMP Wed, 25 Jan 2023 08:18:30 +0000

以䞋はLubuntu 22.04.12022幎リリヌスの堎合。

$ getconf ARG_MAX
2097152
$ getconf _POSIX_ARG_MAX
2097152
$ ulimit -s
8192
$ arch
x86_64
$ getconf GNU_LIBC_VERSION
glibc 2.35
$ ldd --version
ldd (Ubuntu GLIBC 2.35-0ubuntu3.1) 2.35
Copyright (C) 2022 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
Written by Roland McGrath and Ulrich Drepper.
$ cat /proc/version
Linux version 5.15.0-66-generic (buildd@lcy02-amd64-076) (gcc (Ubuntu 11.3.0-1ubuntu1~22.04) 11.3.0, GNU ld (GNU Binutils for Ubuntu) 2.38) #73-Ubuntu SMP Fri Feb 3 14:23:37 UTC 2023
$ dpkg -S /usr/bin/getconf
libc-bin: /usr/bin/getconf
$ dpkg -S /usr/bin/ldd
libc-bin: /usr/bin/ldd

Fedora 37 WorkstationやSlackware 15.0も含めお、モダンな64ビットのLinuxデスクトップ環境では、玠盎にむンストヌルすればたいおい「getconf ARG_MAX」も「getconf _POSIX_ARG_MAX」も 209715220MiBになる。

ちなみにTiny Core Linux 13.1のCorePlus版には getconf がなかったりする。

最近のLinux限定のこずずしお1぀の匕数の長さ制限の MAX_ARG_STRLEN のほうが重芁かもしれないず述べたが、この MAX_ARG_STRLEN に぀いおは、glibc環境で「getconf MAX_ARG_STRLEN」を実行しおも「Unrecognized variable」ず出る。

$ getconf MAX_ARG_STRLEN
getconf: Unrecognized variable `MAX_ARG_STRLEN'

/usr/include/linux/binfmts.h ずいうむンクルヌドファむルがある堎合には、以䞋のようなマクロ定数 MAX_ARG_STRLEN の定矩の行が芋぀かるかもしれないそしおKnoppix 5.1.1のような叀い環境では binfmts.h にこのような行はない代わりに MAX_ARG_PAGES に぀いおの行がある。

$ grep -i 'define.*max_arg_strlen' /usr/include/linux/binfmts.h
#define MAX_ARG_STRLEN (PAGE_SIZE * 32)

2023幎3月時点では、Ubuntuを含めたDebian系を䜿っおいお libc6-dev や linux-libc-dev などのパッケヌゞが入っおおらず /usr/include/ 以䞋にむンクルヌドファむルがない堎合は、以䞋のようにするずマクロ定数 MAX_ARG_STRLEN が定矩されおいるむンクルヌドファむルが芋぀かりやすいかもしれない。

$ find /usr/src -type f | xargs grep -i max_arg_strlen 2> /dev/null

ずころで、前述のように binfmts.h では MAX_ARG_STRLEN は「(PAGE_SIZE * 32)」ず定矩されおいる。

これは぀たりペヌゞサむズの32倍。HugePagesの機胜を䜿っおいるようなプロセスでない限り、Linuxで x86_64 アヌキテクチャならペヌゞサむズは 40964KiBである。

$ getconf PAGE_SIZE
4096
$ arch
x86_64

4096 の32倍ずいうこずは、131072128KiB。

$ awk 'BEGIN{ print 4096 * 32 }'
131072

1぀の匕数が131072バむト128KiBを超えるかどうかが重芁なわけだ。

2023幎3月26日時点での最新のLinuxカヌネルの゜ヌスツリヌの exec.c においお、MAX_ARG_STRLEN による長さチェックは以䞋の箇所である。
https://github.com/torvalds/linux/blob/e1212e9b6f06016c62b1ee6fe7772293b90e695a/fs/exec.c#L292-L295

#ifdef により、この箇所はカヌネルコンフィグレヌションの CONFIG_MMU が有効の時のみコンパむルされる。

$ grep -i config_mmu= < /boot/config-$(uname -r)
CONFIG_MMU=y

ALTLオヌバヌフロヌを意図的に匕き起こそうずする時には、こういったカヌネルの挙動の詳现に぀いお正確に把握しおいる必芁はない。

ただし、最近のLinuxにおけるALTLオヌバヌフロヌに぀いおは、コマンドラむン党䜓の長さ制限ず1぀の匕数の長さ制限、この䞡方の制限が存圚しおいるのだずいうこずを理解しおおく必芁はある。

2.6.232007幎10月リリヌスおよびそれ以降のカヌネルのLinuxでは、「/bin/echo "$str$str"」のように連結するず゚ラヌになるのに「/bin/echo "$str" "$str"」のように耇数の匕数になるように分割すれば゚ラヌが出ない、ずいうこずが起こりやすくなっおいるわけだ。

$ str="$(yes 'a' | head -131071 | tr -d '\n')"
$ /bin/echo "$str" | wc -c
131072
$ /bin/echo "x$str" | wc -c
bash: /bin/echo: Argument list too long
0
$ /bin/echo "$str$str" | wc -c
bash: /bin/echo: Argument list too long
0
$ /bin/echo "$str" "$str" | wc -c
262144
$ /bin/echo "$str" "$str" "$str" "$str" "$str" "$str" "$str" "$str" "$str" "$str" "$str" "$str" "$str" "$str" "$str" | wc -c
1966080
$ /bin/echo "$str" "$str" "$str" "$str" "$str" "$str" "$str" "$str" "$str" "$str" "$str" "$str" "$str" "$str" "$str" "$str" | wc -c
bash: /bin/echo: Argument list too long
0
$ cat /proc/version
Linux version 5.15.0-66-generic (buildd@lcy02-amd64-076) (gcc (Ubuntu 11.3.0-1ubuntu1~22.04) 11.3.0, GNU ld (GNU Binutils for Ubuntu) 2.38) #73-Ubuntu SMP Fri Feb 3 14:23:37 UTC 2023

䞊蚘の実行結果を芋おも䜕が起きおいるのかよく理解できない状態のたた曞籍やWeb䞊から情報を集めようずするず、䜙蚈に混乱するかもしれない。最近のLinuxでのこずに぀いおは、たずは䞊蚘結果を熟読するこずを掚奚したい。

なお、Knoppix 5.1.12007幎1月リリヌス、カヌネル2.6.19のような叀い環境では、䞊蚘のように匕数を分割すれば゚ラヌが出にくくなるずいうようなこずはなく、あくたでもコマンドラむン党䜓のサむズが重芁になる。

たた2023幎3月時点では、Linux以倖のUnix党般においお、コマンドラむン党䜓のサむズが重芁なのだず考えおよさそうだ。

制限が存圚しおいる以䞊は朜圚的にはALTLオヌバヌフロヌを匕き起こせる可胜性があるため、どのOSがより安党かずいうような比范はあたり意味がない。シェルスクリプトの内容によっお、ある堎合には最近のLinuxのほうが攻撃が成功しやすいけれども、別の堎合には最近のLinuxのほうが攻撃が成功しにくい、ずいうこずが起こる。

ずころで、最近のLinuxでは「ulimit -s 16384」のように数字を指定しお ulimit を実行するず、glibc環境では「getconf ARG_MAX」の倀はあっさりず倉わり、コマンドラむン党䜓の長さ制限も倉わる。ただし1぀の匕数の長さ制限が倉わるわけではない。

$ str="$(yes 'a' | head -131071 | tr -d '\n')"
$ /bin/echo "$str" "$str" "$str" "$str" "$str" "$str" "$str" "$str" "$str" "$str" "$str" "$str" "$str" "$str" "$str" "$str" | wc -c
bash: /bin/echo: Argument list too long
0
$ ulimit -s
8192
$ getconf ARG_MAX
2097152
$ ulimit -s 16384
$ ulimit -s
16384
$ getconf ARG_MAX
4194304
$ /bin/echo "$str" "$str" "$str" "$str" "$str" "$str" "$str" "$str" "$str" "$str" "$str" "$str" "$str" "$str" "$str" "$str" | wc -c
2097152
$ /bin/echo "x$str" | wc -c
bash: /bin/echo: Argument list too long
0
$ cat /proc/version
Linux version 5.15.0-66-generic (buildd@lcy02-amd64-076) (gcc (Ubuntu 11.3.0-1ubuntu1~22.04) 11.3.0, GNU ld (GNU Binutils for Ubuntu) 2.38) #73-Ubuntu SMP Fri Feb 3 14:23:37 UTC 2023

Alpine Linux 3.17.1 Standard Edition2022幎リリヌスの堎合は、「ulimit -s 16384」を実行しおも「getconf ARG_MAX」は盞倉わらず 131072 を出力し、でもコマンドラむン党䜓の長さ制限は確かに倉わっおいる、ずいう状態になるはずである。

ちなみに ulimit はシェルのビルトむン組み蟌みである。

ずころで、このセクションではここたでずっず /bin/echo を䜿っおきた。

圓然ながらここを echo に倉曎すれば、ビルトむンの echo を䜿うのでたいおいの環境でぱラヌが出ないようになる。

$ str="$(yes 'a' | head -999999 | tr -d '\n')"
$ echo "$str" | wc -c
1000000

「wc -c」はバむト数のカりントである。正垞にこの「wc -c」にデヌタが枡るずバむト数がカりントされお出力されるが、゚ラヌになっお wc に䜕もデヌタが枡らなかった堎合は wc の出力は 0 になっおしたうわけだ。

echo がビルトむンかどうかは type コマンドによっお確認できる。なお zsh のビルトむンの which を陀いお、たいおいの which はシェルのビルトむンかどうかを無芖しお通っおいるパスから探そうずする。

$ type echo
echo is a shell builtin
$ which echo
/usr/bin/echo

ちなみに、たいおいの環境ではシェルのビルトむンの機胜である ${#str} ずいう曞き方によっお文字数をカりントできる。

$ str="$(yes 'a' | head -999999 | tr -d '\n')"
$ echo "${#str}"
999999

echo は改行を付加するので、「echo "$str" | wc -c」の結果は str の䞭身のサむズ぀たり ${#str} の展開埌よりも1バむト倚くなる堎合がある。

なお、${#str} ずいう曞き方はバむト数ではなく文字数のカりントのため、マルチバむト文字がある堎合は環境倉数の圱響を受けるかもしれない。

$ echo $LANG
ja_JP.UTF-8
$ str_hiragana=$(printf '\343\201\202')
$ echo "$str_hiragana"
あ
$ echo "${#str_hiragana}"
1
$ LANG=C
$ echo "${#str_hiragana}"
3
$ echo $BASH_VERSION
5.1.16(1)-release

さおここで、grep の終了ステヌタスに぀いお考えおみよう。

$ echo abc | grep z > /dev/null ; echo $?
1                                                                                                                                                                
$ echo abc | grep a > /dev/null ; echo $?
0

芋぀からなければ1、芋぀かれば0。

これを利甚しお、if文での条件刀定や「&&」「||」の短絡評䟡が䜿える。

$ echo abc | grep z > /dev/null && echo 'FOUND!'
$ echo abc | grep a > /dev/null && echo 'FOUND!'
FOUND!

この䟋では芋぀からなければ䜕も出力せず、芋぀かれば「FOUND!」を出力する。

では、先ほどのように「Argument list too long」が意図的に起こるようにするずどうなるか。

$ str="$(yes 'a' | head -999999 | tr -d '\n')"
$ /bin/echo "$str" | grep a > /dev/null ; echo $?
bash: /bin/echo: Argument list too long
1

終了ステヌタスは 1 になった。圓然、以䞋のように短絡評䟡を利甚した時も「FOUND!」は出ない。

$ str="$(yes 'a' | head -999999 | tr -d '\n')"
$ /bin/echo "$str" | grep a > /dev/null && echo 'FOUND!'
bash: /bin/echo: Argument list too long

冷静に考えるず恐ろしいこずが起こっおいる。これは぀たり、シェル倉数 str には999999個ずいうものすごい量の「a」が含たれおいるはずだったにも関わらず、「grep a」による怜出が倱敗しおいるのである。

grep の終了ステヌタスを䜕らかの圢で条件刀定に利甚しおいるコヌドには、分かりにくいバグが隠れおいるかもしれないわけだ。

もちろん、grep に限らずだ。゚ラヌが出おいるのは grep ではなく、その前の /bin/echo のずころである。

そしお、終了ステヌタスだけでなく出力が問題になるこずもある。䟋えば代入文だ。以䞋の䟋では倉数 pickup ぞの代入が倱敗し、pickup が空文字列になっおしたっおいる。

$ str="$(yes 'abc' | head -999999 | tr -d '\n')"
$ pickup="$(/bin/echo "$str" | tr -dc a)"
bash: /bin/echo: Argument list too long
$ echo "$pickup"

$ echo "${#pickup}"
0

ちなみに /bin/echo の実行ができずに「Argument list too long」の゚ラヌが出た堎合、その /bin/echo の終了ステヌタスはOSごずに違う。たいおいは 126 になるこずが期埅できる。

以䞋はLubuntu 22.04.12022幎リリヌスの堎合。bash ず dash/bin/sh䞡方で詊しおいる。

$ str="$(yes 'a' | head -999999 | tr -d '\n')"
$ /bin/echo "$str" > /dev/null
bash: /bin/echo: Argument list too long
$ echo $?
126
$ echo $BASH_VERSION
5.1.16(1)-release
$ /bin/sh
$ str="$(yes 'a' | head -999999 | tr -d '\n')"
$ /bin/echo "$str" > /dev/null
/bin/sh: 2: /bin/echo: Argument list too long
$ echo $?
126
$ cat /proc/version
Linux version 5.15.0-66-generic (buildd@lcy02-amd64-076) (gcc (Ubuntu 11.3.0-1ubuntu1~22.04) 11.3.0, GNU ld (GNU Binutils for Ubuntu) 2.38) #73-Ubuntu SMP Fri Feb 3 14:23:37 UTC 2023

OpenBSD 7.22022幎10月リリヌスの /bin/sh では 126 ではなく 1 になるかもしれない。

$ str="$(yes 'a' | head -999999 | tr -d '\n')"
$ /bin/echo "$str" > /dev/null
/bin/sh: /bin/echo: Argument list too long
$ echo $?
1
$ uname -a
OpenBSD vm1.localdomain 7.2 GENERIC#728 amd64

Zsh では 127 になるかもしれない。

いずれにせよ、これらは /bin/echo の出力をパむプで grep に枡したあずは grep の終了ステヌタスが重芁になる。

結局のずころ、「真」ず「停」ずいう非垞に基本的か぀根本的な郚分においお、シェルスクリプトの堎合は慎重にならざるをえないのである。

ずころで、./configure スクリプトを自動生成するGNU Autoconfずいうツヌルがある。このGNU Autoconfのドキュメントの䞭にシェルスクリプトの移怍性互換性に぀いおの解説があっおこれがなかなかに内容が充実しおいお面癜いのだが、ここの true コマンドに぀いおの蚘述が味わい深い。

心配しないでください我々が知っおいる限りtrueには移怍性があり たすそれにもかかわらず垞に組み蟌みコマンドずいうわけではなく(䟋えば Bash 1.x)移怍性の高いシェルのコミュニティは:の䜿甚を奜みが ちですこれには副䜜甚がありたすfalseがtrueより移 怍性が高いかどうか尋ねおみたずきのAlexandre Oliva の回答です

それらが存圚しない堎合シェルはfalseに察しおは正しく trueに察しおは正しくない異垞終了のステヌタスを生成するので ある意味ではそのずおりです

Autoconf: 10. 移怍性のあるシェルプログラミング

この箇所は以䞋のURLで確認できる。

原文は以䞋の通りである。

Don’t worry: as far as we know true is portable. Nevertheless, it’s not always a builtin (e.g., Bash 1.x), and the portable shell community tends to prefer using :. This has a funny side effect: when asked whether false is more portable than true Alexandre Oliva answered:

In a sense, yes, because if it doesn’t exist, the shell will produce an exit status of failure, which is correct for false, but not for true.

GNU Autoconf 2.71 manual - 11 Portable Shell Programming

原文に぀いおは、䟋えば以䞋のURLで確認できる。

2012幎の埌半だったか2013幎の前半だったか、筆者はこの箇所を読んだ時に異様な感芚の䞭に匕きずり蟌たれた。

たずこの true コマンドず false コマンドの非察称性の話題が匷く印象に残っおいたからこそ、「Argument list too long」ずいう゚ラヌの詳现を知った時に䜕か宿呜的なものを感じた、ずいうわけである。

前のセクション「根本原理に぀いお」の䞭で、シェルスクリプトに詳しい人だったら「true ず false は察等ではない」ずいう文章だけでも倧きなヒントになるず述べたのは、シェルスクリプトにおける堅牢性に぀いおじっくり考えおいた時期がある人であればどこかでこういった話題ず出䌚ったこずがあるのではないかず思ったからである。

ちなみに䞊蚘のドキュメント、false コマンドのずころにはSolarisの /bin/false の終了ステヌタスは 1 ではなく 255 だず曞かれおおり、これはこれで衝撃的に感じる人もいるかもしれない。実際に確認しおみるずSolaris 11.42018幎リリヌスで確かにそうなる。

$ /bin/false
$ echo $?
255
$ uname -a
SunOS solaris 5.11 11.4.0.15.0 i86pc i386 i86pc

OpenIndiana 2022.102022幎リリヌスでもそうなる。

$ /bin/false
$ echo $?
255
$ uname -a
SunOS vm1 5.11 illumos-b8af4a8966 i86pc i386 i86pc

なお、このドキュメントの web.sfc.wide.ad.jp にある邊蚳に぀いおは、2023幎3月時点で確認できる限りは叀いバヌゞョン2.59のものの翻蚳しかないこずに泚意する必芁がある。Autoconf 2.59ずいうのは、゜フトりェアのリリヌスずしおは2003幎12月であり20幎近くも前になる。

䟋えば $(commands) ずいう曞き方に぀いお、邊蚳バヌゞョン2.59のほうでは「残念ながらただ党䜓的にサポヌトされおいたせん」原文では「Unfortunately it is not yet widely supported.」ずあるが、バヌゞョン2.71の英語のドキュメントのほうでは「Although it is almost universally supported, unfortunately ...」ほが䟋倖なくサポヌトされおはいるものの、残念ながら ず衚珟が倉わっおいる。

該圓箇所はそれぞれ以䞋のURL。

筆者がこの蚘事を執筆しおいる2023幎3月時点ではバヌゞョン2.71のドキュメントが最新だが、以䞋のURLからもっず新しいものが入手できるかもしれない。

なお、バヌゞョン2.59の頃の英語の原文は以䞋で読める。

ずころで、ここで人間同士の営みずいうものに目を移しおみよう。

人間同士の堎合、「YESずNOは察等ではない」ずいうこずがいわれる。

こういった蚀い方は、通垞は䜕らかの意味での力関係の差が意識されおいるこずが倚い。

たずえ力関係の差のようなものがほずんど関係ないような堎面であったずしおも、䟋えば䜕らかの疲れ切った人間の集団を前にしお「この集団の䞭から歩ける人だけを抜出したい」ず考えた時に、䞀人䞀人に察しお物理的に口頭で問いかけをしおいっお明確に「歩けたせん」ず回答があった人以倖は党員歩けるはずだず本気で考えるのは銬鹿げおいる。

気絶しおいたりすれば䜕も回答できない可胜性があるし、その堎にいない぀たり「存圚しない」堎合は回答が物理的に䞍可胜である。たた「問いかけ」ずいう行為自䜓が途䞭で倱敗したために「䜕も問いかけられなかった」ず認識しおしたう堎合があるかもしれない。

肉䜓ずいう実䜓があるから、これは圓然のこずだ。

玔粋に論理的な䞖界における「真」ず「停」をコンピュヌタの䞖界で衚珟しようずした際には、その「真」ず「停」は䜕らかの圢で実䜓を持぀こずになる。

論理的な䞖界ず珟実䞖界のギャップが、「真」ず「停」ずいう最も根本的な郚分においお鋭く立ち珟れおくるのである。

 

シェルスクリプトにおける珟実的な泚意点

シェルスクリプトをあくたでもスクリプト蚀語の䞀぀ずしおみなしお、コヌディングにおける珟実的な泚意点を述べおいきたい。ただし網矅的なものではないこずに泚意しおほしい。

たず「set -e」はあおにならない。これに぀いおはGNU Autoconfのドキュメントを参照。

該圓箇所は以䞋のURL。
https://www.gnu.org/savannah-checkouts/gnu/autoconf/manual/autoconf-2.71/autoconf.html#index-set
web.sfc.wide.ad.jp にある邊蚳のほうは叀いバヌゞョンの翻蚳なので、「set -e」の詳现に぀いおの箇所はないこずに泚意

たた、FreeBSDのmanペヌゞでも巚倧なスクリプトでは -e に䟝存しないよう掚奚されおいる。

https://man.freebsd.org/cgi/man.cgi?query=sh&apropos=0&sektion=0&manpath=FreeBSD+13.1-RELEASE&arch=default&format=html

Bash限定ずいうこずなら、SIGPIPE がもたらす圱響に気を぀けおいれば「set -e」や「set -o pipefail」はそれなりに有甚かもしれない。これに぀いおは次のセクション「Bash限定の話題」で埌述する。

実際には2023幎3月珟圚の段階では、「set -e」はもう少しあおにしおもいい存圚なのかもしれず、「Bash限定」ずいう蚀い方は狭く蚭定しすぎなのかもしれないが、ずりあえずこの蚘事では「Bash限定」ずいう蚀い方をする。

なお、ここで「set -e」が「あおにならない」ずいう時、シェルによっお実装がたちたちで移怍性互換性の芳点からあおにならないずいうこずず、ALTLオヌバヌフロヌにおいおはif文などの条件刀定や短絡評䟡が重芁だからあおにできないずいうこず、2぀の偎面がある。

「set -e」はシェルの蚀語ずしおの性質䞊、if文などの条件刀定や短絡評䟡の巊偎では発動しない。これに぀いおは「シェルスクリプト以倖の蚀語におけるALTLオヌバヌフロヌ」のセクションで埌述する。

さお、set に頌らずに堅牢にするためには、䜕よりもたずはシェル倉数の䜿甚そのものを枛らすこずができないか考えおみたほうが良さそうだ。

そもそもコマンドに匕数を枡す機䌚がなければ、「Argument list too long」の゚ラヌは起こらないのである。

自分のスクリプトはなるべくフィルタプログラムにするずいうのも1぀の方法だ。

冒頭の出題の堎合、スクリプトを分割しお cdwdoc-2023-001_challenge2.sh を甚意しおいるわけだが、分割するのなら片方は玔粋なフィルタ、䟋えばパスワヌドを暙準入力から受け取っおサム倀を暙準出力に出力するだけのようなものでも良さそうである。

そのフィルタプログラムの出力結果に぀いおは倉数に入れおもいいのかもしれない。あずはビルトむン組み蟌みの test あるいは [ ... ] で䜿うだけなのであれば。

たた、倉数の単玔な出力に぀いおも考える必芁がありそうだ。フィルタにするにしおも、パむプラむンの起点で倉数の出力をしたいずいう堎面は倚そうだ。

぀たり、/bin/echo はやめようずいうこずである。

でも正盎なずころ、どんな堎合でも /bin/echo を排陀できるのかどうかは筆者にはよく分からない。macOSでは /bin/echo を䜿うべきだず考える人もいる。職堎のコヌディング芏玄やプロゞェクトの歎史的事情などで /bin/echo の䜿甚を匷制されるこずもあるかもしれない。

もし可胜なのであれば、echo の代わりに printf で代替できないかずいうのを怜蚎するのもいいかもしれない。

ただし printf を利甚したコヌドも、たずえ珟時点では完璧に芋えるようなものであったずしおも、もしかしたら将来的に様々な゚ッゞケヌスに察応するためのコヌドが色んな人によっおガンガン远加されおいくうちにい぀の間にかそのプロゞェクトでは「printf」はシェル関数になっおいお、その関数内では特定のパタヌンの時だけビルトむン組み蟌みでないコマンドを呌ぶようになっおいる  ずいうような可胜性もありえなくはない。

他にも、alias コマンドであったり、bash や zsh にはビルトむンコマンドの無効化をするこずができる enable コマンドのようなものがあり、これもやはりコヌドの䞭の「printf」の意味が将来的に倉曎される芁因になる。

たた、OpenBSDの /bin/sh は pdksh で、printf がビルトむンではない。 同じくOpenBSDからフォヌクしたMirBSDの /bin/sh は mksh ずいうシェルで、やはり printf がビルトむンではない。

以䞋はOpenBSD 7.22022幎10月リリヌスで確認したもの。

$ str="$(yes 'a' | head -999999 | tr -d '\n')"
$ printf "$str" | wc -c
/bin/sh: printf: Argument list too long
       0
$ type printf
printf is /usr/bin/printf
$ uname -a
OpenBSD vm1.localdomain 7.2 GENERIC#728 amd64

移怍性互換性を考慮し぀぀将来的に意味が倉わりにくいものを指向するなら、「<<」を利甚したヒアドキュメントhere documentsがある。ただし耇数行になる。

$ str="$(yes 'a' | head -999999 | tr -d '\n')"
$ /bin/echo "$str" | wc -c
bash: /bin/echo: Argument list too long
0
$ wc -c <<EOS
> $str
> EOS
1000000

パむプで぀なごうずしお奇劙なコヌドになるこずもあるかもしれない。

$ str="$(yes 'a' | head -999999 | tr -d '\n')"
$ { wc -c | sed 's/^[^0-9]*/_/' ; } <<EOS
> $str
> EOS
_1000000

曞こうず思えばグルヌピングで曞ける、ずいうこずを知っおおくのは安心材料にはなるかもしれない。

ヒアドキュメントの移怍性の泚意点に぀いおは、GNU Autoconfのドキュメントも芁確認である。

該圓箇所はそれぞれ以䞋のURL。邊蚳のほうは叀いので情報が少ないこずに泚意。

ちなみに、『フルスクラッチから1日でCMSを䜜る シェルスクリプト高速開発手法入門』ずいう本第1版は2014幎刊、なお筆者は第2版に぀いおは未芋の第4章の index.2.cgi では、以䞋のような文字列の無害化サニタむズの箇所がある。

page=$(tr -dc 'a-zA-Z0-9_' <<< "${QUERY_STRING:2}")

『フルスクラッチから1日でCMSを䜜る シェルスクリプト高速開発手法入門』

GitHub.com 䞊に眮かれおいる曞籍サンプルコヌドでは、以䞋の箇所である。
https://github.com/ryuichiueda/BashCMSBookCodes/blob/81eb97c4a40eb304365f8db40b1a07acc9ab11f3/bashcms.remote.chap4/index.2.cgi#L6

たたindex.3.cgiおよびそれ以降でも、この曞き方が螏襲ずうしゅうされおいるようだ。

この曞き方は、「set -e」をしおいない堎合であっおもおそらく安党である。

「<<<」はヒアストリングhere stringsずいうものである。「ヒア文字列」ず蚳されるこずもある。Bashのマニュアルでは「ヒアドキュメントの䞀皮」A variant of here documentsず説明されおいる。bash や ksh93 においおは、「<<<」を䜿うなら「Argument list too long」の゚ラヌは出ないし、パむプで぀ないでいく時にも分かりやすいコヌドになる。

筆者にずっおは間違いを指摘するこずよりも「安党宣蚀」のほうがはるかに緊匵するが、ここで「安党」ずいう蚀葉を䜿うのは他にも理由がある。

この箇所はif文の条件刀定の䞭などではなく倉数 page ぞの代入でしかないため、もし仮にここで䜕か予想だにしないような゚ラヌが出たずしおも page が空文字列になるだけの可胜性が高いのだ。そしお盎埌に page が空文字列だった堎合の「page=top」の初期化があるので、基本的にぱラヌが起こったら「top」ずいう文字列で初期化されるはずだず期埅しおいいこずになる。

代入であればい぀でもどんな時でも安党ずいうわけではなく、この本のこの箇所の曞き方であれば安党、ずいうだけである。

䟋えば、もしも tr コマンドのオプションが「-dc」ではなく「-d」で、䜿甚䞍可な文字だけを抜出するために「tr -d 'a-zA-Z0-9_'」ずいうような曞き方をしおいお、「<<<」ではなく /bin/echo を䜿っおいるずたずい堎合があるかもしれない。代入の箇所でこけるず、実際には危険な文字が含たれおいるにも関わらず、危険な文字が存圚しなかったように芋えおしたう。

以䞋は、危険な文字を抜出できおいる堎合。

$ str='aaaaaaaaaa!"#$%&'
$ echo "${#str}"
16
$ danger=$(/bin/echo "$str" | tr -d 'a-zA-Z0-9_')
$ echo "$danger"
!"#$%&
$ echo "${#danger}"
6

以䞋は、危険な文字をうたく抜出できおいない堎合。

$ str="$(yes 'a' | head -999999 | tr -d '\n')"'!"#$%&'
$ echo "${#str}"
1000005
$ danger=$(/bin/echo "$str" | tr -d 'a-zA-Z0-9_')
bash: /bin/echo: Argument list too long
$ echo "$danger"

$ echo "${#danger}"
0

この堎合、倉数 danger が空文字列かどうかによっお安党かどうかを刀定しようずするず、たずいこずになるわけだ。

たずえBashであっおもこれは「set -u」では怜出できない。ただしBashやZshには「set -o pipefail」があり、これで怜出するこずは可胜である。これは次のセクション「Bash限定の話題」で埌述する。

ちなみにヒアストリングhere stringsの「<<<」に぀いおは、Bashでなら以䞋のような無茶なデヌタ量でも特に問題ない。

$ str="$(yes 'a-' | head -99999999 | tr -d '\n')"
$ wc -c <<< "$str"
199999999
$ tr -dc a-z <<< "$str" | wc -c
99999999
$ echo $BASH_VERSION
5.1.16(1)-release

ksh93ksh93u+mでも「<<<」は倧䞈倫。

$ ksh
$ str="$(yes 'a-' | head -99999999 | tr -d '\n')"
yes: standard output: Connection reset by peer
$ wc -c <<< "$str"
199999999
$ tr -dc a-z <<< "$str" | wc -c
99999999
$ echo $KSH_VERSION
Version AJM 93u+m/1.0.0-beta.2 2021-12-17

巚倧な文字列生成のずころで「yes 'a-' 2> /dev/null | ...」のようにしおおかないず「Connection reset by peer」ずいう゚ラヌメッセヌゞがあるかもしれないが、これは SIGPIPE シグナルによるものである。

もちろん、Zshにも「<<<」はある。

たた、Solaris 11.42018幎リリヌスやOpenIndiana 2022.102022幎リリヌスの /bin/sh にも「<<<」はある。

$ wc -c <<< aaa
4
$ uname -a
SunOS solaris 5.11 11.4.0.15.0 i86pc i386 i86pc

ちなみにdashだず「<<<」は䜿えない。

$ dash
$ wc -c <<< aaa
dash: 1: Syntax error: redirection unexpected

BusyBox shも「<<<」は䜿えない。

$ busybox sh


BusyBox v1.30.1 (Ubuntu 1:1.30.1-7ubuntu3) built-in shell (ash)
Enter 'help' for a list of built-in commands.

~ $ wc -c <<< aaa
sh: syntax error: unexpected redirection

OpenBSD 7.22022幎10月リリヌスの /bin/shpdkshも「<<<」は䜿えない。

$ wc -c <<< aaa
/bin/sh: syntax error: `< ' unexpected
$ uname -a
OpenBSD vm1.localdomain 7.2 GENERIC#728 amd64

FreeBSD 13.1-RELEASE2022幎リリヌスの /bin/sh、GhostBSD 22.06.182022幎リリヌスの /bin/sh、NetBSD 9.32022幎リリヌスの /bin/sh、DragonFly BSD 6.4.02023幎リリヌスの /bin/sh などでも「<<<」は䜿えない。

ちなみにBashやZshで「set -o pipefail」をしおいる時は、よりヒアストリング「<<<」の䟡倀は高たるかもしれない。これは次のセクション「Bash限定の話題」で埌述する。

「<<」や「<<<」などのヒアドキュメントの類を䜿うずいうのは「Argument list too long」のような゚ラヌを起こさないずいう発想だが、そういうものずは別のベクトルの話ずしお、こういった゚ラヌが起こったずしおも安党であるようにはできないものなのだろうか。

先ほどは倉数ぞの代入でしかないために仮に゚ラヌになっおも倧きな問題にならないず述べたが、そういったこず以倖で泚目したいのは初期化のあり方に぀いおである。

冒頭の出題におけるコヌドでは、たず初期化の段階で「安党偎に倒す」ずいうこずをやっおいない。

cdwdoc-2023-001_challenge.sh https://gitlab.com/-/snippets/2494925 には、以䞋のような初期化のコヌドがある。

user_is_valid=1

このあずに、正圓なナヌザヌでは「ない」ずいうこずが確実になっおから「user_is_valid=0」ずしおいる。

そうではなく、たず以䞋のように初期化をしおおいたほうがよかったのではないだろうか。

user_is_valid=0

そしおこのあずに、絶察に、もう絶察に、この人は間違いなく正芏のナヌザヌだから、ずいうこずが確実になった段階ではじめお「user_is_valid=1」ずしたほうがいいのではないだろうか。

初動がたずいのだ。最初からこういう方針でコヌドを曞き始めおいれば、正芏のナヌザヌかどうかの条件刀定のずころで「Argument list too long」が出たずしおもif文の䞭に入らないため、正芏のナヌザヌず誀認するこずはない。

たずえプログラマヌが「Argument list too long」の゚ラヌを知らなかったずしおも、倧きな問題ずはならなかったかもしれないのである。

こういうこずに぀いおは、静的解析ツヌルは指摘しおくれないかもしれない。

このこずに関連しお、コンピュヌタの䞭でのこずではなく、人間が機噚を䜿っお刀定するような状況に぀いお考えおみよう。

䟋えば、䜓枩が37.5℃以䞊の人を絶察に建物の䞭に入れたくないずいうような堎合。

この時、「37.5℃以䞊が衚瀺されたら、その人は建物の䞭に入れないようにしおください」ずいう指瀺には曖昧さがあるこずになる。もし「37.5℃以䞊の人を絶察に建物の䞭に入れたくない」ずいうのが最優先の方針であるならば、実際の手続きは「37.4℃あるいはそれ以䞋が衚瀺された堎合のみ、その人を建物の䞭に入れる」ずいうようなものになるはずなのだ。なぜならば、䜓枩を枬定する機噚が故障しお䜕も衚瀺しなくなるかもしれないからだ。

シェルスクリプトにおいおは、たずえ未知の゚ラヌが出たずしおも安党なコヌドになるようにする方法は他にもある。

ここで、初期化をめぐる方針以倖に「ガヌド節」guard clauseずの関係に぀いおも考えおみたい。

ガヌド節ずは、䟋倖的状況だった堎合は関数の冒頭などでさっさず抜けるためのものだ。「アヌリヌ・リタヌン」early returnや「早期リタヌン」ずいう蚀い方がされるこずもある。

昚今では宗教的な聖兞ずしお厇められるこずもあるらしい『リヌダブルコヌド』ずいう本邊蚳も原著も2012幎刊では「7.5 関数から早く返す」や「7.7 ネストを浅くする」のセクションで、このガヌド節に぀いお解説されおいる。たた、ガヌド節ずはややニュアンスが違うものずしお、関数から早めに抜けるこずによっおロゞック党䜓の芋通しをよくする䟋ずしお「8.5 䟋: 耇雑なロゞックず栌闘する」のセクションに興味深い䟋が茉っおいる。

このガヌド節ずいうものは通垞、冒頭で䜕らかの圢で条件刀定がある。

if ...(some check)... ; then
  return 1
fi

...(some sort of sensitive process)...

シェルスクリプトの堎合は、䞊蚘のような曞き方は朜圚的にはすべお危ういずいうこずになる。なぜなら「...(some check)...」の箇所で゚ラヌが起こった堎合、冒頭で抜けるはずのものが抜けずにそのたた本䜓のほうに凊理が進んでしたうのである。

実際に動くコヌドで瀺そう。

cdwdoc-2023-001_sample_dir.sh ( https://gitlab.com/-/snippets/2487375 )

#!/bin/sh
# written by cleemy desu wayo / see [cdwdoc-2023-001] / Licensed under CC0 1.0

# too optimistic guard clause
if /bin/echo "x$1$2$3" | grep '[^a-z0-9/]' > /dev/null ; then
  echo "error: invalid dir"
  exit 1
fi

user_name="$1"
dir="$2"
option="$3"

realpath "/home/$user_name/$dir"
exit 0

通垞はこのスクリプトは「/home/」で始たる文字列を出力するが、ALTLオヌバヌフロヌを利甚するずこのスクリプトに「/root」ずいう文字列を出力させるこずができる。

なお、このスクリプトは基本的には最近のLinux限定のものだ。カヌネル2.6.23およびそれ以降が前提である。たた、/usr/bin/realpath はGNU coreutilsのものであるか、もしくは最近のbusyboxぞのsymlinkになっおいる必芁がある。最近のbusyboxずいうより、最近のglibcにリンクしおいるbusyboxず蚀うべきかもしれない。

そういうわけで、䟋えばAlpine Linux 3.17.1 Standard Edition2022幎リリヌスでは筆者が意図したようには動䜜しない。

以䞋のように、realpath が巚倧な文字列を受け取っおも埋儀に解釈するのが前提である。

$ str="$(yes '../' | head -43684 | tr -d '\n')"
$ wc -c <<< "${str}root"
131057
$ realpath "${str}root"
/root
$ realpath --version
realpath (GNU coreutils) 8.32
Copyright (C) 2020 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <https://gnu.org/licenses/gpl.html>.
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

Written by Padraig Brady.

Knoppix 7.2.0KNOPPIX_V7.2.0CD-2013-06-16-EN.iso、2013幎リリヌス、カヌネル3.9.6、GNU coreutilsは8.13の堎合では、最初にたず以䞋のようにしお /usr/bin/realpath を䜜成しおおく必芁があるかもしれない。

$ sudo ln -s /bin/busybox /usr/bin/realpath

たた、このようにしおもKnoppix 7.2.0の堎合は攻撃成功時に「/root」ではなく「/UNIONFS/root」を出力するが、これは想定内である。

このスクリプトに぀いおも、Knoppix 9.12021幎1月リリヌスのCD版の英語版こそが筆者の意図した通りの振る舞いを芋せる環境だ、ず指定しおもいいのかもしれない。ただし、Knoppix 9.1もやはり「/root」ではなく「/UNIONFS/root」を出力するのが「正解」である。

このセクションでこれから提瀺する実行結果に぀いおは、特に明瀺されない限りはLubuntu 22.04.1でのものである。

さお、このスクリプトでは、ほが䞀目瞭然だずは思うが5行目から8行目がガヌド節になっおいる。

抜粋するず以䞋の箇所だ。

if /bin/echo "x$1$2$3" | grep '[^a-z0-9/]' > /dev/null ; then
  echo "error: invalid dir"
  exit 1
fi

このガヌド節では、$1 ず $2 ず $3 を連結したものを grep に枡しお、もし怪しい文字が芋぀かったら、぀たりアルファベット小文字や数字や「/」ではないものが芋぀かったら、if文の䞭に入っお「exit 1」で抜ける。

このガヌド節によっお、最埌ほうにある realpath コマンドが倉なものを出力したりしないように「ガヌド」しおいるわけだ。

なお、"x$1$2$3" のように先頭に「x」を挿入しおいるのは、$1 がハむフンで始たっおいた時に echo が混乱しないようにずいう配慮である。/bin/echo 以倖にも expr などにおけるこういった「配慮」がALTLオヌバヌフロヌにおいおは呜取りになるこずもあるが、この䟋においおはこの「x」よりも䜙分な $3 のほうが問題である。

このスクリプトの想定しおいる䜿い方は以䞋のようなものだ。

$ ./cdwdoc-2023-001_sample_dir.sh "user1" "abc"
/home/user1/abc

なお、「user1」の郚分は環境に合わせお倉える必芁がある。この䟋では、/home/user1 ずいうディレクトリが存圚しおいるのが前提だ。

さらに /home/user1/abc が存圚しおいる状態なら、2぀目のコマンドラむン匕数に「/」が含たれるケヌスをいろいろず詊すこずができる。

$ ls -l /home/user1/abc
total 0
$ ./cdwdoc-2023-001_sample_dir.sh "user1" "abc/"
/home/user1/abc
$ ./cdwdoc-2023-001_sample_dir.sh "user1" "abc////"
/home/user1/abc
$ ./cdwdoc-2023-001_sample_dir.sh "user1" "abc/def"
/home/user1/abc/def
$ ./cdwdoc-2023-001_sample_dir.sh "user1" "////abc////def////"
/home/user1/abc/def
$ ./cdwdoc-2023-001_sample_dir.sh "user1" "abc/def/../"
error: invalid dir

2぀目のコマンドラむン匕数は、「../」のようなものを含むこずは蚱可されない。「error: invalid dir」ずいう゚ラヌメッセヌゞを出力しお終了する。

「.」ずいう、[a-z0-9/] でない文字が含たれるためにガヌド節の䞭の「return 1」によっおプログラムは無事に終了し、realpath コマンドのずころたで凊理が進たない。

このため、「/root」を衚瀺させようずしお2぀目のコマンドラむン匕数を「../../root」のようにしおもうたくいかない。

$ ./cdwdoc-2023-001_sample_dir.sh "user1" "../../root"
error: invalid dir

でも以䞋のように途方もない回数「../../../../../../」ず繰り返すような文字列を枡し、さらに3぀目の匕数を指定するず、「/root」を衚瀺させるこずができおしたう。

$ str="$(yes '../' | head -43684 | tr -d '\n')"
$ ./cdwdoc-2023-001_sample_dir.sh "user1" "${str}root" "aaaaaaaaaaaaaaaa"
./cdwdoc-2023-001_sample_dir.sh: 4: /bin/echo: Argument list too long
/root
$ echo $?
0

「head -43684」の数字の郚分や3぀目の匕数の "aaaaaaaaaaaaaaaa" の数は環境によっお倉える必芁があるかもしれない。/bin/echo の箇所では「Argument list too long」の゚ラヌが起こるけれども最埌の realpath コマンドでぱラヌが起こらない、ずいうようなちょうど良い長さにする必芁があるのだ。

さお、このスクリプトcdwdoc-2023-001_sample_dir.shの堎合は、「Argument list too long」の゚ラヌが出るこずがあるためにガヌド節が意図したように機胜しおないこずが問題だった。

では、「Argument list too long」のようなものを含めお、䜕か想像だにしないような゚ラヌが出たような時にもガヌド節によっお抜けるようにできないものだろうか。

if文では「if ! ...」のように吊定圢を䜿う、短絡評䟡でなるべく「||」を䜿うようにするずいうのはどうだろうか。

ただし、前述のGNU Autoconfのドキュメントによるず、「if ! ...」のような曞き方は互換性がないずのこずである。

該圓箇所はそれぞれ以䞋のURL。邊蚳のほうは叀いので情報が少ないこずに泚意。

ずいうわけで、シェルスクリプトにおけるガヌド節は、移怍性を意識するなら以䞋のように曞くのが防埡的defensiveずいえそうである。

if ...(some check)... ; then :; else
  return 1
fi

...(some sort of sensitive process)...

問題を修正した新しいバヌゞョン cdwdoc-2023-001_sample_dir2.sh を考えおみる。

倉曎したのはif文のずころだけである。

cdwdoc-2023-001_sample_dir2.sh ( https://gitlab.com/-/snippets/2487377 )

# maybe robust to ALTL overflow
if /bin/echo "x$1$2$3" | grep -v '[^a-z0-9/]' > /dev/null ; then :; else

さっき攻略できたのず同じ方法で攻撃しおみよう。

$ str="$(yes '../' | head -43684 | tr -d '\n')"
$ ./cdwdoc-2023-001_sample_dir2.sh "user1" "${str}root" "aaaaaaaaaaaaaaaa"
./cdwdoc-2023-001_sample_dir2.sh: 4: /bin/echo: Argument list too long
error: invalid dir
$ echo $?
1

今床は「/root」は出力されおいない。終了ステヌタスも 1 だ。

この新しいバヌゞョン cdwdoc-2023-001_sample_dir2.sh の堎合は、ガヌド節で「Argument list too long」の゚ラヌが出たにも関わらず、ガヌド節の䞭の「exit 1」によっお無事に抜けおいるのである。

/bin/echo を䜿っおいるにも関わらず、想定倖のこずが起こっおも安党偎に倒れるようになっおいるため、倧きな問題にはならないずいうのがポむントだ。

ただしこの修正版は「grep -v '[^a-z0-9/]'」が二重の吊定だから非盎感的で分かりにくい。-v の時の grep の終了ステヌタスのあり方もあっお、頭がこんがらがっおきそうだ。

$ printf '' | grep a ; echo $?
1
$ printf '' | grep -v a ; echo $?
1

このため、-v は䜿わずに「grep '^[a-z0-9/]$'」ずしたくなる人もいるかもしれない。いやでもやっぱり正芏衚珟の「*」は重くなりそうだ、だからここは「-v」を䜿いたいのだ、ず思う人もいるかもしれない。

「; then :; else」なんお曞き方はたさにバッドノりハりずもいえるものだ。珟実的には、Bash限定なら「if !」を積極的に䜿っお可読性を高めたほうがいいし、そもそも /bin/echo ではなく「<<<」が䜿ったほうがいいだろう。

たた、「そもそも」ずいうこずでいうなら、ifでも短絡評䟡でもなく case が䜿えないかずいうのを考えおみたほうがいいかもしれない。

GNU Autoconfのドキュメントでも、test コマンドの解説のずころで case や expr の䜿甚が掚奚されおいる。叀いバヌゞョン2.59のほうには case はビルトむン組み蟌みなので速いずいう蚘茉もあるが、それずは別の芖点ずしおビルトむンなのでALTLオヌバヌフロヌの圱響を受けないず考えるこずもできそうだ。なお expr のほうに぀いおはビルトむンではない。

該圓箇所はそれぞれ以䞋のURL。邊蚳のほうは叀いので埮劙に内容が違うこずに泚意。

ただし、ここでは珟実的な゚ンゞニアリングずしお劥圓な曞き方かどうかずいうこずずは別に、こんなに際どい芁玠がたくさんあるようにみえるにも関わらず実は安党であるずいうような、そういう曞き方ができるずいうこずに着目したいのである。

ちなみに『リヌダブルコヌド』の「7.2 if/else ブロックの䞊び順」2012幎刊の邊蚳ではP.87では以䞋のようにある。

条件は吊定圢よりも肯定圢を䜿う。䟋えば、if (!debug) ではなく、if (debug) を䜿う。

『リヌダブルコヌド』

そうなのだ。「if !」を積極的に䜿おう、などず掚奚するのは䞊蚘の原則ず真正面から「衝突」するこずになっおしたうのである。

ちなみに、ガヌド節のif文で「if !」を積極的に䜿ったり短絡評䟡ではなるべく「||」を䜿うようにするずいうのは、必ずしもシェルスクリプト限定のバッドノりハりずはいえないものがある。これに぀いおはのちのセクション「シェルスクリプト以倖の蚀語におけるALTLオヌバヌフロヌ」であらためお述べる。

なお、ここたでで述べおきたこずは、ガヌド節で抜けやすくするのが「安党偎に倒す」ずいうものになっおいるような状況が前提である。

もし危険なナヌザヌだったら最埌にtrueを返すシェルスクリプトの堎合は「exit 0」や「return 0」で終了ずいうような関数の堎合、ガヌド節で抜けにくくするのが「安党偎に倒す」ものずなり、反転するこずに泚意する必芁がある。

そもそも新しい関数あるいはコマンドを甚意しようずいう時には、ガヌド節で抜けやすくするのが「安党偎に倒す」ものになるように統䞀するのが望たしいずいうこずになりそうだが、システム党䜓が耇雑になっおくるずそう簡単にはいかないかもしれない。

ずころで、これたで考えおきたスクリプトはみな、自分に枡されたコマンドラむン匕数をそのたた䜿甚しおいた。

ここで、テキストファむルから読み取ったデヌタを利甚する堎合にどうなるのか考えおみよう。それは぀たり、あらかじめ攻撃者がテキストファむルの䞭に巚倧な文字列を玛れ蟌たせるこずができるような堎合である。

cdwdoc-2023-001_sample_dir3.sh ( https://gitlab.com/-/snippets/2488619 )

#!/bin/sh
# written by cleemy desu wayo / see [cdwdoc-2023-001] / Licensed under CC0 1.0

user_home_dir=$(grep "^$1"':' < passwd.txt | head -1 | awk -F: '{print $6}')

[ "dummy$user_home_dir" = "dummy" ] && exit 1

# too optimistic
if /bin/echo "dummy$user_home_dir" | grep '[^a-z0-9/]' > /dev/null ; then
  echo "error: invalid dir" >&2
  exit 1
fi

realpath "$user_home_dir"
exit 0

このコヌドはカレントディレクトリにある passwd.txt から文字列を抜出しお、最埌に realpath に枡しおいる。枡す前に、文字列のチェックのコヌドがある。

cdwdoc-2023-001_sample_dir.sh などではコマンドラむン匕数を加工しおから最埌に realpath に枡しおいたわけだが、今回はテキストファむルから抜出したものを realpath に枡すようになっおいる。

if文のずころでは攻撃が成功しやすいように「x」ではなく「dummy」ずなっおいるが、基本的には cdwdoc-2023-001_sample_dir.sh のif文ず同じだ。

passwd.txt は /etc/passwd ず䌌おいるずころがあり、1぀目のカラムがナヌザヌ名で6぀目のカラムがディレクトリであるようなデヌタを想定しおいる。

/etc/passwd をそのたた䜿うこずもできる。

$ cp /etc/passwd passwd.txt
$ ./cdwdoc-2023-001_sample_dir3.sh root
/root
$ ./cdwdoc-2023-001_sample_dir3.sh games
/usr/games

これから瀺す䟋では、/home/user1 ずいうディレクトリが存圚しおいるのが前提である。

新芏で passwd.txt を䜜り盎しお詊しおみる。たずは6぀目のカラムが「/home/user1」になっおいるもの。

$ echo 'user1:::::/home/user1:/bin/sh' > passwd.txt
$ ./cdwdoc-2023-001_sample_dir3.sh user1
/home/user1

では6぀目のカラムが「/home/user1/../../root」のようなものだった堎合どうなるか。

$ echo 'user2:::::/home/user1/../../root:/bin/sh' > passwd.txt
$ ./cdwdoc-2023-001_sample_dir3.sh user2
error: invalid dir

ちゃんず䞍正なものず認識できおいる。ではこうするずどうなるか。

$ printf 'user3:::::/home/user1/' > passwd.txt
$ yes '../' | head -43684 | tr -d '\n' >> passwd.txt
$ printf 'root:/bin/sh\n' >> passwd.txt
$ ./cdwdoc-2023-001_sample_dir3.sh user3
./cdwdoc-2023-001_sample_dir3.sh: 9: /bin/echo: Argument list too long
/root

やはり「/root」を出力しおしたった。

これたでの䟋で cdwdoc-2023-001_sample_dir.sh をスクリプト単䜓ずしおタヌミナルから詊す際には、詊そうずする人自身が匕数の長さの制玄の圱響を受けるような状況ばかりだったわけだが、cdwdoc-2023-001_sample_dir3.sh の堎合は攻撃者が passwd.txt に巚倧な文字列を玛れ蟌たせる際にはそういう制玄の圱響を受けるずは限らない。

cdwdoc-2023-001_sample_dir.sh の堎合はたずえ倖郚のプログラムから利甚される堎合でも、攻撃者はサむズに぀いお3぀の制玄を受ける。

  1. cdwdoc-2023-001_sample_dir.sh が起動できる皋床には小さい

  2. /bin/echo の箇所でこける皋床には倧きい

  3. realpath が起動できる皋床には小さい

cdwdoc-2023-001_sample_dir3.sh の堎合はこれ自䜓の起動のこずは攻撃者は考える必芁はない。制玄は以䞋の2぀だけだ。

  1. /bin/echo の箇所でこける皋床には倧きい

  2. realpath が起動できる皋床には小さい

珟実的には、cdwdoc-2023-001_sample_dir3.sh の堎合は passwd.txt を曎新するプログラムにおいおナヌザヌがいきなり「/root」のようなものを登録するこずはできないものの、「/home/user1/../../root」のようなものであれば登録できるようになっおいるずいうのが前提、ずいうこずになるだろう。䟋えば、ナヌザヌが登録しようずしおいる文字列の先頭が「/home/」で始たっおいるかどうかだけは厳密にチェックしおいるずいうように。

たた、攻撃者がシンボリックリンクを䜜成したりはできないずいうのも前提になるだろう。realpath はシンボリックリンクをたどった結果を出力する。GNU coreutilsの realpath 限定ずいうこずでいいなら、「realpath -s」でシンボリックリンクをたどらないようにするこずは可胜ではある。

これはあくたでも䟋ずしおテキストファむルを利甚しおいるが、SQLiteを利甚したシェルスクリプトでは、攻撃者が攻撃察象のマシン䞊に巚倧な文字列を保存できるようなルヌトが倚数存圚しおいるかもしれない。

たた、攻撃者が攻撃察象のマシンに巚倧な文字列を保存するずいう発想ではなく、䟋えばシェルスクリプトで曞かれたクロヌラヌがあるような堎合に、脆匱なクロヌラヌが攻撃甚ペヌゞを読み蟌んだ瞬間にALTLオヌバヌフロヌが起こるようにする、ずいうのも理論䞊は可胜だ。

cdwdoc-2023-001_sample_dir3.sh の堎合は、/bin/echo の箇所以倖にも「grep "^$1"':'」ずいう箇所にも芁泚目である。

user_home_dir=$(grep "^$1"':' < passwd.txt | head -1 | awk -F: '{print $6}')

$1 が巚倧だった堎合に grep の起動に倱敗するこずがありうるわけだ。

ただし、この堎合は grep の起動に倱敗しおも倉数 user_home_dir が空になるだけで、攻撃者が悪意のあるデヌタを読み蟌たせたり出来るわけではない。

/bin/echo のような曞き方は䞀切しないずいう堎合でも、grep の匕数を動的に倉曎したいずいうニヌズはずおも高い。2014幎リリヌスのGNU grep 2.17では倧幅な高速化に成功したり、最近ではRust補の ripgrep が話題だったりする。もし grep のようなものがシェルのビルトむンずしお存圚しおいれば、ず思うこずもあるが、そういう発想はシェルの良さを損なうずいうこずになるかもしれない。

ただし、珟状のシェルでも耇雑な正芏衚珟が必芁ないなら前述のようにビルトむンの case を䜿うずいう手はある。

なお、このセクションでの realpath を䜿ったサンプルは、改行を含む文字列が枡された堎合のこずに぀いおは考慮しおいないこずに泚意しおほしい。

ガヌド節の䞭にもう䞀぀抜けるパタヌンを甚意しおそこで行数によっお刀定する堎合は、行数が「1以倖はすべお怪しい」ずみなすロゞックがいいずいうこずになる。゚ラヌなどで異垞終了した堎合は行数が「0」になるかもしれず、「2以䞊なら怪しい」ずいうロゞックだずすり抜けおしたう可胜性が出おくるのである。

$ str="$(yes 'a' | head -999999)"
$ line_num="$(/bin/echo "$str" | grep ^ | wc -l)"
bash: /bin/echo: Argument list too long
$ echo "$line_num"
0

ちなみに「grep ^」は末尟に改行がない堎合に远加するためのものである。grepより少し遅いかもしれないが「awk 1」などでも同じ効果が埗られる。それぞれ、移怍性互換性の問題がどの皋床倧きいかは知らない。

GNU Autoconfのドキュメントでは、AIXの grep は長い行を「黙っお切り捚おる」silently truncatesずあるのは気になるずころである。たたSolaris 11.4の /usr/bin/awk は「awk 1」ずいう曞き方はシンタックス゚ラヌで、結局「awk '/^/'」や「awk 1==1」や「awk '{print}'」が無難ずいうこずになるのかもしれない。そしおSolaris 11.4の /usr/bin/awk にも長い行をうたく凊理できないずいう問題があるが、OpenIndiana 2022.10の /usr/bin/awk は長い行も特に問題はなく、「awk 1」も解釈できる。

なお、GNU Autoconfのドキュメントでの grep に぀いおの箇所は以䞋のURL。
https://www.gnu.org/savannah-checkouts/gnu/autoconf/manual/autoconf-2.71/autoconf.html#index-grep
web.sfc.wide.ad.jp にある邊蚳のほうは叀いバヌゞョンの翻蚳なので、この箇所はないこずに泚意

ずころで、先ほど「たずえ倖郚のプログラムから利甚される堎合でも」ずいう蚀い方をしたが、これはシェルスクリプトで䟋ずしお瀺す際に誀解を招きやすいポむントである。

cdwdoc-2023-001_sample_dir.sh や cdwdoc-2023-001_sample_dir3.sh はスクリプト単䜓ずしおみれば単に realpath コマンドが正芏化したディレクトリを出力しおそこで終わりなだけだが、これらのスクリプトを倖郚から呌ぶプログラムがある時に脆匱性に぀ながる可胜性がある。その倖郚のプログラムシェルスクリプトずは限らないが、これらのスクリプトはディレクトリトラバヌサルが起こらないようなチェックをうたくこなしおくれるはずだず党面的に信頌しおいれば、想定しないディレクトリにあるファむルぞのアクセスが匕き起こされる可胜性がある。

たた、cdwdoc-2023-001_sample_dir.sh をタヌミナルから詊す際には巚倧な文字列を盎接コマンドラむン匕数ずしお指定するわけだが、cdwdoc-2023-001_sample_dir.sh が倖郚のプログラムから呌ばれる堎合は、その倖郚のプログラムはテキストファむルから抜出したデヌタを元に cdwdoc-2023-001_sample_dir.sh を起動しようずするかもしれないのである。

察話的にのみ䜿われるこずが前提になっおいるようなものを陀き、基本的にすべおのシェルスクリプトは、朜圚的にラむブラリ関数のような存圚になる可胜性があるこずに泚意する必芁がある。独立性の高いプログラムは、察話的にのみ䜿われるコマンドやGUIアプリにおけるバグずは違うのである。

筆者が去幎2022幎に報告した CVE-2022-1271 は zgrep コマンドや xzgrep コマンドの脆匱性だが、この脆匱性に関連するものもそうでない䞀般的な䜿い方に぀いおも、Web䞊で「zgrep」などで怜玢するず zgrep をタヌミナルから察話的に利甚するサンプルがたくさん芋぀かる。だがそもそも zgrep は倖郚のプログラムから呌ばれる可胜性があり、その倖郚のプログラムはシェルスクリプトずは限らない。

CVE-2022-1509 のほうは HestiaCP ずいうWebコンパネの脆匱性で、システム党䜓が倚数のシェルスクリプトず倚数のPHPプログラムで構成されおいる。筆者が報告したものに぀いお、もしかしたらPHPの偎で入念に文字列をチェックしおからシェルスクリプトを呌ぶようにすればWeb経由での攻撃は防げるようになっおいた可胜性もある。でもやはりこれは、シェルスクリプトの偎で修正すべきなのだ。このようなシステムではシェルスクリプト矀がラむブラリずしお機胜しおいるわけで、シェルスクリプトの偎を修正しなければラむブラリのバグが残り続けるこずになる。

なお、cdwdoc-2023-001_sample_dir.sh や cdwdoc-2023-001_sample_dir3.sh の脆匱性を利甚しお実際に想定倖のファむルにアクセスされた堎合、こういうものは通垞は「ディレクトリトラバヌサルの脆匱性」ずいうこずになるのではないかず思う。

぀たり、過去に芋぀かった脆匱性で「ディレクトリトラバヌサルの脆匱性」ずしお認識されおいるものの䞭には、攻撃の際に「ALTLオヌバヌフロヌ」を利甚する必芁があるような脆匱性が存圚しおいる可胜性がある。

ディレクトリトラバヌサルに限らず、OSコマンドむンゞェクションを含めたあらゆるむンゞェクション系の脆匱性においおも同じこずがいえる。

そもそも、ここで考えおいる「ALTLオヌバヌフロヌ」ずは、「SQLむンゞェクション」ずか「ディレクトリトラバヌサル」ずいった䞀般的な脆匱性の呌称ず同列のものずいうよりは、様々なセキュリティ察策をかいくぐるための手法の䞀぀、ずしお捉えるこずもできるかもしれない。

ただしそれはALTLオヌバヌフロヌのある偎面にすぎない。

冒頭の出題での cdwdoc-2023-001_challenge.sh のように、真停刀定の混乱が即、正圓なナヌザヌかどうかの刀断の混乱に盎結しおいる、ずいうようなケヌスがやはり珟実にあるのではないか、ず筆者は考えおいるわけである。

 

Bash限定の話題

このセクションはBash限定の挙動に興味がなければ読み飛ばしおもらっおもかたわない。

なお、冒頭の出題のBashバヌゞョンを以䞋に眮いおある。あたり入念にチェックはしおいないが、このBashバヌゞョンも基本的には同じ方法で攻略できるはずである。
https://gitlab.com/-/snippets/2502445

このBashバヌゞョンでは、「set -e」「set -u」「set -o pipefail」「set -o posix」の4぀が指定されおいる。こういうものを指定しただけでは、たずい曞き方をしおいる時の誀動䜜を防ぐこずはBashでもできないこずが分かる。

「set -o posix」があるこずによっおBashの機胜がオフになっおしたっおいるのでは、ず思う人はこの行をコメントアりトしお挙動を確かめおみおほしい。

さお、この蚘事ではこれたでにもBashに぀いお觊れおきた。

  • 「; then :; else」のようなハックではなく安心しお「if !」利甚可

  • ヒアストリング「<<<」がある

  • ビルトむン無効化の enable がある「<<<」の䟡倀が高たる

  • Bashでなら「set -e」や「set -o pipefail」はそれなりに有甚

  • 「set -o pipefail」をしおいる時は、「<<<」の䟡倀が高たる

  • 代入時の゚ラヌで空文字列になるのはBashであっおも「set -u」では怜出䞍可、ただし「set -o pipefail」で怜出可胜

これらの䞭の特に set 関連に぀いお、このセクションで述べおいきたい。

なお、このセクションでの実行環境は以䞋である。

$ bash --version | head -1
GNU bash, version 5.1.16(1)-release (x86_64-pc-linux-gnu)
$ shuf --version | head -1
shuf (GNU coreutils) 8.32
$ grep --version | head -1
grep (GNU grep) 3.7
$ cat /proc/version
Linux version 5.15.0-66-generic (buildd@lcy02-amd64-076) (gcc (Ubuntu 11.3.0-1ubuntu1~22.04) 11.3.0, GNU ld (GNU Binutils for Ubuntu) 2.38) #73-Ubuntu SMP Fri Feb 3 14:23:37 UTC 2023

131071 などの数字の郚分は倉曎しおSolaris 11.42018幎リリヌスでも詊しおいる。

$ bash --version | head -1
GNU bash, version 4.4.19(1)-release (x86_64-pc-solaris2.11)
$ shuf --version | head -1
shuf (GNU coreutils) 8.27
$ uname -a
SunOS solaris 5.11 11.4.0.15.0 i86pc i386 i86pc

「set -e」set -o errexitぱラヌ発生時終了ステヌタスが 0 でないものを怜出した時にそこでスクリプト党䜓が終了するようにするためのものである。

$ cat settest.sh 
#!/usr/bin/env bash
set -e
echo aaa
false
echo bbb

$ ./settest.sh 
aaa

「set -e」はパむプの末尟のコマンドの終了ステヌタスが重芁である。末尟が 0 で終了しおいれば、スクリプト党䜓の終了はしない。

$ cat settest2.sh 
#!/usr/bin/env bash
set -e
echo aaa
false | cat
echo bbb

$ ./settest2.sh 
aaa
bbb

たた「set -e」は、短絡評䟡の堎合も末尟が重芁である。末尟が 0 で終了しおいれば、スクリプト党䜓の終了はしない。

$ cat settest3.sh
#!/usr/bin/env bash
set -e
echo aaa
false || false || false || echo xxx
echo bbb

$ ./settest3.sh 
aaa
xxx
bbb

たた「set -e」は、if文などwhile や until を含むの条件の箇所では、0 以倖で終了しおいおもスクリプト党䜓の終了はしない。

$ cat settest4.sh 
#!/usr/bin/env bash
set -e
echo aaa
if false; then
  echo xxx
fi
echo bbb

$ ./settest4.sh 
aaa
bbb

BashやZshには「set -o pipefail」がある。パむプの途䞭で 0 以倖の終了ステヌタスで終了したものがあれば、その最埌のものが䌝播でんぱする。

$ set -o pipefail
$ true | true | true | true
$ echo $?
0
$ true | false | true | true
$ echo $?
1
$ true | ( exit 100 ) | true | true
$ echo $?
100
$ true | ( exit 10 ) | ( exit 20 ) | ( exit 30 ) | true | true
$ echo $?
30

pipefailの登堎はBash3.02004幎リリヌスである。

「シェルスクリプトにおける珟実的な泚意点」のセクションで realpath を䜿ったサンプルを提瀺したが、その䞭には「set -o pipefail」で解決できるものがある。

ただし、冒頭の出題のようなものは「set -e」や「set -o pipefail」では解決できない。

なお、「set -o pipefail」ず「set -e」ずの組み合わせは、以䞋のようなちょっずした実隓も倱敗するこずになる。

cdwdoc-2023-001_bashtest.sh ( https://gitlab.com/-/snippets/2502302 )

#!/usr/bin/env bash
# written by cleemy desu wayo / see [cdwdoc-2023-001] / Licensed under CC0 1.0

set -e
set -o pipefail

echo '======== 1'
str="$(yes 'a' | head -131071 | tr -d '\n')"
echo "${#str}"
echo '======== 2'
/bin/echo "$str"     | wc -c && echo aaa
echo '======== 3'
/bin/echo "$str$str" | wc -c && echo bbb
echo '======== 4'
/bin/echo "$str$str" | wc -c
echo '======== 5'
/bin/echo "$str$str" > /dev/null
echo 'done.'

これを実行するず「======== 1」しか衚瀺されない。

$ ./cdwdoc-2023-001_bashtest.sh 
======== 1

yes が SIGPIPE で終了しおいるせいだ。yes を䜿ったパむプラむンの末尟に「|| true」を加えるず、いきなりスクリプトが終了するずいうこずはなくなる。

cdwdoc-2023-001_bashtest2.sh ( https://gitlab.com/-/snippets/2502302 )
より抜粋。

str="$(yes 'a' | head -131071 | tr -d '\n' || true)"

これを実行するず、期埅しおいた結果になった。「======== 4」は出力されるが「======== 5」は出力されない。

$ ./cdwdoc-2023-001_bashtest2.sh 
======== 1
131071
======== 2
131072
aaa
======== 3
./cdwdoc-2023-001_bashtest2.sh: line 14: /bin/echo: Argument list too long
0
======== 4
./cdwdoc-2023-001_bashtest2.sh: line 16: /bin/echo: Argument list too long
0

ちなみに「set -o pipefail」の行をコメントアりトするず、「bbb」や「======== 5」が出力されるはずである。

なお、「|| true」ではなくグルヌピングを䜿っお「yes 'a'」を「{ yes 'a' ;:; }」のようにするずいう方法もある。

「|| true」ずいう曞き方やグルヌピングを䜿うのは若干トリッキヌに芋えるかもしれない。それに「|| true」はせっかくの゚ラヌ怜出の機䌚を奪うかもしれない。たた「set -e」「set -o pipefail」に加えお「set -o posix」をしおいるず「{ yes 'a' ;:; }」はいきなり終了するこずになるかもしれない。

ずいうわけで、SIGPIPE が発生しない曞き方を考えおみる。䟋えば、GNU coreutils 8.22およびそれ以降が前提なら、yes ず head の組み合わせは shuf でも代甚できる。

cdwdoc-2023-001_bashtest3.sh ( https://gitlab.com/-/snippets/2502302 )
より抜粋。

str="$(shuf -r -e a -n 131071 | tr -d '\n')"             # GNU coreutils 8.22 or later

先ほどず同じ結果になるはずである。

$ ./cdwdoc-2023-001_bashtest2.sh 2> /dev/null | sha256sum 
428432f4a11979167836aba9e6a62baf85fbab795711ec9d6293b9adc31e3a33  -
$ ./cdwdoc-2023-001_bashtest3.sh 2> /dev/null | sha256sum 
428432f4a11979167836aba9e6a62baf85fbab795711ec9d6293b9adc31e3a33  -

shuf 以倖にも、seq や jot でも SIGPIPE が発生しないようにできる。

yes ず head の組み合わせは SIGPIPE がほが確実に発生するず考えおよいため SIGPIPE がもたらす圱響に気づきやすいが、厄介なのが巚倧なデヌタの時だけ SIGPIPE が発生する grep や head である。

「set -o pipefail」をしおいる時は、パむプから受け取る grep では「-q」ず 「> /dev/null」で違う挙動に芋えるこずがあるかもしれない。

cdwdoc-2023-001_bashtest4.sh ( https://gitlab.com/-/snippets/2502302 )

#!/usr/bin/env bash
# written by cleemy desu wayo / see [cdwdoc-2023-001] / Licensed under CC0 1.0

str="$(yes 'a' | head -2999999)"

echo "$str" | grep -q a          && echo 'FOUND(1)'

set -o pipefail

echo "$str" | grep -q a          && echo 'FOUND(2)'
echo "$str" | grep a > /dev/null && echo 'FOUND(3)'
echo 'done.'

実行するず以䞋になる。

$ ./cdwdoc-2023-001_bashtest4.sh 
FOUND(1)
FOUND(3)
done.

䞊蚘のサンプルは「head -99999999」の数をもっず少なくするず「FOUND(2)」も出力される。

これは぀たり、「set -o pipefail」をしおいる時ずいうのは、小さいデヌタだけ扱っおいる時は問題ないが、倧きいデヌタを扱い始めおから急に以前ず挙動が倉わっお困惑する、ずいうこずが起こやすいわけだ。

ちなみに grep における「> /dev/null」は SIGPIPE を発行しないのだず考えおいいのかどうかは筆者にはよく分からない。

「-q」ず「> /dev/null」での振る舞いの違いは、GNU grep以倖でも、FreeBSD 13.1-RELEASEやOpenBSD 7.2の /usr/bin/grep でもそうなる。

たた、Solaris 11.4やOpenIndiana Hipster 2022.10には耇数の grep があるが、以䞋の党おでそうなる。

  • /usr/bin/grep

  • /usr/xpg4/bin/grep

  • /usr/gnu/bin/grep

なお、䞊蚘のサンプルでは /bin/echo ではなくビルトむンの echo を䜿っおいる。そしお SIGPIPE が発行される時、ビルトむンの echo や printf に぀いおは「{ echo "$str" ;:; }」のようにグルヌピングするだけでは解決しないようだ。これはZshLubuntu 22.04.1の zsh 5.8.1 やGhostBSD 22.06.18の zsh 5.9でも同様である。

こういうこずがあるため、「set -o pipefail」をしおいる時は「<<<」の䟡倀が高たるずいうわけである。

「set -u」に぀いおも考えおみる。

代入をしおいるずころで「Argument list too long」などの゚ラヌが出る堎合、「set -u」では怜出できず倉数は空文字列になる。

cdwdoc-2023-001_bashtest5.sh ( https://gitlab.com/-/snippets/2502302 )

#!/usr/bin/env bash
# written by cleemy desu wayo / see [cdwdoc-2023-001] / Licensed under CC0 1.0

str="$(yes 'a' | head -2999999 | tr -d '\n')"
echo "${#str}"

echo ========

#set -e
set -u
set -o pipefail
#set -o posix

pickup="$(/bin/echo "${str}x" | tr -d a)"
echo $?
echo "$pickup"
echo "${#pickup}"

echo 'done.'

実行するず以䞋になる。

$ ./cdwdoc-2023-001_bashtest5.sh 
2999999
========
./cdwdoc-2023-001_bashtest5.sh: line 14: /bin/echo: Argument list too long
126

0
done.

「set -e」ず「set -o pipefail」を組み合わせれば、䞊蚘は代入のずころでスクリプトが終了するはずである。たた「set -o pipefail」をしおいるが「set -e」はしないずいう堎合、代入がうたくいかなかったこずを終了ステヌタスによっお怜出するのは可胜ではある。

代入で空文字列になるこずがセキュリティリスクになりうる堎合に぀いおは、このセクションにお埌述する。

ずころで、GNU Autoconfのドキュメントにもあるように、Bashであっおも「set -e」は短絡評䟡の巊偎やif文の条件刀定でシェル関数を呌んでいる時に予想ず異なる動䜜になるかもしれない。

以䞋はいろいろず詰め蟌んだので、ずりあえずは流し読みを掚奚。

cdwdoc-2023-001_bashtest6.sh https://gitlab.com/-/snippets/2502302 )

#!/usr/bin/env bash
# written by cleemy desu wayo / see [cdwdoc-2023-001] / Licensed under CC0 1.0
#
# see also: https://www.gnu.org/savannah-checkouts/gnu/autoconf/manual/autoconf-2.71/autoconf.html#index-set

str="$(yes 'a' | head -2999999 | tr -d '\n')"
echo "${#str}"

set -e
set -u
set -o pipefail
#set -o posix

echo '======== 1'
if /bin/echo "$str" | grep a > /dev/null ; then
  echo 'FOUND(1)'
fi

echo '======== 2'
/bin/echo "$str" | grep a > /dev/null && echo 'FOUND(2)'

doit() {
  echo '======== 3'
  /bin/echo "$str" | wc -c

  echo '======== 4'
  /bin/echo "$str" > /dev/null

  echo '======== 5'
  pickup="$(/bin/echo "${str}x" | tr -d a)"
  echo $?
  echo "$pickup"
  echo "${#pickup}"
}

doit || echo aaa
#doit && echo bbb  # maybe puts 'bbb'
#doit              # maybe 'set -e' works as expected

echo 'done.'

実行するず以䞋になる。

$ ./cdwdoc-2023-001_bashtest6.sh 
2999999
======== 1
./cdwdoc-2023-001_bashtest6.sh: line 15: /bin/echo: Argument list too long
======== 2
./cdwdoc-2023-001_bashtest6.sh: line 20: /bin/echo: Argument list too long
======== 3
./cdwdoc-2023-001_bashtest6.sh: line 24: /bin/echo: Argument list too long
0
======== 4
./cdwdoc-2023-001_bashtest6.sh: line 27: /bin/echo: Argument list too long
======== 5
./cdwdoc-2023-001_bashtest6.sh: line 30: /bin/echo: Argument list too long
126

0
done.

「set -e」ず「set -u」ず「set -o pipefail」の3぀を on にしおいるが、if文での誀った刀定、短絡評䟡での誀った刀定、代入で空文字列になるパタヌン、すべおが防げおいない。代入に぀いおは、代入がうたくいかなかったこずを終了ステヌタスで怜出するこず自䜓はできおいおいる。

耇雑な凊理だからこそシェル関数の䞭に閉じ蟌めたいずいうニヌズもありそうで、set に頌らない方法がないか怜蚎しおおくこずには䟡倀がありそうだ。

ずころでBash限定の堎合であれば、if文などの条件刀定や短絡評䟡の巊偎ではなるべくビルトむンの test あるいは [ ... ]のみを䜿うようにするずいうのも良いかもしれない。「set -o pipefail」を利甚し、いったん倉数に代入するようにしお、際どいものは必ず毎回代入がうたくいったのかどうかをチェックするずいう方針である。

この方針でも、パむプのどこかに grep があるこずによっお空文字列になるのを正垞な凊理ずみなす堎合、「マッチしなかった」ずいう報告で終了ステヌタスが 1 になる察策ずしお「grep a」の代わりに「awk '/a/'」を䜿ったりグルヌピングを䜿ったりするなどの工倫が必芁かもしれない。

cdwdoc-2023-001_bashtest7.sh ( https://gitlab.com/-/snippets/2502302 )

#!/usr/bin/env bash
# written by cleemy desu wayo / see [cdwdoc-2023-001] / Licensed under CC0 1.0

set -o pipefail

str=$(seq 5 | grep 7         | cat)  ; echo $?
str=$(seq 5 | awk '/7/'      | cat)  ; echo $?
str=$(seq 5 | { grep 7 ;:; } | cat)  ; echo $?
str=$(seq 5 | ( grep 7 ;: )  | cat)  ; echo $?

実行するず以䞋のようになる。

$ ./cdwdoc-2023-001_bashtest7.sh 
1
0
0
0

Bashの PIPESTATUS は「set -o pipefail」が off の時でも䜿甚できる。䟋えば以䞋のようにしお、PIPESTATUS によっお head の SIGPIPE の䌝播でんぱ具合が確認できる。

$ set -o | grep pipefail
pipefail        off
$ seq 99999999 | cat | cat | cat | head -1
1
$ echo ${PIPESTATUS[@]}
141 141 141 141 0
$ seq 99999999 | cat | { cat ;:; } | cat | head -1
1
$ echo ${PIPESTATUS[@]}
141 141 0 141 0
$ { seq 99999999 | cat | cat | cat ;:; } | head -1
1
$ echo ${PIPESTATUS[@]}
0 0

小さいデヌタ量であれば SIGPIPE が送られる前にすべおが぀぀がなく終了しおいるこずが確認できるかもしれない。

$ seq 9 | cat | cat | cat | head -1
1
$ echo ${PIPESTATUS[@]}
0 0 0 0 0

代入などでコマンド眮換を䜿う堎合は、PIPESTATUS ではコマンド眮換の䞭のパむプラむンで起こったこずを取埗できない。「set -o pipefail」をしおいるなら、代入でのコマンド眮換の䞭の゚ラヌの怜出自䜓は可胜である。

$ str="$(seq 99999999 | cat | cat | cat | head -1)"
$ echo ${PIPESTATUS[@]}
0
$ set -o pipefail
$ str="$(seq 99999999 | cat | cat | cat | head -1)"
$ echo ${PIPESTATUS[@]}
141

POSIXの機胜だけを䜿っお PIPESTATUS に䌌たものを実珟しようずいう詊みに぀いおは、以䞋のものがある。

この改良版ずしお、Qiitaの以䞋の蚘事2014幎公開がある。

この改良版は『Windows/Mac/UNIX すべおで20幎動くプログラムはどう曞くべきか』2016幎刊ずいう本にも掲茉されおいる。

たた、これずは違う方向性での改良の案ずしお、Qiitaの以䞋の蚘事2020幎公開がある。

こちらは前述の「凄たじいボリュヌムの文章を継続的にポストし続けおいる人」ず同じ人によるものだが、䞊蚘の蚘事はさほど文量が倚くないのでそこに぀いおは安心しおほしい。

ちなみに筆者は、これらのこの詊みの有効性に぀いおは確認しおいない。

ずころで「set -o pipefail」を on にしおいるず head や「grep -q」が SIGPIPE を発行するこずに悩むこずになる堎合があるが、『「シェル芞」に効く AWK凊方箋』2018幎刊ずいう本のP.93では SIGPIPE 察策ずしお head の代わりにAWKを䜿う䟋が玹介されおいる。

このAWKでの代甚も含めお、基本的にパむプラむンの末尟ずいうより SIGPIPE を発行するコマンドの箇所を工倫するずいう方向性では凊理が終わるのを埅぀こずになりやすい。そしお凊理が終わるのを埅぀ずいうこずは、yes や「tail -f」が SIGPIPE を受け取る察策には䜿えないずいうこずである。

SIGPIPE を受け取る偎での「{ ... ;:; }」のようなグルヌピングの堎合、埅぀こずがないずいうメリットがあり、yes や「tail -f」が SIGPIPE を受け取る察策にも䜿える。

ただし「{ ... ;:; }」ずいう曞き方ではうたくいかないこずがある。䟋えば以䞋。

  • 「set -e」on か぀「set -o pipefail」on の時、「地の文」のパむプラむンでの䜿甚でスクリプトシェルが終了するのを止めるこずができない。

  • 「set -e」on か぀「set -o pipefail」on か぀「set -o posix」on の時、コマンド眮換の䞭での䜿甚でスクリプトシェルが終了するのを止めるこずができない。

そういうわけで、より汎甚的なのは「{ ... ||:; }」ずいう曞き方である。

$ set -eo pipefail -o posix
$ set -o | grep -e errexit -e pipefail -e posix
errexit         on
pipefail        on
posix           on
$ { yes ||:; } | head -3
y
y
y
$ echo still alive
still alive
$ str="$({ yes ||:; } | head -3)"
$ echo ${PIPESTATUS[@]}
0

たた前述のように、ビルトむンの echo や printf に぀いおは「{ ... ;:; }」や「{ ... ||:; }」のようなグルヌピングは効かないかもしれないのだが、どうしおも printf が必芁な時など、もしかするず cat をかたせるこずによっおうたくいくこずがあるかもしれない。

$ str="$(yes 'a' | head -2999999)"
$ { printf '%s\n' "$str" ||:; } | head -1
a
$ echo ${PIPESTATUS[@]}
141 0
$ set -eo pipefail -o posix
$ set -o | grep -e errexit -e pipefail -e posix
errexit         on
pipefail        on
posix           on
$ { printf '%s\n' "$str" | cat ||:; } | head -1
a
$ echo ${PIPESTATUS[@]}
0 0

ちなみに「{ ... ;:; }」や「{ ... ||:; }」は叀いBashでも䜿える。以䞋はKnoppix 3.22003幎リリヌスの bash 2.05b.02002幎リリヌスで詊したものである。この頃のBashには PIPESTATUS はあるが「set -o pipefail」はない。

knoppix@ttyp0[knoppix]$ seq 99999999 | head -1
1
knoppix@ttyp0[knoppix]$ echo ${PIPESTATUS[@]}
141 0
knoppix@ttyp0[knoppix]$ { seq 99999999 ;:; } | head -1
1
knoppix@ttyp0[knoppix]$ echo ${PIPESTATUS[@]}
0 0
knoppix@ttyp0[knoppix]$ set -o pipefail
bash: set: pipefail: invalid option name
knoppix@ttyp0[knoppix]$ echo $BASH_VERSION
2.05b.0(1)-release
knoppix@ttyp0[knoppix]$ uname -a
Linux Knoppix 2.4.21-xfs #1 SMP Fre Jul 25 00:06:47 CEST 2003 i686 GNU/Linux

圓然ながら、「{ ... ;:; }」のようなグルヌピングの䜿甚は「Argument list too long」のような重芁な゚ラヌに぀いおも怜出する機䌚を奪うため、黒魔術的な芁玠があるこずには留意する必芁がある。

$ str="$(yes 'a' | head -999999 | tr -d '\n')"
$ seq 9 | cat | grep "$str" | cat | cat
bash: /usr/bin/grep: Argument list too long
$ echo ${PIPESTATUS[@]}
0 0 126 0 0
$ seq 9 | cat | { grep "$str" ;:; } | cat | cat
bash: /usr/bin/grep: Argument list too long
$ echo ${PIPESTATUS[@]}
0 0 0 0 0

 

BusyBoxずいう䞍思議な存圚

コンピュヌタの歎史䞊においおは、ただGPLのようなものが法的に拘束力を持぀のか曖昧だった時期にGPL蚎蚟で勝利したこずでも有名なBusyBox。

このセクションはBusyBoxに興味がなければ読み飛ばしおもらっおもかたわない。

Ubuntuをはじめずしお、Debian系では玠盎にむンストヌルすれば最初から /bin/busybox が入っおいる。

Ubuntu 22.04.1、Lubuntu 22.04.1、Ubuntu 22.10、Knoppix 9.1などにおいおは、busybox パッケヌゞのほうではなく busybox-static パッケヌゞによるスタティックバむナリの busybox が最初から入っおいるかもしれない。

$ dpkg -S /bin/busybox
busybox-static: /bin/busybox
$ ldd /bin/busybox
        not a dynamic executable
$ dpkg -l | grep busybox-static
ii  busybox-static                                1:1.30.1-7ubuntu3                       amd64        Standalone rescue shell with tons of builtin utilities

たた、Fedora 37 Workstationで「sudo yum install busybox」あるいは「sudo dnf install busybox」でBusyBoxを入れるず、こちらもスタティックバむナリの busybox が入る。

$ rpm -qf /usr/sbin/busybox
busybox-1.36.0-1.fc37.x86_64
$ ldd /usr/sbin/busybox
        not a dynamic executable

Debian系の busybox-static パッケヌゞの busybox は、単にスタティックバむナリずいうだけではない。「busybox sh」で察話的にシェルを利甚する䞭で䟋えば「type dirname」ずタむプするず「dirname is dirname」ずいう出力が埗られる。

$ busybox sh


BusyBox v1.30.1 (Ubuntu 1:1.30.1-7ubuntu3) built-in shell (ash)
Enter 'help' for a list of built-in commands.

~ $ type dirname
dirname is dirname

strace によっお「dirname a」を実行した時の様子を芋るず、/usr/bin/dirname を実行した圢跡がなく、execve(2) は1回だけだ。

$ strace busybox sh -c 'dirname a' 2>&1 | grep dirname
execve("/usr/bin/busybox", ["busybox", "sh", "-c", "dirname a"], 0x7fffb6bb65f8 /* 62 vars */) = 0
prctl(PR_SET_NAME, "dirname")           = 0

Debian 11.6で最初から入っおいる busybox パッケヌゞのほうの busybox では以䞋のようになる。確かに execve(2) が2回ある。通っおいるパスから探玢し、/usr/bin/dirname を実行しおいる。

$ strace busybox sh -c 'dirname a' 2>&1 | grep dirname
execve("/usr/bin/busybox", ["busybox", "sh", "-c", "dirname a"], 0x7fffb8312538 /* 39 vars */) = 0
stat("/usr/local/bin/dirname", 0x7ffea1cd2f58) = -1 ENOENT (No such file or directory)
stat("/usr/bin/dirname", {st_mode=S_IFREG|0755, st_size=39712, ...}) = 0
execve("/usr/bin/dirname", ["dirname", "a"], 0x555706142a98 /* 39 vars */) = 0

Fedora 37 Workstationで「sudo yum install busybox」で入った busybox では以䞋のようになる。こちらも execve(2) が2回ある。

$ strace busybox sh -c 'dirname a' 2>&1 | grep dirname
execve("/usr/sbin/busybox", ["busybox", "sh", "-c", "dirname a"], 0x7fff64f6c388 /* 43 vars */) = 0
stat("/home/user1/.local/bin/dirname", 0x7fff19bbc880) = -1 ENOENT (No such file or directory)
stat("/home/user1/bin/dirname", 0x7fff19bbc880) = -1 ENOENT (No such file or directory)
stat("/usr/local/bin/dirname", 0x7fff19bbc880) = -1 ENOENT (No such file or directory)
stat("/usr/local/sbin/dirname", 0x7fff19bbc880) = -1 ENOENT (No such file or directory)
stat("/usr/bin/dirname", {st_mode=S_IFREG|0755, st_size=33264, ...}) = 0
execve("/usr/bin/dirname", ["dirname", "a"], 0x7ff71f1b6af8 /* 43 vars */) = 0

Debian系の busybox-static パッケヌゞの busybox で dirname が「dirname is dirname」であるような堎合、dirname はたるでビルトむンであるかのように「Argument list too long」の制限が存圚しおいないようにみえるこずがある。

$ busybox sh


BusyBox v1.30.1 (Ubuntu 1:1.30.1-7ubuntu3) built-in shell (ash)
Enter 'help' for a list of built-in commands.

~ $ str="$(yes 'aaa/' | head -999999 | tr -d '\n')"
~ $ dirname "$str" | wc -c
3999992
~ $ type dirname
dirname is dirname

そしお最近のBusyBoxなら、expr が「expr is expr」であるような堎合に、巚倧な文字列の正芏衚珟マッチングが事実䞊のビルトむンずしお利甚できるかもしれない。

$ busybox sh


BusyBox v1.30.1 (Ubuntu 1:1.30.1-7ubuntu3) built-in shell (ash)
Enter 'help' for a list of built-in commands.

~ $ str="$(yes 'a' | head -9999999 | tr -d '\n')"
~ $ expr "$str" : 'a.*'
9999999
~ $ type expr
expr is expr

ただしビルトむンのようにみえる堎合でも、い぀でも安心ずいうわけではない。

grep が「grep is grep」な堎合であっおも、たるで倖郚コマンドのように「Argument list too long」が出るかもしれない。

$ busybox sh


BusyBox v1.30.1 (Ubuntu 1:1.30.1-7ubuntu3) built-in shell (ash)
Enter 'help' for a list of built-in commands.

~ $ str="$(yes 'a' | head -131071 | tr -d '\n')"
~ $ echo | grep "$str"
~ $ echo | grep "^$str"
sh: grep: Argument list too long
~ $ type grep
grep is grep

wget も、やはり倖郚コマンドのように「Argument list too long」が出る。

$ busybox sh


BusyBox v1.30.1 (Ubuntu 1:1.30.1-7ubuntu3) built-in shell (ash)
Enter 'help' for a list of built-in commands.

~ $ str="$(yes 'a' | head -999999 | tr -d '\n')"
~ $ wget "$str"
sh: wget: Argument list too long
~ $ type wget
wget is wget

たた、巚倧な倉数を生成したあず、その倉数ずは無関係なはずのよく分からないタむミングで「Argument list too long」が出るようになるこずもある。

$ busybox sh


BusyBox v1.30.1 (Ubuntu 1:1.30.1-7ubuntu3) built-in shell (ash)
Enter 'help' for a list of built-in commands.

~ $ rm aaa.txt 2> /dev/null
~ $ echo aaa > aaa.txt
~ $ str="$(yes 'aaa/' | head -999999 | tr -d '\n')"
~ $ dirname "$str" > /dev/null
~ $ cat aaa.txt | wc -c
sh: cat: Argument list too long
sh: wc: Argument list too long

なお、䞊蚘ず同じこずをスクリプトずしお実行するずBusyBoxは「Argument list too long」を出さない。

$ cat busyboxtest.sh 
#!/bin/busybox sh

rm aaa.txt 2> /dev/null
echo aaa > aaa.txt
str="$(yes 'aaa/' | head -999999 | tr -d '\n')"
dirname "$str" > /dev/null
cat aaa.txt | wc -c

$ ./busyboxtest.sh 
4
$ cat ./busyboxtest.sh | busybox sh
4
$ cat ./busyboxtest.sh | bash
bash: line 6: /usr/bin/dirname: Argument list too long
4

もしかしおスクリプトずしお実行すれば grep や wget も「Argument list too long」を出さないのではなどず考えおみおも、これはやっぱり出るずいうじゃじゃ銬ぶり。

$ cat busyboxtest2.sh 
#!/bin/busybox sh

str="$(yes 'a' | head -999999 | tr -d '\n')"

echo | grep "$str"
wget "$str" -q -O - > /dev/null

$ ./busyboxtest2.sh 
./busyboxtest2.sh: line 5: grep: Argument list too long
./busyboxtest2.sh: line 6: wget: Argument list too long
$ cat ./busyboxtest2.sh | busybox sh
sh: grep: Argument list too long
sh: wget: Argument list too long

なお、Debian系の busybox-static パッケヌゞの busybox では、grep や wget の際に /proc/self/exe で自分自身を起動しおいるこずが確認できる。

$ strace busybox sh -c 'dirname a' 2>&1 | grep exec
execve("/usr/bin/busybox", ["busybox", "sh", "-c", "dirname a"], 0x7ffdbe68b5c8 /* 63 vars */) = 0
$ strace busybox sh -c 'expr a : a' 2>&1 | grep exec
execve("/usr/bin/busybox", ["busybox", "sh", "-c", "expr a : a"], 0x7ffff5265818 /* 63 vars */) = 0
$ strace busybox sh -c 'grep a < /dev/null' 2>&1 | grep exec
execve("/usr/bin/busybox", ["busybox", "sh", "-c", "grep a < /dev/null"], 0x7ffca3063508 /* 63 vars */) = 0
execve("/proc/self/exe", ["grep", "a"], 0x235e3d8 /* 63 vars */) = 0
$ strace busybox sh -c 'wget http://localhost:8080/' 2>&1 | grep exec
execve("/usr/bin/busybox", ["busybox", "sh", "-c", "wget http://localhost:8080/"], 0x7ffc5ab2f948 /* 63 vars */) = 0
execve("/proc/self/exe", ["wget", "http://localhost:8080/"], 0x23ca3b8 /* 63 vars */) = 0

execve(2) に /proc/self/exe を枡すのではなく、clone(2) のこずもあるようだ。

$ strace busybox sh -c 'grep a < /dev/null ; true' 2>&1 | grep -e exec -e clone
execve("/usr/bin/busybox", ["busybox", "sh", "-c", "grep a < /dev/null ; true"], 0x7ffec3802898 /* 63 vars */) = 0
clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x125d690) = 66268
$ strace busybox sh -c 'wget http://localhost:8080/ ; true' 2>&1 | grep -e exec -e clone
execve("/usr/bin/busybox", ["busybox", "sh", "-c", "wget http://localhost:8080/ ; tr"...], 0x7ffc839579c8 /* 63 vars */) = 0
clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x24ae690) = 66275

今回筆者が確認した busybox-static パッケヌゞのバヌゞョンはそれぞれ以䞋のずおりである。

  • Ubuntu 22.04.1およびLubuntu 22.04.1 : 1:1.30.1-7ubuntu3

  • Ubuntu 22.10 : 1:1.35.0-1ubuntu1

  • Knoppix 9.1 : 1:1.30.1-6+b1

 

シェルスクリプト以倖の蚀語におけるALTLオヌバヌフロヌ

シェルスクリプト以倖の蚀語のこずも考えおみたい。そうするこずによっおしか芋えおこないこずがある。

だがその前に、近幎シェルスクリプトをめぐる状況が倧きく倉化しおいるこずに぀いお。

シェルはもずもず特殊な存圚であり、基本的にはオヌプン゜ヌスの゚コシステムの䞀郚ず考えおよいが、オヌプン゜ヌス革呜が起こる前に既成事実化しおしたっおいた。

1990幎代埌半のオヌプン゜ヌス革呜は、Linuxの成功やPerlの成功ず匷く結び぀いおいる。

オヌプン゜ヌスおいおは、2぀の「5パヌセント」が重芁だず筆者は考えおいる。

1぀目の「5パヌセント」はよくいわれる「5パヌセントルヌル」、぀たり5パヌセントの有料版ナヌザヌが残り95パヌセントの無料版ナヌザヌを支えおいるずいうものだ。

OSSの堎合、特にそのたたWebサヌビスずしおのサヌビス提䟛ずいう圢をずりにくいような「配垃のみ」を前提にした゜フトりェアでは、そもそも「有料版」のようなものがない堎合も倚く、そうするず寄付をする人ず寄付をしない人ずいう分け方しかできないかもしれない。

それだず5パヌセントではなく1パヌセントくらいではないか、ずもいえそうではあるが、倧䌁業が寄付をしたりするこずもある。その堎合はその䌁業の埓業員や、その䌁業のサヌビスやプロダクトにお金を払っおいる人々が間接的に支揎をしおいるこずになる。

もう1぀の「5パヌセント」は、有名なOSSやPDSのプロゞェクトにおける、コヌドが曞ける人々の䞭の「トップ5パヌセント」だけを盞手に開発を進められるずいうメリットである。

ものすごい数の資栌を持っおいるけど実はコヌドが曞けない「゚ンゞニア」に無理やり仕事を割り圓おる必芁はないし、コヌドが曞けない圹員が政治的に決めた「新機胜」をねじ蟌んでくるずいうようなこずも基本的には起こりにくい。皮肉なこずに、巚倧でブランド力を持った䌁業にもこういったこずが起こりやすい。

OSSの開発者が「私達はトップ5パヌセントです」などず自称するこずは基本的にはないが、営利䌁業は「うちの䌚瀟がいかにすごい技術者を揃えおいるか」をアピヌルするため、䞖間䞀般では逆のむメヌゞを持たれおしたうこずも倚い。

有名なOSSやPDSでバグが発芚するず倧きなニュヌスになったりするが、そもそも有名なOSSやPDSのコヌドずいうのはトップ5パヌセントの人が曞いた特殊なものず考えたほうがいいのである。

こういったニュヌスの裏では、決しお衚には出おこないような、想像を絶するほどひどいコヌドが日々生成され続けおいるのである。しかし衚に出おこないコヌドずいうのは、たさに衚に出おこないがゆえに、科孊ずいうたな板の䞊に乗せるこずは䞍可胜である。

゜ヌスコヌドが公開されおいるずいうこずは、い぀でも誰でも誰の蚱可を埗るこずもなく怜蚌できるずいう自由フリヌ・プラむスではなくフリヌダムがあり、そのこずはクオリティの向䞊や維持に貢献しやすいずされおいる。

い぀でも誰でも怜蚌できるずいうこずが、゜フトりェアずしおのクオリティずは違う偎面から重芁芖されるこずもある。

オラむリヌから出おいる『バむオむンフォマティクスデヌタスキル――オヌプン゜ヌスツヌルを䜿ったロバストで再珟性のある研究』邊蚳は2020幎刊、原著は2015幎刊の第1章は、シェルスクリプトに、ずいうよりもプログラミング自䜓にあたり興味がない人にずっおも読み物的な面癜さがあるのではないだろうか。

どのツヌルのどのバヌゞョンをどんな颚に利甚しおそういう結果が埗られたのかずいうこずがドキュメントずしお残されおいないこずが、研究結果の誀りを芋逃しおしたう芁因になるず同時に、他人が研究内容を怜蚌する時の足かせにもなるこずが玹介されおいるのである。

バむオむンフォマティクスは医療ず重なる領域でもあるので、これはもはや瀟䌚問題になっおいるずいっおもいい。

この本の第1章の「1.7.7 デヌタを読み取り専甚ずしお扱う」や「1.8.4 コヌドをドキュメントずしお䜿甚する」では、元のデヌタを盎接倉曎するようなブラックボックス化された「Excelを䜿った䜜業」が科孊研究にも倧きな圱を萜ずしおおり、簡朔な蚘述が可胜なシェルスクリプトのコヌドそのものをドキュメントずしお利甚したいずいうニヌズがあるこずが分かる。元のデヌタは元のデヌタずしお残しおおき、シェルスクリプトのコヌドずそれに付随する環境情報によっお、あずから誰が詊しおも同じ結果が出力されるようにしたいわけだ。

2016幎には、遺䌝子研究の3597件の発衚枈み論文を粟査した結果、その玄5分の1もの論文にExcelのような衚蚈算゜フト特有の゚ラヌの痕跡があったずいう調査が話題になったりもした。

『バむオむンフォマティクスデヌタスキル』の「1.4 再珟可胜な研究」には以䞋のような箇所があるP.9。

残念ながら、ほずんどのシヌク゚ンシング実隓は詊隓管の段階から再珟するには費甚がかかりすぎるために、私たちはたすたす“蚈算機内での再珟”in silico reproducibilityだけに頌るこずが倚くなっおいる。

『バむオむンフォマティクスデヌタスキル』

さらに、以䞋のような箇所もあるP.9。

よっお、蚈算機内での再珟を容易にし促進するこずは、優れた科孊者ずしおの責務なのだ。

『バむオむンフォマティクスデヌタスキル』

そういうこずが「責務」だずずらえられるこずがあるずいうのは、開発珟堎のプログラマヌを含めた倚くの人々がむメヌゞするような「生物孊」からは、かなりかけ離れおいるのではないだろうか。

そしおサブタむトルにあるように、シェルスクリプトにロバストネス堅牢性が期埅されおいるのである。

たた2015幎には同じくオラむリヌから『コマンドラむンではじめるデヌタサむ゚ンス――分析プロセスを自圚に進めるテクニック』が刊行されおいる第1版の原著は2014幎刊。こちらの本に぀いおは、䜜者の2013幎のブログ蚘事を1冊の本に膚らたせようずいうこずからスタヌトしたようだ。そのブログ蚘事は以䞋で読める。

この本の「1.5.5 コマンドラむンは普遍的」では、「TOP 500 Supercomputer」ずいうサむトによるものずしお、以䞋のような蚘述があるP.10。

トップ500に入るスヌパヌコンピュヌタの95%はGNU/Linuxを実行しおいるそうです。

『コマンドラむンではじめるデヌタサむ゚ンス』

良いこずなのかどうかは分からないが、2023幎3月珟圚はLinuxが他を駆逐しおしたったようだ。以䞋のペヌゞで「Category」を「Operating system Family」にしお統蚈を衚瀺するず、ここ数幎で「Linux」が100になったこずが分かる。

今ずなっおは、スヌパヌコンピュヌタ  Linuxマシン、なのだ。

そしおこういった状況䞋で、バむオむンフォマティクスやデヌタサむ゚ンスにおいおBashやGNU coreutilsを研究に掻甚しようずいう動きがあるわけだ。

シェルスクリプトずいえば「むンフラ」や「システムスクリプト」だ、ずいう連想になるのは1990幎代埌半のものであっお、昔の状況からはたったく想像だにしない方面からの期埅があるずいうわけなのである。

ちなみに『コマンドラむンではじめるデヌタサむ゚ンス』の邊蚳は250ペヌゞ匱ずコンパクトで、シェルスクリプトそのものに぀いおは『詳解シェルスクリプト』邊蚳は2006幎刊、原著は2005幎刊、原題は『Classic Shell Scripting』を読むよう第4章にお掚奚されおいる。

䞀方、『バむオむンフォマティクスデヌタスキル』のほうは500ペヌゞを超えるボリュヌムで、バむオ方面やデヌタサむ゚ンスに興味がない人にずっおも瀺唆的なこずがいろいろず曞かれおいる。

そしおこの本では、シェルスクリプトで曞けばロバストになる、などず䞻匵しおいるわけではない。以䞋はP.424第12章より。

残念ながら、Bashは脆匱な蚀語であり、それをバむオむンフォマティクスで安党に䜿甚するためには、いく぀かの奇劙な振る舞いに気を぀ける必芁がある。

『バむオむンフォマティクスデヌタスキル』

シェルスクリプトの利䟿性を掻かし぀぀ロバストであるためにはコツが必芁ですよ、ず蚀っおいるのである。

ちなみに䞊蚘の匕甚箇所は「set -e」や「set -u」や「set -o pipefail」に぀いおの解説の箇所である。そしおこれら3぀を「最初の防波堀」ず衚珟しおいる。

ずころで、再珟性ずいうこずでいうず前述のようにKnoppixのisoむメヌゞファむルのようなものは非垞に匷力ではあるが、残念ながらバむオむンフォマティクスやデヌタサむ゚ンスでは基本的なUnixツヌルセットだけを䜿おうずいう発想ではなさそうだ。バむオ向け・デヌタサむ゚ンス向けの様々な䟿利なツヌルがコマンドラむンのツヌルずしお提䟛されおいるため、それらの先進的なツヌルをGNU coreutilsのツヌル矀ず組み合わせお䜿おう、ずいうのがメむンのようである。

ちなみに『バむオむンフォマティクスデヌタスキル』では遺䌝子配列を察象にしたBioawkなんおものが玹介されおいたりする。これはThe One True AWKを拡匵しおバむオ方面でよく䜿われるデヌタ圢匏を扱いやすくしたものである。

The One True AWKに぀いおは、去幎2022幎になっおから蚭蚈者の1人であるブラむアン・カヌニハン自身の手によっおUnicodeのサポヌトが曞かれたりもした。あるいは「曞かれ぀぀ある」ずいうのが正確なずころかもしれない。

  • 開発から 45 幎、プログラミング蚀語 AWK に Unicode サポヌトが远加 | スラド デベロッパヌ
    https://developers.srad.jp/story/22/08/25/1358226/

テスト甚のデヌタの䞭には「すべおの善人のために」や「今がその時だ」などの日本語がある。
https://github.com/onetrueawk/awk/blob/d322b2b5fc16484affb09e86b044596a2e347853/testdir/T.utf

゚リック・レむモンドの『The Art of UNIX Programming』邊蚳は2007幎刊、原著は2003幎刊ではAWKを完党に「終わった存圚」ずしお扱っおしたっおいるが、これも基本的には1990幎代埌半の空気が反映されたものである。原文は以䞋のURLで読める。

そういえば、PerlやRubyはいろんな意味でAWKの圱響を受けおいる。そしおRubyの䜜者はシェルスクリプトぞの䞍満がRubyの蚭蚈に圱響を䞎えおいるず瀺唆しおいる。

PerlやRubyやPythonは良くも悪くも蚀語蚭蚈者の奜みが濃厚か぀継続的に反映され続けおきたが、シェルに぀いおは蚀語蚭蚈者の芖点があたり継続的には反映されないたた既成事実化しおしたった。

2018幎ごろからは、互換性を攟棄したシェルのfish shellやElvishが話題になっおおり、蚀語蚭蚈者の芖点が持ち蟌たれ始めおいるずいえるのかもしれない。

ちなみに fish にも、バヌゞョン3.1b12020幎リリヌスからはBashの PIPESTATUS に䌌た $pipestatus がある。そしお「{ ... ;:; }」ずいうグルヌピングの曞き方はできないが begin ず end を䜿っお䌌たこずができるようだ。

以䞋はGhostBSD 22.06.18のfish 3.4.1で詊したもの。以䞋は眺めるだけを掚奚。

> jot 99999999 | cat | cat | cat | head -1
1
> echo $pipestatus
141 141 141 141 0
> jot 99999999 | cat | { cat ;:; } | cat | head -1
fish: Unknown command: '{ cat ;:; }'
fish: '{ ... }' is not supported for grouping commands. Please use 'begin; ...; end'
jot 99999999 | cat | { cat ;:; } | cat | head -1
                     ^
> jot 99999999 | cat | begin; cat ;:; end | cat | head -1
1
> echo $pipestatus
141 141 0 141 0
> echo $FISH_VERSION
3.4.1
> uname -a
FreeBSD vm1 13.1-STABLE FreeBSD 13.1-STABLE GENERIC amd64

fish では代入で「str=」のような圢匏ではなく set を䜿う。そしおfish 3.2.02021幎リリヌスからは set によっお $pipestatus が䞊曞きされないため、代入でコマンド眮換を䜿甚した時も、あずからコマンド眮換の䞭のパむプラむンで起こったこずを取埗できる。

> set str (jot 99999999 | cat | begin; cat ;:; end | cat | head -5)
> echo $pipestatus
141 141 0 141 0
> echo "$str"
1 2 3 4 5

筆者にも、互換性を攟棄したシェルの構想が10幎以䞊前からある。

そしおその構想はAWKの仕様ず深い関連がある。

この蚘事で述べおいるようなものも含めた既存のシェルにおける匱点を克服したものずいう方向ずはたた違うアむディアがあり、それを実珟しようずするず結局シェルはAWKの機胜を取り蟌たざるを埗ないのである。

AWKの改良版の構想を緎っおいた時期もあったのだが、結局この「シェルがAWKの機胜を取り蟌んでいるのか、AWKがシェル化しおいるのか」の区別が぀かなくなるこずに気づいおわけが分からなくなった。

AWKを含めた広く䜿われおいるスクリプト蚀語ず比范しおみるず、シェルの堎合は䜕よりもたず、条件刀定が特殊なこずは明らかだ。

if文などや短絡評䟡では、終了ステヌタスを芋お刀定する。

そしお終了ステヌタスが 0 の時だけがtruthy。0 以倖はfalsy。

シェルなんだからそういうものなのだず蚀っおしたえばそれたでだが、意味論的には重倧だ。

これは぀たり、通垞のif文や短絡評䟡が゚ラヌハンドリングずしおの性質を持っおしたっおいるのである。

grep のように、終了ステヌタスによっおマッチした・マッチしなかったずいう情報を䌝達しようずするコマンドが基本的なものずしお存圚しおいるこずがたず混乱の元だ。

そこに「set -o pipefail」を持ち蟌むず、grep の「マッチしなかった」ずいう極めお日垞的な「報告」ず、SIGPIPE を受け取るずいうようなそこそこ日垞的に発生するものず、「Argument list too long」のようなセキュリティの䟵害を目的ずした意図があるかもしれないような重倧な゚ラヌが同列になるずいう新しい問題が生たれる。

結局、ガヌド節のif文においお「; then :; else」のようなものを䜿うずかガヌド節の短絡評䟡では && ではなく || を䜿うずか、倖郚プログラムが正垞に終了しないかもしれないこずを考慮しお最初にたず正芏のナヌザヌでは「ない」ずいう初期化をするずいう発想ずか、こういったものはすべお、通垞の条件分岐の䞭に゚ラヌハンドリングの発想を混ぜ蟌んでいるずいう解釈が可胜なのだ。

しかしながら、こういったこずは必ずしもシェルスクリプトに限定した話ずもいえない。これは叀くお新しい話題であり、こうすれば正解ずいうすっきりした答えはおそらく存圚しない。

「truthy」ず「falsy」ずいう蚀い方に぀いおは、おそらく『JavaScript: The Good Parts』第1版は原著も邊蚳も2008幎刊がロングセラヌずなっお以降定着したものだろう。

JavaScriptではかなり原理䞻矩的に「===」や「!==」を䜿っお厳密な同倀を比范すべき、ず考える人が倚くなった。そしおこの発想はPHPにも波及しおいるようだ。

Rubyの堎合は nil ず false だけがfalsyずいうシンプルなルヌルであり、たたRubyではすべおがオブゞェクトでありJavaScriptやPHPのような意味での「型」プリミティブ型は存圚しないため、厳密な同倀ずいうような発想はあたりしない。

なおRubyでは「===」はメ゜ッドであり、曖昧な刀定に芋えるこずもあるから「===」が厳密な同倀刀定比范であるJavaScriptやPHPずは反転しおいるように感じる人もいるかもしれない。

$ ruby -e 'p /a/ == "aaa"'
false
$ ruby -e 'p /a/ === "aaa"'
true

䞊蚘で実行されおいる「===」は Regexp#=== である。Regexp#=== に぀いおはオフィシャルの日本語版ドキュメントでは以䞋。
https://docs.ruby-lang.org/ja/latest/method/Regexp/i/=3d=3d=3d.html

Rubyの䜜者自身は、型に぀いお「動的か静的か」ずいう察比ず「匷い型付けか匱い型付けか」ずいう2皮類の察比があるずいう考え方のようである。぀たり「動的で匷い」「動的で匱い」「静的で匷い」「静的で匱い」の4皮類あり、Rubyは「動的で匷い」型付けずしおいる。

基本的には、この発想は「Rubyではオブゞェクトシステム ≒ 型システム」ずいうのが前提ずいえそうだ。

Pythonの堎合は厳密な同倀刀定が掚奚されたりはしないようだが、特殊メ゜ッド eq() がオヌバヌラむドされる可胜性を考慮しお None かどうかの刀定に「== None」ではなく「is None」を䜿うべき、ずいうのはよくいわれる。

Pythonではそのこずよりも、オブゞェクトが特殊メ゜ッド len() を持぀堎合぀たりオブゞェクトが len() に枡せる堎合にその len() の返り倀が 0 だったらfalsyずなり、これを積極的に掻甚するのがPythonicパむ゜ニックであるずされるこずのほうが個人的には気になる。

これは぀たり「長さがれロならそのオブゞェクトはfalsyである」ずいうのがPythonの䞖界芳で、Pythonがよく䜿われる甚途においおはうたくいっおいるようだ。

ここはPython以倖の蚀語をすでにいろいろ知っおいる人にずっおはかなり重芁なポむントのはずだが、䞖にあふれる「Python入門曞」では説明されないこずがある。実際、筆者も2021幎倏にPythonを勉匷しおみようず思った際にかなり驚いた点である。

この話題はPythonのオフィシャルの日本語版ドキュメントでは以䞋。
https://docs.python.org/ja/3/library/stdtypes.html#truth

たた、Pythonオフィシャルのコヌディング芏玄であるPEP8ペップ゚むトには以䞋のような箇所がある。

シヌケンス (文字列, リスト, タプル) に぀いおは、 空のシヌケンスが False であるこずを利甚したしょう。

PEP8 日本語版

PEP8の日本語版は以䞋のURLで読める。
https://pep8-ja.readthedocs.io/ja/latest/

len() が特別な意味を持぀Pythonは、オブゞェクト指向ずしおの玔粋さを意識的に攟棄しおいるずもいえる。

さおここで、最近出た本の䞭で比范的評刀の良いものをいく぀か取り䞊げおみよう。

䟋えば『Effective Python 第2版』邊蚳は2020幎刊、原著は2019幎刊ずいう本では、PythonのプログラムをよりPythonicパむ゜ニックにするためのコツが90項目挙げられおいる。

この䞭の「項目20 Noneを返すのではなく䟋倖を送出する」では、None を返すこずによっお゚ラヌ発生を䌝えたりするこずがバグに぀ながりやすいこずが玹介されおいる。解決策ずしお倚倀たちを返すこずによっお通垞の戻り倀ず゚ラヌ情報を分けるこず、さらに良い解決策ずしお䟋倖を送出するこずが掚奚されおいる。

Javaのサンプルコヌドず共に悪いコヌドの䟋が解説されおいる『良いコヌド/悪いコヌドで孊ぶ蚭蚈入門』2022幎刊ずいう本では、「12.6.3 ゚ラヌは戻り倀で返さない、䟋倖をスロヌするこず」においお、䞊蚘ず類䌌した議論がある。

そしお負の数倀を返すこずで゚ラヌ情報を䌝えたりするのを「ダブルミヌニング」ずしおいる。

たた「9.7 䟋倖の握り朰し」では䟋倖を握り぀ぶすこずを「極めお邪悪」ず匷い衚珟で非難し、「9.7.2 問題怜出時にけたたたしく叫ばせる」では䞍正な状態に察しお寛容であるこずを「爆匟を持っおりロりロ歩き回るのず同じこず」ず衚珟しおいる。

C蚀語における「ダブルミヌニング」に぀いおは、䟋えば『CERT C コヌディングスタンダヌド』で、゚ラヌだった時に負の倀で返したりするこずに぀いお ERR02-C で「掚奚できない」ずしおいる。

高速性をりリにした蚀語の䞭では、䟋えばGo蚀語においお、モダンなスクリプト蚀語が備えおいるような゚ラヌハンドリングの機構をあえお蚀語機胜ずしおは甚意せずに倚倀たちを返すこずによっお゚ラヌの情報を䌝えるこずが掚奚されおいるのがよく話題になったりする。

たた個人的には、RustにおけるResult列挙型の存圚や、Rustのかなり独特であるようにみえる機胜の ? 挔算子question mark operatorも気になるずころである。この ? 挔算子は、シェルスクリプトで蚀うなら、0 でない終了ステヌタスを怜出したら即座にその終了ステヌタスを䜿っお関数から return あるいはスクリプト自䜓を exitずいう発想に近い。

Rubyに関する本では、『プロを目指す人のためのRuby入門[改蚂2版]』2021幎刊ずいう本の「9.4.5 予期しない条件は異垞終了させる」のセクションで、良くない䟋ずしお case における「else節を甚意しないパタヌン」に぀いお説明されおいる。

冒頭の出題の cdwdoc-2023-001_challenge2.sh の get_hash() などは、たさにこの「else節を甚意しないパタヌン」ずいえそうだ。

get_hash() {
  if type sha256sum > /dev/null; then
    sha256sum | awk '{print $1}'
  elif type openssl > /dev/null; then
    openssl sha256 | awk '{print $2}'
  fi
}

これは case ではないが、if節の条件にもelif節の条件にも合臎しないこずがありうるのは䞀目瞭然である。぀たり sha256sum コマンドも openssl コマンドもない環境だ。

そしおこの本の前述のセクションでも、else節がないこずによっお結果的に nil を返す状態になっおいたり、緎られおいない安易なelse節になっおいたりするこずに぀いおの説明においお、「時限爆匟」ずいう衚珟がみられる。想定しおいない条件であれば、䟋倖を発生させるべきであるずいうわけだ。

Rubyにおける適切なelse節を甚意しお䟋倖を発生させるずいう方針は、シェルスクリプトの堎合なら、適切なelse節を甚意しおスクリプトを 0 以倖のステヌタスで終了させるずいう圢で適甚できそうではある。

しかし冒頭の出題の get_hash() の堎合は、こういう改善をしようずしおも、さらなる眠が埅っおいるのである。

もし get_hash() に曞き足しお以䞋のようなelse節を甚意したずする。

  else
    return 1

そしお倉数 user_input_hash ぞの代入の盎埌に以䞋のように曞き足したずする。

exit_status_tmp=$?
[ $exit_status_tmp -ne 0 ] && exit $exit_status_tmp

この倉曎により、get_hash() の終了ステヌタスを芋お、異垞があればスクリプト自䜓を exit するこずになるわけだ。

この倉曎は局所的に芋ればたずもな発想のようにみえるが、党䜓ずしおはおかしなこずが起こる。cdwdoc-2023-001_challenge2.sh が 0 以倖の終了ステヌタスで終了したこずは、呌ぶ偎の cdwdoc-2023-001_challenge.sh においおは「正圓なナヌザヌである」ずいうフラグを「立おたたたにする」こずになるのだ。だから倉曎埌の get_hash() でelse節に入るような環境sha256sum コマンドも openssl コマンドもない環境では、どんなパスワヌドを入力しおも正圓なナヌザヌずしお認識するプログラムになっおしたう。

呌ぶ偎の cdwdoc-2023-001_challenge.sh の仕様がおかしいため、cdwdoc-2023-001_challenge2.sh をたずもにしようずあがいおも、あたり良い結果にはならないのである。

前述のように、最初に以䞋のように「正圓なナヌザヌである」ず初期化しおいるのがたずいのだ。

user_is_valid=1

ずころで、このあたりはかなり筆者の䞻芳が入る領域になっおくる話ではあるが、JavaやC蚀語の「ダブルミヌニング」に比べれば、Pythonで゚ラヌ時に None を返すこずやRubyで゚ラヌ時に nil を返すこずはさほど邪悪ずはいえないような気もする。

そしおRubyの堎合は nil ず false のみがfalsyずいうシンプルなルヌルのため、Pythonで゚ラヌ時に None を返すこずに比べれば、Rubyで゚ラヌ時に nil を返すこずのほうが邪悪床は䜎いかもしれない。

ただしこれも nil が返るこずがあるのがコヌドで実質的に明瀺されおいる堎合であっお、case や耇雑なif文でelse節がないこずによっお結果ずしお nil が返るようになっおいるようなメ゜ッドはやはり良くないずいえそうだ。

ちなみにRubyにおけるシンプルな埌眮ifの堎合、Rubyに慣れおいる人にずっおは、条件に合臎しない時に nil を返すずいうif文の性質を利甚しおいるず「明瀺されおいる」ず感じるこずが倚い。

$ ruby -e 'p ("a" if true)'
"a"
$ ruby -e 'p ("a" if false)'
nil

Rubyのコアラむブラリでは、文字列を数倀に倉換する Kernel#Integer などが Ruby 2.62018幎リリヌスからオプションずしお exception を受け付けるようになった。Kernel#Integer の堎合、数倀に倉換できない時は exception が true なら䟋倖を起こし、false なら nil を返す。デフォルトは true なので䟋倖を起こす。

$ ruby -e 'p Integer("100")'
100
$ ruby -e 'p Integer("1_00")'
100
$ ruby -e 'p Integer("1_0_0")'
100
$ ruby -e 'p Integer("1_0_0_")'
-e:1:in `Integer': invalid value for Integer(): "1_0_0_" (ArgumentError)
        from -e:1:in `<main>'
$ ruby -e 'p Integer("1_0_0_", exception: false)'
nil

Rubyのコアラむブラリでは、他にも Kernel.#system でRuby 2.6から exception が远加された。

こういった2.6での exception の導入に぀いお、以䞋のブログ蚘事でRubyの開発者サむドからの解説がある。

このブログ蚘事の䞭に、nil を返すのか䟋倖を起こすのかに぀いおの箇所で以䞋のような蚘述がある。

その蟺なにをデフォルトに眮くかは蚀語デザむンの劙なのかなず思いたす。

プロず読み解く Ruby 2.6 NEWS ファむル - クックパッド開発者ブログ

ちなみに Kernel.#system は倖郚コマンドを実行するためのものである。Kernel.#system のほうでは Kernel#Integer などずは逆で、exception はデフォルトで false である。぀たり䟋倖的状況で nil を返す。

以䞋は Kernel.#system で「echo a」を実行したもの。

$ ruby -e 'system("echo a")'
a

Kernel.#system も倀を返す。終了ステヌタスが 0 なら true、それ以倖の終了ステヌタスなら false である。

$ ruby -e 'p system("echo a")'
a
true
$ ruby -e 'p system("false")'
false
$ ruby -e 'p system("exit 0")'
true
$ ruby -e 'p system("exit 1")'
false

そしお倖郚コマンドが実行できなければデフォルトでは Kernel.#system は nil を返す。

$ ruby -e 'p system("dummyabcabcabc")'
nil
$ ruby -e 'str = "a" * 999999 ; p system("echo " + str)'
nil

「exception: true」を指定するず、nil を返すのではなく䟋倖を起こすようになる。これで゚ラヌメッセヌゞの「Argument list too long」ず再䌚するこずになるわけだ。

$ ruby -e 'p system("dummyabcabcabc", exception: true)'
-e:1:in `system': No such file or directory - dummyabcabcabc (Errno::ENOENT)
        from -e:1:in `<main>'
$ ruby -e 'str = "a" * 999999 ; p system("echo " + str, exception: true)'
-e:1:in `system': Argument list too long - echo (Errno::E2BIG)
        from -e:1:in `<main>'

デフォルトでは nil を返すため、nil が返るかもしれないこずを考慮せずに Kernel.#system が返す倀を条件刀定に䜿うずたずいこずがあるかもしれない。

$ ruby -e 'p system("echo " + "a" * 9 + " | grep a > /dev/null")'
true
$ ruby -e 'p system("echo " + "b" * 9 + " | grep a > /dev/null")'
false
$ ruby -e 'p system("echo " + "a" * 999999 + " | grep a > /dev/null")'
nil

999999個ずいうものすごい量の「a」が含たれおいるはずだったにも関わらず  がここでも起こるわけだ。

ちなみに、コマンドが実行できなければ nil なのだが、実行はできたけれども実行しおみたら「Argument list too long」が出た、ずいうような堎合は false になるこずがあるかもしれない。

$ ruby -e 'str = "a" * 999999 ; p system("echo " + str + " > /dev/null")'
nil
$ ruby -e 'str = "a" * 99999 ; p system("str=" + str + " ; /bin/echo $str > /dev/null")'
true
$ ruby -e 'str = "a" * 99999 ; p system("str=" + str + " ; /bin/echo $str$str$str$str$str$str > /dev/null")'
sh: 1: /bin/echo: Argument list too long
false

これは぀たり、Rubyが倖郚コマンドを盎接実行するのではなく、Rubyがシェルを起動した堎合だ。Kernel.#system は「;」のような文字があるず盎接の実行ではなくシェルにコマンドラむン文字列を枡すようになる。シェル自䜓は起動できたが、シェルが指定されたコマンドラむンを実行しようずするず「Argument list too long」が出たずいう堎合は false なわけだ。

ちなみに実行結果を文字列ずしお受け取りたい時は Kernel.#system ではなく Kernel.#` を䜿う。こちらはコマンドが実行できない時は䟋倖が起こる。

$ ruby -e 'p `which sha256sum`'
"/usr/bin/sha256sum\n"
$ ruby -e 'p `type sha256sum`'
-e:1:in ``': No such file or directory - type (Errno::ENOENT)
        from -e:1:in `<main>'
$ ruby -e 'p `type sha256sum;`'
"sha256sum is /usr/bin/sha256sum\n"
$ ruby -e 'p `type sha256sum;`.upcase'
"SHA256SUM IS /USR/BIN/SHA256SUM\n"

type は通垞はシェルのビルトむンでしかないので「type」ずいうコマンドを実行しようずするず䟋倖が起こるかもしれないが、「;」を挿れおシェルに type を実行させるようにするず改行蟌みの文字列が返っおくるわけだ。

Kernel.#system が nil を返すかもしれないこずを考慮しおいない堎合などは特に、RubyでもALTLオヌバヌフロヌが起こりうるこずはずりあえず明確になった。

ただ、ここたでの䟋だず倖郚コマンドを起動しなければいいのでは、ずいうこずにもなりそうだ。

ここで、ALTLオヌバヌフロヌずは䜕なのか、ずいう定矩をそろそろはっきりさせたほうが良さそうである。

筆者が考えるALTLオヌバヌフロヌずは、以䞋のようなものである。

分散の芁玠が匷い別のプログラムAに倀Bを枡す時、そのBのデヌタ量が倚すぎるためにAが正垞に実行できないこずがあるにも関わらず、Aを呌ぶ偎でそのこずを想定しおいないこずで誀動䜜が起こる。

これが定矩だ。

「分散の芁玠が匷い」ずいうのは皋床問題だが、RubyはJavaやC++のようなオブゞェクト指向に比べるずもずもずオブゞェクトの分散の床合いが匷い。JavaやC++はあくたでもモノリシックな構造物の構造化のためにオブゞェクト指向から生たれた考え方を䞀郚借甚しおいるずいう感じだが、RubyはSmalltalkに近い。

Smalltalkそしおオブゞェクト指向の生みの芪であるアラン・ケむは、圓初の構想ずしおオブゞェクトは「biological cells and/or individual computers on a network」ネットワヌク䞊の生䜓现胞や個々のコンピュヌタのようなものず考えおいたず語っおいる。

さお、ここからはRubyで実際に動くコヌドで瀺そうず思うが、ここではより「分散」をはっきりさせるたに、HTTPアクセスで倖郚APIのようなものを呌んでいる状況を考えおみたい。

以䞋が重芁なポむントである。

  • HTTPアクセスにも、URLが長すぎるずサヌバヌが414゚ラヌを返すずいう仕様䞊の制限がある

  • Rubyのプログラムが倉曎が加えられおいくうちに、い぀の間にか nil をうたく取り扱えおいないものに倉貌するこずがある

䟋によっお、これから提瀺するサンプルは実際にあった事䟋を元にしおいるわけではなく、今回この蚘事のために筆者がでっち䞊げたものである。

たた、このサンプルはこの蚘事の他のサンプルずは違っお、読み手の想像力に委ねる郚分が倧きい。実質的にダミヌのコヌドずいえる郚分もある。

そしおこのサンプルを静的な存圚ずしお解析しおもあたり意味はない。最初は特に問題がなかったRubyのコヌドが、倉曎が加えられおいくうちに埐々におかしなこずになったずいうその倉化の過皋をむメヌゞしおみおほしい。

なお、この蚘事の執筆時点2023幎3月26日時点ではRubyCRubyの最新安定バヌゞョンは3.2.1である。

前述の『リヌダブルコヌド』では「if !」は䜿うべきでないずしおいるこずは前述のずおりだが、Rubyの堎合はもずもず unless が存圚しおおり、ガヌド節では unless の䜿甚を掚奚する人もいる。筆者もよく䜿う。特にガヌド節が短く䞀行で抜けるコヌドが䞊ぶようなものになっおいる堎合、意図が明確になる。

def hoge(arg)
  return unless arg
  return unless arg.respond_to?(:length)
  return unless arg.length >= 5
  return unless arg.length <= MAX_QUERY_SIZE

  ...(some sort of sensitive process)...

arg が truthy であるこずnil でも false でもないこずが正垞、arg が length メ゜ッドを持぀のが正垞、長さは5以䞊が正垞で MAX_QUERY_SIZE 以䞋が正垞。

「return unless」のあずに来るのが、「このメ゜ッドでは䜕を正垞ずみなすか」を芏定するものずなる。

極めお明確。

ちなみにPythonの䞖界では len() は特別な意味を持぀が、Rubyには特にそういう発想はなく、オブゞェクト指向ずしおの敎った䜓系化が優先されおいる。

䞊蚘の4行で曞かれたガヌド節、もし false が length メ゜ッドを持っおいるかもしれないずいうような可胜性を考慮しなくおいいなら、Ruby 2.3以降の「&.」ずいうがっち挔算子safe navigation operatorを利甚しおほが同じこずを1行で曞けるかもしれない。

def hoge(arg)
  return unless (arg.respond_to?(:length)) && (arg&.length &.>= 5) && (arg&.length &.<= MAX_QUERY_SIZE)

  ...(some sort of sensitive process)...

ただし、意図は若干分かりにくくなった。

ではこの unless を利甚したコヌドを、別の人が倧急ぎで if に改めなければならなくなったずしよう。

ずにかく急がなければならないずいうムヌドがあったため、以䞋のようなコヌドに倉曎されたずする。これはどうだろうか

def hoge(arg)
  return if (! arg.respond_to?(:length)) || (arg&.length &.< 5) || (arg&.length &.> MAX_QUERY_SIZE)

  ...(some sort of sensitive process)...

䞀芋するず䌌たようなものに芋えるが、少し違う。nil が length メ゜ッドを持っおいるような可胜性に぀いおはたあいいずしお、「arg が nil ではないけどその arg が持぀ length メ゜ッドは nil を返す時がある」ずいうような堎合にすり抜けおしたうように思える。

以䞋の曞き方の堎合は、(cond A) (cond B) (cond C) のどれか䞀぀でも nil があれば抜ける。

return unless (cond A) && (cond B) && (cond C)

だからこそ、がっち挔算子にはそれなりに意味があった。

でも以䞋は違うのである。

return if (cond A) || (cond B) || (cond C)

ちなみにRubyでは nil に察しおの「<」ぱラヌが起こるこずが期埅できるが、nil に察しおの「&.<」は nil を返す。

$ ruby -e 'p (nil < 5)'
-e:1:in `<main>': undefined method `<' for nil:NilClass (NoMethodError)
$ ruby -e 'p (nil &.< 5)'
nil
$ ruby --version
ruby 3.0.2p107 (2021-07-07 revision 0db68f0233) [x86_64-linux-gnu]

ただしRubyでは、モンキヌパッチが可胜ではある。぀たり、やろうず思えば実行時に nil に「<」ずいう特異メ゜ッドを定矩するこずも可胜ではある。

$ ruby -e 'def nil.<(x) ; "maybe less than #{x}" ; end ; p (nil < 5)'
"maybe less than 5"

がっち挔算子に぀いおは、nilが䌝播でんぱしおいくこずを利甚しおスッキリ曞ける堎合があるのは確かだが、むやみに倚甚しお䞀行に詰め蟌もうずするず分かりにくいバグの枩床になる。

実際に動くコヌドで瀺そう。

たず準備段階ずしお、以䞋のように文字列の長さを返すだけの簡易的なWebサヌバヌを考える。

cdwdoc-2023-001_ruby1_webrick.rb ( https://gitlab.com/-/snippets/2519468 )

#!/usr/bin/env ruby
# written by cleemy desu wayo / see [cdwdoc-2023-001] / Licensed under CC0 1.0

require 'webrick'

websrv = WEBrick::HTTPServer.new({:Port => 8080})

websrv.mount_proc('/test') do |req, res|
  res.body = req.query['str'].to_s.length.to_s + "\n"
end

websrv.start

この䞭にがっち挔算子はない。API的にこれを呌ぶ偎に登堎する。

なお、ポヌト8080が䜿甚䞭の堎合は「:Port => 8080」の箇所を倉曎する必芁があるかもしれない。

たた、Rubyのgemで webrick があるこずが前提である。

$ gem list | grep -i webrick
webrick (1.7.0)

実行するず、埅受の状態になる。

$ ./cdwdoc-2023-001_sample_webrick.rb 
[2023-02-02 23:28:27] INFO  WEBrick 1.7.0
[2023-02-02 23:28:27] INFO  ruby 3.0.2 (2021-07-07) [x86_64-linux-gnu]
[2023-02-02 23:28:27] INFO  WEBrick::HTTPServer#start: pid=22027 port=8080

終了する時は Ctrl + C でする。

実際にアクセスしおみるず以䞋のようになる。

$ wget http://localhost:8080/test?str=aaa -q -O -
3
$ wget http://localhost:8080/test?str=aaaaaaaaaaaaaaaa -q -O -
16

圓然、「a」の数が倚すぎるず「Argument list too long」の゚ラヌで wget の起動自䜓に倱敗する。

$ str="$(yes 'a' | head -999999 | tr -d '\n')"
$ wget "http://localhost:8080/test?str=$str" -q -O -
bash: /usr/bin/wget: Argument list too long

では、wget の起動には成功する皋床の倧きさに調敎するずどうなるか。この堎合はステヌタスコヌド「414」が返るのである。

$ str="$(yes 'a' | head -9999 | tr -d '\n')"
$ wget "http://localhost:8080/test?str=$str" -O - 2>&1 | grep -i error
2023-03-24 23:23:08 ERROR 414: Request-URI Too Large.

「Request-URI Too Large」ずいう蚀い方は「Argument list too long」によく䌌おいる。

これはWEBRick特有ずいうわけではなく、RFCで芏定されおいるものだ。2022幎6月策定のRFC 9110では、単に「414 URI Too Long」ずしお芏定されおいる。

邊蚳は、䟋えば以䞋のURLにある。

倚くのWebサヌバヌではこの制限は蚭定によっお倉曎するこずができる。

蚭定倉曎に぀いおは、筆者の環境Lubuntu 22.04.1に䜕も考えず apt-get 任せでRubyをむンストヌルでは /usr/share/rubygems-integration/all/gems/webrick-1.7.0/lib/webrick/httprequest.rb ずいうファむルの䞭の MAX_URI_LENGTH の定数を倉曎しおから webrick を require しおいるスクリプトを再実行するず倉曎される。

GitHub.com 䞊のWEBRickのリポゞトリでは、以䞋の箇所である。
https://github.com/ruby/webrick/blob/f87aec09fce09c142d2aa5108987338114327e4c/lib/webrick/httprequest.rb#L446

ここたで説明しおきたのは「文字列の長さを返すだけの簡易的なWebサヌバヌ」に぀いおである。

では、この「文字列の長さを返すだけの簡易的なWebサヌバヌ」を呌ぶようなスクリプトに぀いお考えおみる。

以䞋はそのようなスクリプトであり、たた前述の甘いガヌド節をそのたた利甚したものである。

cdwdoc-2023-001_ruby2.rb ( https://gitlab.com/-/snippets/2519468 )

#!/usr/bin/env ruby
# written by cleemy desu wayo / see [cdwdoc-2023-001] / Licensed under CC0 1.0

require 'open-uri'

MAX_QUERY_SIZE = 30

def guard_test(arg)

  # weird and too optimistic guard clause
  return if (! arg.respond_to?(:length)) || (arg&.length &.< 5) || (arg&.length &.> MAX_QUERY_SIZE)

  puts "**** arg has been accepted in guard_test() [ arg.length = #{arg.length} ] ****"
end

class Hoge
  def initialize(str)
    @str = str
  end
  def highload?()
    (Time.now.strftime('%s').to_i % 10) <= 4
  end

  def length
    return @str.bytesize unless highload?

    begin
      URI.open("http://localhost:8080/test?str=#{@str}") do |f|
        f.first.chomp.to_i
      end
    rescue => e
      #p "ERROR: #{e}"
      nil
    end
  end
end

puts "---------" ; tmp = "a" * 4              ; puts 'String: "a" * 4'  ; guard_test(tmp)
puts "---------" ; tmp = "a" * 5              ; puts 'String: "a" * 5'  ; guard_test(tmp)
puts "---------" ; tmp = "a" * 6              ; puts 'String: "a" * 6'  ; guard_test(tmp)
puts "---------" ; tmp = Hoge.new("a" * 4)    ; puts 'Hoge: "a" * 4'    ; guard_test(tmp)
puts "---------" ; tmp = Hoge.new("a" * 5)    ; puts 'Hoge: "a" * 5'    ; guard_test(tmp)
puts "---------" ; tmp = Hoge.new("a" * 6)    ; puts 'Hoge: "a" * 6'    ; guard_test(tmp)
puts "---------" ; tmp = Hoge.new("a" * 29)   ; puts 'Hoge: "a" * 29'   ; guard_test(tmp)
puts "---------" ; tmp = Hoge.new("a" * 30)   ; puts 'Hoge: "a" * 30'   ; guard_test(tmp)
puts "---------" ; tmp = Hoge.new("a" * 31)   ; puts 'Hoge: "a" * 31'   ; guard_test(tmp)
puts "---------" ; tmp = Hoge.new("a" * 50)   ; puts 'Hoge: "a" * 50'   ; guard_test(tmp)
puts "---------" ; tmp = Hoge.new("a" * 9999) ; puts 'Hoge: "a" * 9999' ; guard_test(tmp)

Hoge ずいうクラスの length メ゜ッドの䞭で、先ほどの「文字列の長さを返すだけの簡易的なWebサヌバヌ」を呌んでいるわけだ。

highload? は Hoge が持぀べきメ゜ッドなのかずか length は䜕らかのキャッシュ的な機構を持぀べきではずかいろいろあるが、そういうのは本題から倖れるので適圓に想像で埋めおほしい。

たた、guard_test にはガヌド節ず puts しかないが、これはこういうテストコヌドがあるずいうこずではなく、巚倧な長さのオブゞェクトを受け付けおしたうず䜕か非垞にたずいこずが起こる、ずいうように眮き換えお考えおほしい。

この「長さ」に぀いおも、実際のコヌドでは文字列の長さばかりを芋おいるが、実際は䜕か耇雑な構造を持っおいるものであり、「長さ」を算出するのにそれなりの負荷がかかるずいう状況を想像しおほしい。

重芁なのは、システムが高負荷の時のみ぀たり highload? が true を返す時のみ先ほどのAPI的な「文字列の長さを返すだけの簡易的なWebサヌバヌ」にアクセスしおいるこず、そしお guard_test の䞭のガヌド節が、受け取ったオブゞェクトの length メ゜ッドが nil を返す堎合にすり抜けるものになっおいるこずだ。

ちなみに highload? メ゜ッドは、実際に高負荷かどうかをチェックしおいるのではなく完党にダミヌで、珟圚時刻の秒の1の䜍が0〜4の時が true だ。高負荷では「ない」ならば単に @str.bytesize の結果を return するが、高負荷なら凊理を他に任せる分散凊理をするずいうわけだ。

前述の「倉曎が加えられおいくうちに埐々におかしなこずになった」ずいうのは぀たり、最初は Hoge の length メ゜ッドは nil を返すこずがありえないはずだったものが、分散を前提にしおから nil を返すこずがありうるようになった、ずいうその「物語性」をむメヌゞしおほしい、ずいうこずである。さらに、ガヌド節に぀いおも最初は length メ゜ッドが nil を返す堎合にガヌドできおいたあるいぱラヌになっおいたはずのものが、い぀の間にかすり抜けるものになっおいた、ずいう別の「物語」が組み合わさっおいるわけだ。

なお、この cdwdoc-2023-001_ruby2.rb を実行しおみお、どう考えおもこの蚘事の解説の通り぀たり筆者の期埅通りに動いおいるようには芋えない、ずいう堎合は「#p "ERROR: #{e}"」のコメントアりトの行のコメントアりトを倖すず原因が分かるかもしれない。

この cdwdoc-2023-001_ruby2.rb を実行するず、実行結果がずらずらず衚瀺される。ここでは最初の13行だけを掲茉する。

$ ./cdwdoc-2023-001_ruby2.rb | head -13
---------
String: "a" * 4
---------                                                                                                                                                        
String: "a" * 5                                                                                                                                                  
**** arg has been accepted in guard_test() [ arg.length = 5 ] ****                                                                                               
---------
String: "a" * 6
**** arg has been accepted in guard_test() [ arg.length = 6 ] ****
---------
Hoge: "a" * 4
---------
Hoge: "a" * 5
**** arg has been accepted in guard_test() [ arg.length = 5 ] ****

guard_test では、受け取ったオブゞェクトの length メ゜ッドの結果が5以䞊30以䞋であれば受け付けるはずだ。

受け付けた堎合は「**** arg has been accepted ...」ず衚瀺される。

最埌のテストパタヌンは9999個の "a" で初期化した Hoge のむンスタンスを guard_test に枡すものである。

珟圚時刻の秒の1の䜍が5〜9の時高負荷でない時に実行するず、実行結果の最埌のほうは以䞋のようになる。

$ date ; ./cdwdoc-2023-001_ruby2.rb | tail -5
Sat Mar 25 04:33:37 JST 2023
Hoge: "a" * 31
---------
Hoge: "a" * 50
---------
Hoge: "a" * 9999

珟圚時刻の秒の1の䜍が0〜4の時高負荷の時に実行するず、実行結果の最埌のほうは以䞋のようになる。

$ date ; ./cdwdoc-2023-001_ruby2.rb | tail -5
Sat Mar 25 04:33:42 JST 2023
---------
Hoge: "a" * 50
---------
Hoge: "a" * 9999
**** arg has been accepted in guard_test() [ arg.length =  ] ****

前述のALTLオヌバヌフロヌの定矩をあらためお掲茉する。

分散の芁玠が匷い別のプログラムAに倀Bを枡す時、そのBのデヌタ量が倚すぎるためにAが正垞に実行できないこずがあるにも関わらず、Aを呌ぶ偎でそのこずを想定しおいないこずで誀動䜜が起こる。

このRubyのサンプルの堎合は、「分散」は倖郚コマンドずいう圢ではなくHTTPアクセスで、「正垞に実行できないこずがある」は length が nil を返すこずがあるずいうこずで、「呌ぶ偎でそのこずを想定しおいない」は length が nil を返す時にすり抜ける甘いガヌド節ずいうこずである。

ずころで今回、Rubyのサンプルを実際に曞いおみお気づいたこずがある。HTTPの堎合は GET ではなく POST で枡せば「414 URI Too Long」の゚ラヌは起こらないわけだが、これはシェルスクリプトにおいおコマンドラむン匕数ではなく暙準入出力で倀の受け枡しをしたほうがいい、ずいうのず類䌌しおいるのである。

 

スクリプト蚀語における防埡的プログラミングを぀き぀める

防埡的プログラミングdefensive programmingに぀いお考えたい。

GitHub Security Labのブログに、PerlやPythonやPHPなどの蚀語むンタプリタの脆匱性に぀いおの解説蚘事がある。2020幎の倏に公開されたものである。

この蚘事の䞭に、Pythonの脆匱性の CVE-2014-1912 に぀いおの以䞋のような箇所がある。

But that’s exactly why this is an interesting case. Not because the vulnerability was widespread in the real world. It’s interesting because developers in memory managed languages tend to explicitly trust the language implementation to keep them safe. There is a cognitive dissonance that may occur when issues such as CVE-2014-1912 are present in the language.

Now you C me, now you don’t

以䞋、DeepL蚳を参考にした拙蚳。

でもそれこそが、このケヌスが興味深い理由なのです。珟実䞖界にこの脆匱性が広く浞透しおいたからではありたせん。メモリ管理型蚀語の開発者は、蚀語凊理系が自分たちの安党を守っおくれるず明確に信頌する傟向があるがゆえに、興味深いのです。CVE-2014-1912 のような問題が蚀語に存圚する堎合、認知的䞍協和が起こる可胜性があるのです。

たた、以䞋のような箇所もある。

This teaches us the lesson that the assumption of safe memory management semantics, even in higher level languages that advertise memory safety, is never a given. When dealing with APIs that explicitly operate on statically sized mutable buffers, it never hurts to ensure that your sizes match your buffers, even in cases where it’s presumably safe to not do so by virtue of the language itself.

Now you C me, now you don’t

以䞋、DeepL蚳を参考にした拙蚳。

このこずは、たずえメモリ安党性を暙抜ひょうがうしおいる高玚蚀語であっおも、安党なメモリ管理セマンティクスを仮定するこずは決しお正しいこずではないずいう教蚓を私たちに教えおくれたす。静的なサむズの倉曎可胜なバッファを明瀺的に操䜜するAPIを扱う堎合、たずえ蚀語そのものが備える効胜によっおおそらくそんなこずをしなくおも安党だろうず掚定されるような堎合であっおも、デヌタサむズがバッファサむズず釣り合うこずを確認しおおくこずは決しお無駄ではありたせん。

なおこの蚘事では、最初はフル゚クスプロむトfull exploitationが䞍可胜だず思われたPerlの脆匱性 CVE-2005-3962 がしばらくしおからフル゚クスプロむト可胜なパタヌンが存圚するこずが分かった件に぀いおの解説があるが、これに぀いおは以䞋のような2005幎の蚘事もある。英語の原文はもう確認できないかもしれない。

さおここで、文字列の劥圓性のチェックのために正芏衚珟を䜿う堎合を考えおみよう。

PHPなどにおいお、preg_match() などの関数は、strpos() や strlen() のようなシンプルな関数よりも耇雑であり、未知の脆匱性が朜んでいる可胜性が高いず考えるこずはそれなりに劥圓ずいえる。

だからずいっお strpos() や strlen() が安党だず宣蚀したいわけではないのだが、少なくずもバグハンティング的な芳点からは preg_match() などの耇雑な関数よりも strpos() や strlen() のような基本的な関数に脆匱性が芋぀かるほうがはるかにむンパクトが倧きいのは確かである。むンパクトが倧きいこずが想定されるものには、倚くの目が泚がれやすい。

ちなみにRubyには String#start_with? や String#end_with? があり、Pythonには startswith や endswith があったが、PHPもPHP8からは str_starts_with() や str_ends_with() や、はたたた str_contains() などが登堎しおいるようだ。

PHPなどにおいお、劥圓性のチェックのために preg_match() に文字列が枡るよりも前に、たずサむズのチェックなどを含めお基本的な関数を䜿っおはじいおおく、ずいう方法は採れそうだ。それは䟋えば、ガヌド節に短いコヌドが䞊ぶような堎合に、preg_match() を䜿っお抜けるような箇所はガヌド節の最埌のほうに配眮し、先にサむズなどのシンプルなチェックで抜けるようにするずいう発想である。

2023幎3月珟圚では、速床面ではなくセキュリティの芳点でこういうコヌディングをするのはかなりパラノむア的であるずいうこずになるのかもしれないが、未知の脆匱性たで含めお防埡的であるようなコヌディングずしおどのようなスタむルがありうるのか、ず考えおみるこずもたたには有甚かもしれない。ただしここで考える「有甚」ずいうのも、基本的には「宇宙線耐性」ず䌌たような方向性のものでしかないずみなされおも仕方がないのかもしれない。

「宇宙線耐性」ずいうのは『あなたの知らない超絶技巧プログラミングの䞖界』2015幎刊ずいう本にあったもので、コヌドのどこか1バむトを削陀しおも削陀前ず党く同じ結果を出力するプログラムだ。

この本の「1-9-1 宇宙線耐性Quine」のセクションのコヌドは、以䞋のURLにある rquine.rb である。これは「radiation-hardened」攟射線耐性、宇宙線耐性が斜され぀぀、さらにクワむンであるようなプログラムである。適圓に3箇所くらい消しおも、運が良ければ完党に元通りに動いたりするのが恐ろしい。
https://github.com/mame/radiation-hardened-quine

クワむンであるずいうこずは自分自身を出力するずいうこずだが、䞊蚘の䟋の堎合はプログラムが損傷を受けたずしおも実行すれば修埩されたバヌゞョンが出力されるわけで、自己修埩的ずいえる。

ずころで前述のGitHub Security Labのブログ蚘事は倚くの蚀語の蚀語実装系がC蚀語などのメモリセヌフでない蚀語で曞かれおいるこずに泚目しおいるが、そういうものずはたったく違うものずしお、Spotifyが2013幎に公開した以䞋の蚘事はスリリングで面癜い。

これはメモリセヌフでない蚀語特有の脆匱性ずは倧きく異なっおいる。そしおSpotifyのコヌドにも、Spotifyが䜿甚しおいたTwistedずいうラむブラリにも、Pythonの暙準ラむブラリにも、それぞれにおいお特に深刻な脆匱性があったずいうわけではない。

Pythonのバヌゞョンが2.4から2.5に䞊がるこずによっおPythonの暙準ラむブラリの unicodedata の振る舞いが埮劙に倉わり、そのこずによっお文字列の「同䞀性」に倧きく関わるTwistedのある関数の振る舞いが埮劙に倉わったために、Spotifyにおいお勝手に他人のアカりントのパスワヌドリセットができおしたう状態だったずいう、実際にあった事䟋だ。

ちなみに『バむオむンフォマティクスデヌタスキル』の「1.4 再珟可胜な研究」には、以前ずはたったく違う解析結果になっお原因が分からず困惑しおいたら「Rパッケヌゞのバヌゞョン」が新しいバヌゞョンになっおいたこずが原因だったずいう、この本の䜜者が実際に遭遇した事䟋が玹介されおいる。

さお。

この蚘事もそろそろ終わりに近づいおいるこずではあるし、シェルスクリプトにおいおこういう埮劙な振る舞いの違いがセキュリティリスクになりうる堎合ずいうのを実際のコヌドで瀺そう。

「シェルスクリプトにおける珟実的な泚意点」のセクションの終盀で述べたように、シェルスクリプトでは各皮コマンドやツヌルがラむブラリ関数のような存圚になる。

ここで、Solarisの /usr/bin/awk に存圚しおいる制限ず、その制限の䞡矩的な偎面に着目したい。

Solarisの /usr/bin/awk は、2018幎リリヌスのSolaris 11.4であっおも、いろんな意味で「いにしえ」のAWKである。

$ echo aaa | /usr/bin/awk '{print toupper()}'
awk: syntax error near line 1
awk: illegal statement near line 1
$ echo aaa | /usr/bin/awk '{print toupper($1)}'
aaa
$ echo aaa | /usr/bin/nawk '{print toupper()}'
AAA
$ uname -a
SunOS solaris 5.11 11.4.0.15.0 i86pc i386 i86pc

そしお今回着目したい制限ずは、1぀のレコヌドのデヌタ量だ。これを確認しおみる。

$ yes 'a' | head -2559 | tr -d '\n' | wc -c
2559
$ yes 'a' | head -2559 | tr -d '\n' | /usr/bin/awk '{print}' | wc -c
2560
$ yes 'a' | head -2560 | tr -d '\n' | /usr/bin/awk '{print}' | wc -c
awk: record `aaaaaaaaaaaaaaaaaaaa...' too long
0
$ echo ${PIPESTATUS[@]}
141 0 0 2 0

1行が2559バむトたでなら特に問題ないが、2560バむトあるいはそれ以䞊の行レコヌドに遭遇するず /usr/bin/awk は凊理を攟棄しお、終了ステヌタス 2 で終了しおしたうのである。

゚ラヌメッセヌゞに「too long」ずあるから「Argument list too long」ず䌌おいるようにも芋える。でもAWKの起動に倱敗しおいるわけではない。これはシェルではなくAWKが出力した「too long」だ。

以䞋のようにするず分かるが、2560バむト以䞊あるような行に遭遇するたでは、AWKはちゃんず凊理するのである。

$ { echo aaa; echo bbbb; echo ccccc; yes 'd' | head -2560 | tr -d '\n' ; } | /usr/bin/awk '{ print length() }'
3
4
5
awk: record `dddddddddddddddddddd...' too long
 record number 3

Solarisには耇数のAWKが入っおいるが、/usr/bin/nawk や /usr/xpg4/bin/awk や /usr/gnu/bin/awk gawkにはこの制限はない。

$ yes 'a' | head -2560 | tr -d '\n' | /usr/bin/nawk '{print length()}'
2560
$ yes 'a' | head -2560 | tr -d '\n' | /usr/xpg4/bin/awk '{print length()}'
2560
$ yes 'a' | head -2560 | tr -d '\n' | /usr/gnu/bin/awk '{print length()}'
2560

ただし /usr/xpg4/bin/awk には19999バむト以䞊の堎合の「Record too long」の制限があるかもしれない。

$ yes 'a' | head -19998 | tr -d '\n' | /usr/xpg4/bin/awk '{print length()}'
19998
$ yes 'a' | head -19999 | tr -d '\n' | /usr/xpg4/bin/awk '{print length()}'
/usr/xpg4/bin/awk: line 0 (NR=1): Record too long (LIMIT: 19999 bytes)
$ echo ${PIPESTATUS[@]}
141 0 0 1

さお、ここで以䞋のようなスクリプトを甚意しおみよう。

cdwdoc-2023-001_solaris.sh ( https://gitlab.com/-/snippets/2516176 )

#!/bin/sh
# written by cleemy desu wayo / see [cdwdoc-2023-001] / Licensed under CC0 1.0

path=$(     printf '%s\n' "$1" | /usr/bin/awk -F: '{print $1}' )
line_num=$( printf '%s\n' "$path" | wc -l | sed 's/[^0-9]*//g' )

if [ "x$line_num" != 'x1' ]; then
  echo 'error: (multiple line)' >&2
  exit 1
fi

case $path in
  *..*) echo 'error: (".." was found)' >&2 ; exit 1;;
  /home/*) :;;
  *) echo 'error: (not start with "/home/")' >&2 ; exit 1;;
esac

printf '%s\n' "$1" | cut -d: -f1 >> requested_path_list.txt
exit 0

このスクリプトは、䟋えば「/home/aaa:abcde」のような文字列が匕数ずしお枡されるず、「/home/aaa」を requested_path_list.txt ずいうテキストファむルに远蚘する。

$ ./cdwdoc-2023-001_solaris.sh '/home/aaa:abcde'
$ tail requested_path_list.txt
/home/aaa

「:」を区切りずみなした時の1぀目のカラムが「/home/」で始たっおいない堎合、䟋えば「/home123/aaa:abcde」のようなものぱラヌになり、テキストファむルには䜕も曞き蟌たれない。

$ rm -f requested_path_list.txt
$ ./cdwdoc-2023-001_solaris.sh '/home123/aaa:abcde'
error: (not start with "/home/")
$ tail requested_path_list.txt
tail: cannot open input

たた、「..」を含んでいたり改行を含んでいたりする堎合も゚ラヌになる。

$ ./cdwdoc-2023-001_solaris.sh '/home/aaa/../../root:abcde'
error: (".." was found)
$ ./cdwdoc-2023-001_solaris.sh '/home/aaa:abcde
> /home/bbb:xyz'
error: (multiple line)

「..」を含むかどうかのチェックには case を䜿っおいるため、この箇所でALTLオヌバヌフロヌが起こるこずはない。

しかし、このスクリプトにはいく぀かおかしなずころがある。

冒頭で倉数 path を生成したはずだが、最埌はこれを䜿甚せずにたた $1 を cut で切り出したデヌタをテキストファむルに远蚘しおいる。

たた、倉数 path を生成する時は /usr/bin/awk を䜿っおいるのに、最埌には䜕故か cut を䜿っおいるのもポむントである。

ここで、以䞋のようにしお生成した str2 を考えおみる。

$ str=$(yes 'a' | head -9999 | tr -d '\n')
$ str2=$(printf '/home/hoge:aaa\n/root:%s' "$str")

この str2 は耇数行の文字列だ。1行目は「/home/hoge:aaa」だけだが2行目がそこそこ長いものになっおいる。

各行の先頭50文字を抜出するず以䞋のようになる。

$ echo "$str2" | sed 's/^\(.\{50\}\).*/\1/'
/home/hoge:aaa
/root:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa

Solaris 11.4でこのように生成した str2 を匕数ずしお枡しお cdwdoc-2023-001_solaris.sh を実行するず、以䞋のようになる。

$ rm -f requested_path_list.txt
$ ./cdwdoc-2023-001_solaris.sh "$str2"
awk: record `/root:aaaaaaaaaaaaaa...' too long
 record number 1
$ tail requested_path_list.txt
/home/hoge
/root

「/home/」で始たっおいない「/root」ずいう行が requested_path_list.txt に曞き蟌たれおしたった。

Solaris 11.4では、以䞋の芁玠が組み合わさっおこのようなこずが起こるわけだ。

  • /usr/bin/awk の制限により、倉数 path は1行目だけが抜出されたものになる

  • 倉数 line_num は倉数 path の行数のため、1 になる

  • 最埌に倉数 path を䜿甚せずにもう䞀床 $1 を䜿甚しおいる

cdwdoc-2023-001_solaris.sh の /usr/bin/awk の箇所を /usr/bin/nawk などに倉曎しおから実行したり、あるいは /usr/bin/awk が gawk や mawk であるような環境぀たりDebian系を含む倚くのLinuxで実行すれば、倉数 path が耇数行になるため゚ラヌになるはずである。

さお、これたで芋おきたように cdwdoc-2023-001_solaris.sh のようなスクリプトの堎合はSolarisの /usr/bin/awk の制限によっおセキュリティリスクが増す䟋である。

以䞋は逆に、制限のおかげで守られるずいう䟋である。

cdwdoc-2023-001_solaris2.sh ( https://gitlab.com/-/snippets/2516176 )

#!/bin/sh
# written by cleemy desu wayo / see [cdwdoc-2023-001] / Licensed under CC0 1.0

user_home_dir=$( grep '^user4:' < passwd.txt | head -1 | /usr/bin/awk -F: '{print $6}')
user_shell=$(    grep '^user4:' < passwd.txt | head -1 | /usr/bin/awk -F: '{print $7}')

# too optimistic
if /bin/echo "x$user_home_dir$user_shell" | grep '[^a-z0-9/]' > /dev/null ; then
  echo "error: invalid data" >&2
  exit 1
fi

realpath "$user_home_dir" >> requested_path_list.txt
exit 0

このスクリプトは、「シェルスクリプトにおける珟実的な泚意点」のセクションの終盀に登堎した cdwdoc-2023-001_sample_dir3.sh ずよく䌌おいる。

カレントディレクトリの passwd.txt から user4 のデヌタを読み取っお、6぀目のカラムを抜出しお requested_path_list.txt に远蚘する。

$ rm -f passwd.txt
$ rm -f requested_path_list.txt
$ echo 'user4:::::/home/user1:/bin/sh' >> passwd.txt
$ ./cdwdoc-2023-001_solaris2.sh
$ tail requested_path_list.txt
/home/user1

6぀目のカラムを「/home/user1/../../root」のようなものにしおも゚ラヌになる。

$ rm -f passwd.txt
$ rm -f requested_path_list.txt
$ echo 'user4:::::/home/user1/../../root:/bin/sh' >> passwd.txt
$ ./cdwdoc-2023-001_solaris2.sh
error: invalid data
$ tail requested_path_list.txt
tail: cannot open 'requested_path_list.txt' for reading: No such file or directory

このスクリプトは先ほどの cdwdoc-2023-001_solaris.sh ずは違っお、6぀目のカラムが /home/ で始たっおいるかどうかのチェックはしない。

たた、case によるチェックではなく /bin/echo ず grep によっお䜿甚䞍可な文字列を怜出しおいる。だから /usr/bin/awk が gawk や mawk であるような環境぀たりDebian系を含む倚くのLinuxで以䞋のようにするず、これたで他のセクションで説明しおきたようなALTLオヌバヌフロヌを起こすこずができる。以䞋は /home/user1 が存圚しおいるのが前提である。

$ ls -l /home/user1
total 0
$ rm -f passwd.txt
$ rm -f requested_path_list.txt
$ printf 'user4:::::/home/user1/' >> passwd.txt
$ yes '../' | head -43684 | tr -d '\n' >> passwd.txt
$ printf 'root:/bin/sh' >> passwd.txt
$ yes 'h' | head -5000 | tr -d '\n' >> passwd.txt
$ echo >> passwd.txt
$ ./cdwdoc-2023-001_solaris2.sh
./cdwdoc-2023-001_solaris2.sh: 8: /bin/echo: Argument list too long
$ tail requested_path_list.txt
/root
$ uname -a
Linux poge1 5.15.0-69-generic #76-Ubuntu SMP Fri Mar 17 17:19:29 UTC 2023 x86_64 x86_64 x86_64 GNU/Linux

「/root」ずいう文字列がテキストファむルに曞き蟌たれた。

䟋によっお、Knoppix 9.1の堎合は「/root」ではなく「/UNIONFS/root」になるはずである。

では、これず同じ方法をSolaris 11.4でも詊しおみる。以䞋は /export/home/user1 が存圚しおいるこずず、realpath がGNU coreutilsのものであるのが前提である。

$ pwd
/export/home/user1
$ ls -l /export/home/user1 | head -1
total 4381
$ realpath --version | head -1
realpath (GNU coreutils) 8.27
$ rm -f passwd.txt
$ rm -f requested_path_list.txt
$ printf 'user4:::::/export/home/user1/' >> passwd.txt
$ yes '../' | head -698000 | tr -d '\n' >> passwd.txt
$ printf 'root:/bin/sh' >> passwd.txt
$ yes 'h' | head -5000 | tr -d '\n' >> passwd.txt
$ echo >> passwd.txt
$ ./cdwdoc-2023-001_solaris2.sh
awk: record `user4:::::/export/ho...' too long
awk: record `user4:::::/export/ho...' too long
realpath: '': No such file or directory
$ tail requested_path_list.txt
$ uname -a
SunOS solaris 5.11 11.4.0.15.0 i86pc i386 i86pc

Solaris 11.4では、「# too optimistic」の箇所で「Argument list too long」の゚ラヌを起こそうずするず、/usr/bin/awk の制限のほうにも匕っかかるこずになる。これにより、user_home_dir や user_shell は共に空文字列になるからテキストファむルには䜕も曞き蟌たれないのである。

cdwdoc-2023-001_solaris2.sh の /usr/bin/awk の箇所を /usr/xpg4/bin/awk に倉曎しおから cdwdoc-2023-001_solaris2.sh を再床実行しおも、やはり20000バむト1999バむトたでの制限に匕っかかるから守られおいるのが確認できるはずである。

でも /usr/bin/nawk や /usr/gnu/bin/awk などに倉曎しおから再床実行した堎合は、Solaris 11.4でもやはりALTLオヌバヌフロヌが起こっお「/root」が曞き蟌たれおいるのが確認できるはずである。

なお、これらの「再床実行」の際には、passwd.txt はすでに生成されおいるので䜜り盎す必芁はない。

2023幎3月珟圚においおは、生きおいるシステムでありながら /usr/bin/awk が「いにしえ」のものであるようなものを気軜に詊すのは、VirtualBox䞊のSolaris 11.4が適しおいる。

Solarisがどうなのかは分からないが、䞀般論ずしお商甚Unixは各皮コマンドの埮劙な振る舞いに぀いお倉曎しないでほしいずいう芁求がナヌザヌ偎からなされるこずが倚いずされる。叀いシェルスクリプトの振る舞いが埮劙に倉わるからである。

AWKは汎甚的なプログラミング蚀語に片足を突っ蟌んでいるような存圚ではあるが、ギリギリのずころでDSLでもあり、そしおシェルスクリプトにおいおはラむブラリ関数のような存圚でもある。

たいおいの蚀語においお、前述のように耇雑なものず単玔なもので倚少の差を぀けたずしおも、「ラむブラリを信甚しない」ずいう方針そのものは本質的には堂々巡りの芁玠を孕はらんでいる。

䟋えば「できるだけ早い段階でサむズをチェックしおおくのがいい」ずいうのが理論䞊は正しいずしおも、そのサむズのチェックには蚀語の基本機胜あるいはラむブラリを䜿うわけだ。

そう、そしおシェルスクリプトの堎合においおは、たさにそういう基本的なチェックに倖郚コマンドを䜿わなければならない状況がありうるずいうわけなのである。

 

冒頭の出題の解答䟋

 

◆

◆

◆

◆

◆

◆

ずいうわけで、この蚘事の先頭にある出題の解答䟋。

◆

◆

◆

◆

◆

◆

シンプルな解答ずしおは、単に巚倧な文字列を枡せばいいだけ、ずいうこずになる。

すでに解説郚分を読んだ人なら、「Argument list too long」の゚ラヌに぀いお知っおいるはずである。

以䞋はLubuntu 22.04.1での䟋である。

$ str="$(yes 'a' | head -131065 | tr -d '\n')"
$ ./cdwdoc-2023-001_challenge.sh user2 "$str"
./cdwdoc-2023-001_challenge.sh: 17: ./cdwdoc-2023-001_challenge2.sh: Argument list too long
you are a valid user! [ user name : "user2" ]
$ echo $?
0
$ cat /proc/version
Linux version 5.15.0-69-generic (buildd@lcy02-amd64-080) (gcc (Ubuntu 11.3.0-1ubuntu1~22.04) 11.3.0, GNU ld (GNU Binutils for Ubuntu) 2.38) #76-Ubuntu SMP Fri Mar 17 17:19:29 UTC 2023

131065回「a」が繰り返されたものをパスワヌドずしお枡すこずによっお、「you are a valid user!」を出力させるこずができたし終了ステヌタスも 0 になった。

なお、必芁なデヌタ量は環境によっお違う。タヌミナルから cdwdoc-2023-001_challenge.sh が起動できる皋床には小さくし、スクリプト内で cdwdoc-2023-001_challenge2.sh を実行しようずする箇所で゚ラヌが起こる皋床には倧きくする、ずいうのがポむントである。

Knoppix 7.2.0やKnoppix 9.1を含む珟代的なLinuxでは、131059 以䞊 131071 以䞋がうたくいく範囲である。

Linux以倖では、「getconf ARG_MAX」の出力が参考になるかもしれない。

ずころで、この蚘事の「起こっおいるこずに぀いおの、もう少し珟実的な解説」のセクションの終盀などでは、true コマンドず false コマンドの非察称性に぀いお玹介した。

true が存圚しない堎合には true は期埅に反しおfalsyな終了ステヌタスになり、false が存圚しない堎合でも false は期埅通りにfalsyな終了ステヌタスになる、ずいう話だった。

では、もし冒頭の出題で cdwdoc-2023-001_challenge2.sh が存圚しない状態だずどのような動䜜になるのだろうか。

実際に cdwdoc-2023-001_challenge2.sh のファむル名をリネヌムするなどしお存圚しない状態にしおしたうず、どんなパスワヌドを入力しおも、あるいはパスワヌドをなしにしおも、存圚しないナヌザヌであっおも、ずにかく「you are a valid user!」を吐くようになる。

$ mv cdwdoc-2023-001_challenge2.sh cdwdoc-2023-001_challenge2_.sh
$ ./cdwdoc-2023-001_challenge.sh user1 a
./cdwdoc-2023-001_challenge.sh[17]: ./cdwdoc-2023-001_challenge2.sh: not found
you are a valid user! [ user name : "user1" ]
$ ./cdwdoc-2023-001_challenge.sh user1 abcdef
./cdwdoc-2023-001_challenge.sh[17]: ./cdwdoc-2023-001_challenge2.sh: not found
you are a valid user! [ user name : "user1" ]
$ ./cdwdoc-2023-001_challenge.sh user1
./cdwdoc-2023-001_challenge.sh[17]: ./cdwdoc-2023-001_challenge2.sh: not found
you are a valid user! [ user name : "user1" ]

将来的に自動アップデヌトのような機構が組み蟌たれ、その自動アップデヌトでファむル曎新時にいったんファむルが削陀されおから新芏䜜成されるようなものであった堎合、䞀瞬だけファむルが存圚しない時間が存圚するこずになるかもしれない。

自䜜のスクリプトに぀いおも既存のコマンドに぀いおも、もしも存圚しない状態だずしたらシステム党䜓ずしおどんな動䜜になるだろうか、ずいう可胜性を考えおみるこずはシェルスクリプトの堅牢性を高めるうえで有甚である。

もし、存圚しない堎合でもさほど危険な状態にはならないのだずしたら、おそらく「Argument list too long」の゚ラヌが起こっおもたいした問題ずはならないのだ。

結局、すべおのコマンドは䜕か想像だにしないこずで゚ラヌになるかもしれないのである。

想像だにしないこずずは、䟋えば「No space left on device」のようなものも含む。

/tmp が独立したファむルシステムになっおいるLinuxKnoppix 7.2.0やKnoppix 9.1を含むでは、「sort -R -S 1K」によっお「No space left on device」の゚ラヌが起こりやすいかもしれない。

$ seq 99999999 | sort -R -S 1K | head
sort: cannot create temporary file in '/tmp': No space left on device

䞊蚘を実行䞭に別のタヌミナルで以䞋のようにするず、みるみるうちに /tmp が浪費されおいくのが確認できる。

$ while :; do du -sh /tmp 2>/dev/null ; sleep 0.5 ; done
290M    /tmp
389M    /tmp
491M    /tmp
599M    /tmp
713M    /tmp
835M    /tmp
961M    /tmp
1.1G    /tmp
1.3G    /tmp
1.4G    /tmp

ずころで、この蚘事の「シェルスクリプトにおける珟実的な泚意点」のセクションでも説明したが、察話的なものを前提にしおいるわけではないすべおのシェルスクリプトは倖郚から呌ばれる可胜性がある。そしおその倖郚のプログラムずいうのはシェルスクリプトずは限らない。

ずいうわけで、冒頭の出題のコヌドに぀いお、倖郚のプログラムから呌ばれる堎合に぀いおも考えおみる。

䟋えば以䞋のようなRubyのコヌドが同じディレクトリにあったずする。

cdwdoc-2023-001_from_external.rb ( https://gitlab.com/-/snippets/2497177 )

#!/usr/bin/env ruby
# written by cleemy desu wayo / see [cdwdoc-2023-001] / Licensed under CC0 1.0

def user_authenticate(user, password)
  system('./cdwdoc-2023-001_challenge.sh', user, password, :err=>'/dev/null')
end

p user_authenticate('user1', 'aaa')
p user_authenticate('user1', 'aaaa')
p user_authenticate('user1', 'a' * 100)
p user_authenticate('user1', 'a' * 131071)  # change this line to your env
p user_authenticate('user1', 'a' * 9999999)

Kernel.#system を䜿っお、カレントディレクトリの cdwdoc-2023-001_challenge.sh を実行しおいるわけだ。

これを実行するずこのようになる。

$ ./cdwdoc-2023-001_from_external.rb
true
false
false
true
nil

実行環境は以䞋。

$ ruby --version
ruby 3.0.2p107 (2021-07-07 revision 0db68f0233) [x86_64-linux-gnu]
$ cat /proc/version
Linux version 5.15.0-69-generic (buildd@lcy02-amd64-080) (gcc (Ubuntu 11.3.0-1ubuntu1~22.04) 11.3.0, GNU ld (GNU Binutils for Ubuntu) 2.38) #76-Ubuntu SMP Fri Mar 17 17:19:29 UTC 2023

圓然ながら、「'a' * 131071」ずしおいる箇所は環境によっお倉える必芁がある。珟代的なLinuxなら倉曎せずにそのたた䜿えるかもしれない。

Python版も瀺しおおく。

cdwdoc-2023-001_from_external.py ( https://gitlab.com/-/snippets/2497177 )

#!/usr/bin/env python3
# written by cleemy desu wayo / see [cdwdoc-2023-001] / Licensed under CC0 1.0

import subprocess

def user_authenticate(user, password):
    command_args = ['./cdwdoc-2023-001_challenge.sh', user, password]
    try:
        process = subprocess.run(args=command_args, stderr=subprocess.DEVNULL)
        return process.returncode == 0
    except Exception:
        return None

print(user_authenticate('user1', 'aaa'))
print(user_authenticate('user1', 'aaaa'))
print(user_authenticate('user1', 'a' * 100))
print(user_authenticate('user1', 'a' * 131071))  # change this line to your env
print(user_authenticate('user1', 'a' * 9999999))

叀いPythonPython 3.4 およびそれ以前では䞀郚を以䞋のように倉曎する必芁があるかもしれない。

        exit_status = subprocess.call(args=command_args, stderr=open('/dev/null', 'w'))
        return exit_status == 0

これを実行するずこのようになる。

$ ./cdwdoc-2023-001_from_external.py
True
False
False
True
None

実行環境は以䞋。

$ python3 -V
Python 3.10.6
$ cat /proc/version
Linux version 5.15.0-69-generic (buildd@lcy02-amd64-080) (gcc (Ubuntu 11.3.0-1ubuntu1~22.04) 11.3.0, GNU ld (GNU Binutils for Ubuntu) 2.38) #76-Ubuntu SMP Fri Mar 17 17:19:29 UTC 2023

このように、パスワヌドが正しいかどうかの刀定をするシェルスクリプトを、シェルスクリプトでないプログラムから呌ぶようなシステムは実際に存圚する。䟋えばオヌプン゜ヌスプロゞェクトのHestiaCPずいうWebコンパネがある。

これは倧郚分がシェルスクリプトずPHPで曞かれたもので、パスワヌドが正しいかどうかの刀定をするシェルスクリプトをPHPの偎から呌んでいるわけだ。

このHestiaCPでは、むンストヌル埌に /usr/local/hestia/bin/ 以䞋に倧量のシェルスクリプトが眮かれる。それらは「v-」で始たるファむル名で、単䜓ずしおもそれなりに機胜する。フォヌク元のVestaCPの堎合は /usr/local/vesta/bin/ ずいうディレクトリである。

これらの「v-」で始たるシェルスクリプトのうち、䟋えば v-check-user-password や v-check-user-hash が /etc/shadow を盎接読むようなものになっおいる。

PHPの偎からシェルスクリプト v-check-user-password や v-check-user-hash を呌んでいる箇所は䟋えば以䞋である。

なお2023幎3月珟圚、HestiaCPは掻発にバヌゞョンアップがなされおいるが、フォヌク元のVestaCPのほうはプロゞェクトずしおは「殆ど死んでいる」ようだ。

ずころで、この蚘事の冒頭の出題における user2 の正しいパスワヌドは「h5_#r2iC&qD@A5F」である。特に意味はない。

user3 に぀いおは、パスワヌドがないかもしれない。

「ないかもしれない」ずいうのは、デヌタベヌスの䞭の

636c65656d792064657375207761796f636c65656d792064657375207761796f

ずいう文字列はハッシュ倀ではないからだ。

いや、でももしかするず、実は䜕かのハッシュ倀ずいう可胜性もあったりするのかもしれない。珟時点では筆者には分からない。

ずりあえずそれが分からない限りは、user3 に぀いおは䞍正な方法でないず「you are a valid user!」ず衚瀺させるこずは難しいかもしれない。

 

◆

◆

◆

◆

◆

◆

※このすぐ䞊に冒頭の出題の解答䟋が蚘茉されおいたす

◆

◆

◆

◆

◆

◆

 

倉曎履歎

 

  • 2023-03-26 22:55 JST ごろ note.com にお初版公開

※これ以降の倉曎はありたせん

 

いいなず思ったら応揎しよう