小ネタ

3.1からLinux の RLIMIT_NPROC のあつかいがちょっと変わります。端的にいうとNetBSDちっくな動きになりました。

まずバックグランドを説明すると、NPROCはユーザあたりのプロセス数を制限する機能であると。端的にいうとプロセス数超過するとforkがEAGAIN返して失敗する。プロセスを作る方法は1つしかないから一見自明に見える。

ところが「ユーザあたりのプロセス数」というのがキモで、プロセスの所有ユーザを変えてしまうという手がある。set*uid() 族と setuidされたプログラムに対するexec()族である。

余談。従来Linuxはset*uid()族はNPROCチェックをしてEAGAINを返していたが、execではチェックしていなかった。ついでにいうとforkでのチェックもちゃんとロックされてなかったので、プロセス数の厳密な保証はもとから無かった。NPROCなんてしょせんforkbomb対策のオモチャだから適当でいいんだよ。というのはLinusの弁。まあ、それは置く。

set*uid()でチェックすると何が問題かというと、POSIX的にはset*uid()は正しい引数を与える限りにおいて失敗しないので、かなり著名なソフトでも誰も返値をチェックしてない。ついでにいうとLinuxはmanページからしてEAGAINの記載がないというていらくで(※ごめん、いま見直したらmanにはちゃんと書いて有ったわ)エスパーでもない限り失敗するケースを思いつく人はいないという状況だった。

さて、set*uid()を使う場面というのは(ごく一部の例外を除いて)端的に言えばrootが非rootに変身して権限を落とすということであるので、ここで失敗してかつエラーチェックを怠るとroot権限をもったまま信頼できないプログラムをexecするアホdaemonが出来上がる。

で、権限を落とすシステムコールを失敗させるのはセキュリティ的にたいへん危険なので、権限昇格させるほう、つまりexec族で失敗させるのが正しいんじゃね?という意見が提出された。普通のデーモンは fork - setuid - exec の3つをつづけて呼ぶのでチェックがsetuidからexecに変わっても、実質の動きはかわらんでしょう、と。

で、いつものように互換性がにゃーにゃー、戻り値をチェックしないプログラムがワンワンと騒いでいたのだが、たまたまtwitterでsodaさんがNは何年も前からexecでチェックしてるとつぶやいていたのでLinusに転送してみたら採用されてしまった。

そのあと互換性議論が復活して、setuid使ってないプロセスはexec失敗しないよう仕様が若干変更され、現在の使用は
・set*uid()時にNPROC超過を検出し、かつ
・その後のexecでも再びNPROC超過を検出したとき
のみexecがEAGAINを返す。となっている

というわけで、この件でなにか困った事にぶちあたった人がいたらsodaさんをdisるといいと思うよ。

・・・という免責事項を軽くジャブしたあとでいうのもなんですけど、NetBSDのみなさんはRLIMIT_NPROCのコメントやドキュメントで「Linuxと違ってNetBSDはhogehoge」などと説明してるところをいちいち治して回る刺身たんぽぽの作業をするといいと思うよ。ではでは


以下はLinus treeに入ってるコミットログ

commit 72fa59970f8698023045ab0713d66f3f4f96945c
Author: Vasiliy Kulikov
Date: Mon Aug 8 19:02:04 2011 +0400

move RLIMIT_NPROC check from set_user() to do_execve_common()

The patch http://lkml.org/lkml/2003/7/13/226 introduced an RLIMIT_NPROC
check in set_user() to check for NPROC exceeding via setuid() and
similar functions.

Before the check there was a possibility to greatly exceed the allowed
number of processes by an unprivileged user if the program relied on
rlimit only. But the check created new security threat: many poorly
written programs simply don't check setuid() return code and believe it
cannot fail if executed with root privileges. So, the check is removed
in this patch because of too often privilege escalations related to
buggy programs.

The NPROC can still be enforced in the common code flow of daemons
spawning user processes. Most of daemons do fork()+setuid()+execve().
The check introduced in execve() (1) enforces the same limit as in
setuid() and (2) doesn't create similar security issues.

Neil Brown suggested to track what specific process has exceeded the
limit by setting PF_NPROC_EXCEEDED process flag. With the change only
this process would fail on execve(), and other processes' execve()
behaviour is not changed.

Solar Designer suggested to re-check whether NPROC limit is still
exceeded at the moment of execve(). If the process was sleeping for
days between set*uid() and execve(), and the NPROC counter step down
under the limit, the defered execve() failure because NPROC limit was
exceeded days ago would be unexpected. If the limit is not exceeded
anymore, we clear the flag on successful calls to execve() and fork().

The flag is also cleared on successful calls to set_user() as the limit
was exceeded for the previous user, not the current one.

Similar check was introduced in -ow patches (without the process flag).