shutdown -p +5

もうすぐ10月、値上げの季節。

オイラーに取って痛いのは煙草の値上げ。オイラーの銘柄は一挙に40円も上がる。困ったものだ。

女房は、これを機に縁を切れと迫る。そんな女房への反論。

酒と煙草は神様がくれた適齢まで生きる為の薬

誰かの小説に出てきた言葉だ。これいいね。もう何十年も前に亡くなった、おじいちゃんを思い出す。昔かたぎの職人さん。酒は飲む分だけを近くの酒屋まで買いに行くのがオイラーのおつかいだった。行って来ると、必ず駄賃が貰えた。可愛い?孫に駄賃をあげたくて、おつかいをさせてた節があるな。

そして煙草は、煙管できざみ煙草をやってた。死ぬまでね。ってか、亡くなる寸前に、煙草をしなかったものだから、今日はしないの?って聞いたんだ。そしたら、もういいと。そして、数日後に亡くなった。薬が切れたんで、おさらばしたんだね。誰にも、迷惑かける事なく逝った。 オイラーもこうありたいものだ。ピンコロね。

2週間ぐらい前から、密かに煙草の買い占め運動を実施中。一挙に大量購入すると騒がれますからね。広く薄く購入です。

10個入れのカートンで買うんだけど、店によっては在庫切れだったりして、やる気あるのかね? 商機だのにね。ローソンは、どの店でも在庫豊富、だまっていても、ライターをバックしてくれる。優良店舗であった。

賞味期限が半年という事なんで、6カートンまで備蓄しようかと思ったけど、そこまで頑張らなくてもいいかなと思って、程ほどの所で止めた。

wallは続くよどこまでも

それって、万里の長城の事? それとも、長大な壁を作って後世まで名を残したい、どこかの大統領の事?

いいえ、前回からの続き、wallの解析。第一次調査隊は、荒い調査を完了してるんで、今回は細部を調査します。

まず、makemsgを見る。引数は、wall起動時に与えたメッセージファイル。

183│         snprintf(tmpname, sizeof(tmpname), "%s/wall.XXXXXXXXXX", _PATH_TMP)
184│         if ((fd = mkstemp(tmpname)) >= 0) {
185├───────────────> (void)unlink(tmpname);
186│                 fp = fdopen(fd, "r+");
187│         }

一時ファイルの名前を生成して、作って、消して、再度オープンして、ファイルディスクリプタを得ている。 出来上がったファイルの外観は

ob6$ ls -l /tmp/wall.ksky99AYqo
-rw-------  1 sakae  wheel  0 Sep 20 14:31 /tmp/wall.ksky99AYqo

直ぐに消されてしまうので、悪い人に付け入る隙を与えない。これが、OpenBSD流?

そして、ヘッダーを付けて、指定したファイルからのデータを読み込んで、表示すべきデータを mbufというメモリー上のバッファーに残す。

(gdb) p mbuf
$16 = 0x39797f4e800 "\r", ' ' <repeats 79 times>, "\r\nBroadcast Message from sa
kae@ob6.localdomain", ' ' <repeats 35 times>, "\a\a\r\n        (/dev/ttyp5) at 1
4:40 ...  "...

が、メモリー上のデータ。このデータがttymsgに最終的に引き継がれる。(iovと言う構造体)

次は、utmpの読み出し。最初の方は結構無効になったエントリーがあるので、有効なやつを取り出している。

(gdb) p utmp
$21 = {
  ut_line = "ttyp0\000\000",
  ut_name = "sakae", '\000' <repeats 26 times>,
  ut_host = "xxx.xxx.xxx.xxx", '\000' <repeats 242 times>,
  ut_time = 1537159604
}

これらの有効なデータを集めておく。そして、いよいよttymsgで端末毎に出力するルーチンへと入っていく。

Breakpoint 2, ttymsg (iov=0x7f7ffffe57c0, iovcnt=1, line=0x75b22f6ea70 "ttyp0",tmout=300) at ttymsg.c:67

(gdb) c
Continuing.

Broadcast Message from sakae@ob6.localdomain                                   8
        (/dev/ttyp5) at 16:18 ...

HOGE FUGA

Breakpoint 2, ttymsg (iov=0x7f7ffffe57c0, iovcnt=1, line=0x75b22f6de80 "ttyC0",tmout=300) at ttymsg.c:67

先に流れをみちゃったけど、デバイス事に、呼び出しが行われている。最初は、自分が使っている端末。2回目は、コンソールからloginしてるroot向け。堂々とrootにも物申す事が出来るって、いい事だ。今の日本みたいに、人の顔色を見て黙っていようなんてないからね。

        SLIST_FOREACH(un, &utmphead, next) {
                if ((p = ttymsg(&iov, 1, un->tty, 60*5)) != NULL)
                        warnx("%s", p);
        }

これが、線形リスト(デバイス名による)をforeachで回る部分だ。呼ばれた方で見ると、適切な名前が付いているので、分かり易い。

ttymsgの中では、lineにデバイス名が入っているので、それを元に、正式だデバイス名を組み立ててから、openする。そして、そのデバイスに対して出力って寸法。

113│                 wret = writev(fd, iov, iovcnt);

実際の出力は、writevっている、write系のシステムコールで行われている。

(gdb) p iovcnt
$6 = 1
(gdb) p iov
$8 = {
  iov_base = 0x1c53f7348800,
  iov_len = 489
}

出力するデータの持ち方が、ベースアドレスからlenバイト分って仕掛けになってる。実体はmakemsgg作ったもの。

ob6$ echo '  Give me more money' > /dev/ttyp0  Give me more money
ob6$ echo '  Give me more money' > /dev/ttyC0
ksh: cannot create /dev/ttyC0: Permission denied

wallがやってる事は、上の実験と同じ。但し、コンソール宛てとかだと、パーミションが無いので、 怒られる。そんな場合でも、wallを通せば、よしなに計らってくれるぞ。

shutdown -p +5

何時もマシンを落とす時、shutdown -p now なんだけど、今回はこの記事を書くために、+5を設定してみた。

なぜ shutdownかって? 前回wallに隠しオプションを見つけたからね。

なお、このshutdownコマンドは、opraterグループになってるので、自分もこのグループに参加してると、わざわざroot権限を得る必要は無い。

リナはどうなってるか、確認しておくか。

debian:~$ ls -l /sbin/shutdown
lrwxrwxrwx 1 root root 14 Jun 14 05:20 /sbin/shutdown -> /bin/systemctl*
debian:~$ ls -l /bin/systemctl
-rwxr-xr-x 1 root root 181876 Jun 14 05:20 /bin/systemctl*

オイラーには、この思想、受け入れがたいな。こんなに機能を集中させちゃうから、いつまでたっても安定しないんだろう。unixの思想、シンプルであれを放棄してる。まあ、MSをお手本にしてたら、自然にそうなるわな。昔の人はいい事を言ってたな。

朱に染まれば、赤くなる。

ob6$ shutdown -p +5
  :
*** System shutdown message from sakae@ob6.localdomain
System going down in 30 seconds

*** FINAL System shutdown message from sakae@ob6.localdomain
System going down IMMEDIATELY

System shutdown time has arrived

この間、5分、2分、1分、30秒って具合にせわしなくメッセージが表示されてたよ。そして、-pのおかげで、ぷちっと電源が切れた。

禁断の -n

shutdown コマンドに取りつかれるきっかけとなった、wall -n を試してみるか。

ob6$ echo hoge fuga | wall -n

Broadcast Message from sakae@ob6.localdomain                                   8
        (/dev/ttyp1) at 15:58 ...

hoge fuga

ヘッダーが出てこない触れ込みなんだけど、普通にwallした時の同じ挙動だなあ。 スーパーユーザーの特権が無いと、機能しないのかな。なにせ、隠しオプションって事だけで 舞い上がってしまって、詳細を見ていなかった。

ob6$ doas sh -c "echo hoge fuga | wall -n"
doas (sakae@ob6.localdomain) password:

hoge fuga

やっぱり、特権が必要だったのね。Broadcast からの2行が省略されてる。 もう一度、wallの該当部分を見ておくか。

                case 'n':
                        /* undoc option for shutdown: suppress banner */
                        pw = getpwnam("nobody");
                        if (geteuid() == 0 || (pw && getuid() == pw->pw_uid))
                                nobanner = 1;
                        break;

後ろの方で、!nobanner なら、って断りを入れてメッセージを作ってた。(バナーが付く)

このcaseの小説(小節)の中に、オイラーの知らない単語が出て来た。知らない単語(関数)は、その場で辞書(man)を引くのが、言語力向上の常識。前後の文脈から推測するなんて、オイラーには、まだ無理。大阪なおみちゃんの、プチ怪しい日本語がかわゆいな。前後の文脈はおろか、空気を読むのもこれからか。いや、今のままの方がいいぞ。

emacsを使ってたら、辞書を引くのは簡単。

(define-key global-map (kbd "C-c m") 'man)

を登録しておけば、知らない単語に照準を合わせて、C-c m するだけ。調べ終わったら、q で、辞書が閉じられる。vimはよう知らんけど、どうやるんだろう? vimでマニュアルを引く

getueidって単語を知らなかったぞ。(自慢になるか)

     The geteuid() function returns the effective user ID of the calling
     process.

実効ユーザーがrootかnobodyならば、ヘッダーを省略出来るよと読むんだな。なんだか、読解力のテストを受けているみたいだな。

wallの使われ方

shutdownにはwallが組み込まれているとの事なんで、どんな風に組み込んでいるか、確認してみる。

pathnames.h なんてのが有って、それを先に見てと、野生の勘が働いたぞ。

#define _PATH_FASTBOOT          "/fastboot"
#define _PATH_HALT              "/sbin/halt"
#define _PATH_REBOOT            "/sbin/reboot"
#define _PATH_WALL              "/usr/bin/wall"
#define _PATH_RC                "/etc/rc"

前回だか学習した通りの名前付けになってた。これが分かれば、一発検出出来るな。

shutdown.c/timewarn(time_t timeleft)の中でforkした子供の部分

                /* wall(1)'s undocumented '-n' flag suppresses its banner. */
                execle(_PATH_WALL, _PATH_WALL, "-n", (char *)NULL,
                    restricted_environ);

ここにも、-nは 秘密のやつだからねと、公然の秘密を謳ってる。 execleって、exec族って事までは分かるんだけど、その変化形か。そろそろ、変化形も覚える時期だな。そうしないと、過去、現在、未来が区別出来ない、不思議な英語を喋っちゃう恐れがあります。

あれ? NULLの前で、文字ポインターにキャストしてる。これが正しい使い方か。移植性を高める方策かな? NULLのキャスト とか、5章 ヌルポインター

でも、これとほぼ同じやつは、前回使った。あの時は、execlまでて、最後にeが付いていなかった。今回は付いてる。eは環境じゃなかろうかって想像出来る。じっと目を凝らすと、NULLの後ろに、更に引数 restricted_environ が付いているじゃん。窮屈な環境って読める。

もうマシンを落とすフェーズに入っているので、窮屈結構って事なんだろうな。さて、ここまで想像しといて、答え合わせに辞書を引く。推測当たった。

所で、

static char *restricted_environ[] = {
        "PATH=" _PATH_STDPATH,
        NULL
};

このSTDPATHって、どんなもの?

ob6$ cc -E shutdown.c | grep  PATH
 "PATH=" "/usr/bin:/bin:/usr/sbin:/sbin:/usr/X11R6/bin:/usr/local/bin",

ふーん、ユーザーが追加した(かもしれない)怪しげなPATHを除外してるんだな。それにしても、PATHの最後に、/usr/local/binが入ってる。ここは、一応親分の監視の目が光っていない所。それでも、この場所を含めてくれているって事は、親分のお情けか。まあ、一番優先度が無い場所ってのが、それを物語っている。

gdb call の復習

さて、前回やったgdbから任意の関数を呼べるっていう機能の復習です。この機能、オイラーに取っちゃ、rubyで多用してた、irb を彷彿させるんで、これからC語の実験台として多用してこう。C語はコンパイルっていう手間がかかるとお嘆きのスクリプターには、必見のはずですから。

実験として、上で出て来た、wall -n の小節部分を取り上げる。

さすがにC語はソースをコンパイルする必要が有るので、下記のような叩き台を作って、gdbにかけられるようにしとく。

ob6$ cat base.c
#include <stdio.h>

char *nobody= "nobody";

int main(){
        puts("Like irb!");
        __asm__  __volatile__("int3");  // drop gdb
        return (0);
}

パスワード関係の動きなんで、元データのユーザー名だけを、自力で書いてみた。(別に書かなくてもいい。ただ、宣言の仕方を復習しただけです)関数の外で ポインター宣言してるので、文字列部分はROM状態になってる。

なお、パスワード関係の関数は、一切このソースには表れていない。libcを含んだバイナリーが 欲しいという事だけだ。

ob6$ cc -g base.c
ob6$ gdb -q a.out
Reading symbols from a.out...done.
(gdb) r
Starting program: /tmp/a.out
Like irb!

Program received signal SIGTRAP, Trace/breakpoint trap.
0x00001e13c8f0054c in main () at base.c:7
9               __asm__  __volatile__("int3");  // drop gdb
(gdb)

ここまでで、irbもどきの実験環境が整った。 そんじゃ、小節を辿ってみる。

(gdb) call getpwnam(nobody)
$1 = (struct passwd *) 0x1e16504d2eb0 <_pw_passwd>
(gdb) p *$1
$2 = {pw_name = 0x1e16504d2f00 <_pw_string> "nobody",
  pw_passwd = 0x1e16504d2f07 <_pw_string+7> "*", pw_uid = 32767,
  pw_gid = 32767, pw_change = 0, pw_class = 0x1e16504d2f09 <_pw_string+9> "",
  pw_gecos = 0x1e16504d2f0a <_pw_string+10> "Unprivileged user",
  pw_dir = 0x1e16504d2f1c <_pw_string+28> "/nonexistent",
  pw_shell = 0x1e16504d2f29 <_pw_string+41> "/sbin/nologin", pw_expire = 0}

関数とかユーザー名(の変数名)は、TABキーでちゃんと補完されるよ。得られたデータは、ちゃんと型が付いている。でも、見にくくねぇ。

(gdb) set print pretty
(gdb) p *$1
$5 = {
  pw_name = 0x1e16504d2f00 <_pw_string> "nobody",
  pw_passwd = 0x1e16504d2f07 <_pw_string+7> "*",
  pw_uid = 32767,
  pw_gid = 32767,
  pw_change = 0,
  pw_class = 0x1e16504d2f09 <_pw_string+9> "",
  pw_gecos = 0x1e16504d2f0a <_pw_string+10> "Unprivileged user",
  pw_dir = 0x1e16504d2f1c <_pw_string+28> "/nonexistent",
  pw_shell = 0x1e16504d2f29 <_pw_string+41> "/sbin/nologin",
  pw_expire = 0
}

set print prettyで、見やすくなれと、gdbに指示した。毎回これをやるのが面倒なら、.gdbinitに書いておけばよい。なそ、$1とかは、gdbの自動変数と呼ばれるもので、得た結果を後で使う事が出来る。

(gdb) call geteuid()
$6 = 1000
(gdb) call getuid()
$7 = 1000
(gdb) p $1->pw_uid
$8 = 32767

後は、流れに沿って、実行してみた。getuidの1000ってのは、誰かしら? 調べてみるか。 つたない経験では、パスワード関係の何かを知りたい時は、getpwホニャラ で、よさそう なので、gdbさんに聞いてみる。

(gdb) call getpw
getpwent           getpwent.name      getpwnam_r         getpwuid_internal
getpwent..yppbuf   getpwnam           getpwnam_shadow    getpwuid_r
getpwent.c         getpwnam_internal  getpwuid           getpwuid_shadow

TABで補完候補を出してみた。getpwuidかな?

(gdb) call getpwuid(1000)
$6 = (struct passwd *) 0x871fda64eb0 <_pw_passwd>
(gdb) p *$6
$7 = {
  pw_name = 0x871fda64f00 <_pw_string> "sakae",
  pw_passwd = 0x871fda64f06 <_pw_string+6> "*",
  pw_uid = 1000,
  pw_gid = 1000,
  pw_change = 0,
  pw_class = 0x871fda64f08 <_pw_string+8> "staff",
  pw_gecos = 0x871fda64f0e <_pw_string+14> "sakae",
  pw_dir = 0x871fda64f14 <_pw_string+20> "/home/sakae",
  pw_shell = 0x871fda64f20 <_pw_string+32> "/bin/ksh",
  pw_expire = 0
}

ビンゴでした。そんじゃ、rootさんの登録情報は?

(gdb) p *(getpwnam("root"))
$11 = {
  pw_name = 0x871fda64f00 <_pw_string> "root",
  pw_passwd = 0x871fda64f05 <_pw_string+5> "*",
  pw_uid = 0,
  pw_gid = 0,
  pw_change = 0,
  pw_class = 0x871fda64f07 <_pw_string+7> "daemon",
  pw_gecos = 0x871fda64f0e <_pw_string+14> "Charlie &",
  pw_dir = 0x871fda64f18 <_pw_string+24> "/root",
  pw_shell = 0x871fda64f1e <_pw_string+30> "/bin/ksh",
  pw_expire = 0
}
(gdb) call getpwnam("hogefuga")
$12 = (struct passwd *) 0x0

ここまで出来れば、gdbはC語のirbと言ってもいいでしょう。

以上、実習終了。

+5

shutdownする時刻を、+5 って、指定させられちゃったけど(-5としたらエラーにされた)何でかな? head -20 とかと違う趣だ。ってな事で、ソースに当たってみる。毎度、重箱の隅でスマソ。

shutdown.c の中の、getoffset関数の中。引数は、上位から渡ってくる、tmiearg。

        if (!strcasecmp(timearg, "now")) {              /* now */
                offset = 0;
                return;
        }

        (void)time(&now);
        if (timearg[0] == '+') {                        /* +minutes */
                minutes = strtonum(timearg, 0, INT_MAX, &errstr);
                if (errstr)
                        errx(1, "relative offset is %s: %s", errstr, timearg);
                offset = minutes * 60;
                shuttime = now + offset;
                return;
        }

        /* handle hh:mm by getting rid of the colon */
          :
        /* case [yy][mm][dd][hh][MM] */
	  :

こんな形で、shutdownする時刻を決めていた。now じゃ無かったら、気分的には、now + NN minutes の積りなのね。だから、+5 とかになるんか。

それ以外は、hh:mm での指定、コロンが含まれていないと、dateの設定を行う時に使う形式として処理してる。柔軟だな。

strtonumは、頑丈な作りの文字列から数値への変換器。上の設定だと、0以上(long long)INT_MAX以内に収まる数値を期待。エラーが見つかったら、errstrに内容を返すとな。

ob6$ shutdown -p +123min
shutdown: relative offset is invalid: +123min

これだけでは、便利な関数strtonumの挙動が分かりずらいので、gdbで確認する。特に注意して見ておきたい所は、BUGの温床になりやすい off by one 境界値付近の挙動。

これの確認の為に、auto変数、char *foo なんてのを上で挙げたbase.cに追加しておく。なぜ必要かと言うと、strtonum のエラーが載ってくる引数に、ポインターのアドレスを要求されるから。(C語で書くと、&foo となるんだけど、gdbでは、この&をダイナミックに宣言したものでは、受け付けてくれなかったため)

(gdb) p (sizeof (char *))
$1 = 8
(gdb) call/x malloc(8)
$1 = 0x8ec1c44a8b0
(gdb) call strtonum("100", (long long)-100, (long long)100, &$1)
Attempt to take address of value not located in memory.

ひょっとして、使ってはいけないアドレスを返してきた? いや、gdbは、そんな使い方を する事、想定していないのだろう。深くは追及しないで、本来の目的を遂行する。

(gdb) call strtonum("100", (long long)-100, (long long)100, &foo)
$3 = 100
(gdb) p foo
$4 = 0x0
(gdb) call strtonum("101", (long long)-100, (long long)100, &foo)
$5 = 0
(gdb) p foo
$6 = 0x8ec716687f0 "too large"
(gdb) call strtonum("-101", (long long)-100, (long long)100, &foo)
$7 = 0
(gdb) p foo
$8 = 0x8ec71663b99 "too small"
(gdb) call strtonum("-100", (long long)-100, (long long)100, &foo)
$9 = -100
(gdb) p foo
$10 = 0x0

これで挙動は分かった。後は安心して使うだけ。

(gdb) p (sizeof (long))
$12 = 8
(gdb) p (sizeof (long long))
$13 = 8
(gdb) call strtonum("-88", -100L, 100L, &foo)
$14 = -88

引数をキャストしたけど、あらかじめサイズを調べておけば、数値をそれなりのサイズに指定出来るよ。最初、キャストするのを忘れていて、不可解な挙動を示されて、悩んだのは秘密だ。ちゃんと、タイプを見極めろとな。

(gdb) p/x &foo
$16 = 0x7f7ffffca028
(gdb) p/x &onbss
$17 = 0x8db70c01058
(gdb) p &onbss
$18 = (char **) 0x8db70c01058 <onbss>

このあたりにstackが有るのか。そしてbss領域に割り当てたポインター。これを見ると、普通に一般アプリが使えそうな所だな。

まとめ

wallを中心に見ちゃったけど、shutdownコマンドってのは、wallが表舞台に立つように設計された、rebootやhalt等へのラッパーなのね。知らんかったわい。(マシンを落とすコマンドは、shutdownだけと思っていたのは、秘密だ)

wallをバイパスしたいなら、halt -p とか、poweroff なんてコマンドを叩くのが良い。

最後に、wallで、しつこくメッセージを出すのを抑制する方法を見ておく。

static const struct interval {
        time_t timeleft, timetowait;
} tlist[] = {
        { 10 H,  5 H }, {  5 H,  3 H }, {  2 H,  1 H }, { 1 H, 30 M },
        { 30 M, 10 M }, { 20 M, 10 M }, { 10 M,  5 M }, { 5 M,  3 M },
        {  2 M,  1 M }, {  1 M, 30 S }, { 30 S, 30 S },
        {  0, 0 }
};

この制御リストを使って、最終shutdown時間までの待ち時間によって、案内を出す時間間隔を決めている。時間に余裕が有る時は、ゆったりした時間間隔、まぎわになるとせわしなくって寸法だ。人間心理をついて、嫌味にならない配慮だな。

お彼岸なので、実家に線香をあげに行った。その時義母が嘆いていた。

近くのスーパーのレジが改悪?され、支払いは機械に向かって自らやるとの事。端数を払おうとして、がま口の中を吟味してると、早く金かカードを入れろと、喚くそうな。それも、こっそりならいざ知らず、大声で。とっても恥ずかしく、焦ってしまって、いつも恐々だったそうだ。

年寄りを虐めるんじゃないぞ。対抗策の秘伝を伝授した。がま口の中の小銭は、構わずにガバーと突っ込んじゃいなさい。機械がいそいそと金勘定してら、おたおたすんじゃねぇーって、大声で叱りつけておやんなさい。

この方法、昔々、方々の国を渡り歩く出張時に編み出した。国によって皆硬貨がまちまち。しかも劣化してるのも混じってる。そんな状況で、さっと硬貨を取り出せる訳が無い。

そこでスッチー(死語だな)ご愛用のワレットの登場。ワレットをさっとレジしてる人に広げて、必要な分だけ、ピックアップして貰う。こういう支払をする人は珍しがれて、にこにこしながら清算出来たよ。何、小銭だから、ちょろまかされても、知れたものと腹を決めての所業だけどね。

ついでに、5円とか50円とか、穴の開いたコインを見せると、めずらしがって興味津々だったよ。