Linux audit ログの調べ物 - libaudit を試す
Linux の audit ログを 内部の実装も調べもせず 何となくな知識まま扱っていたので、いっちょ整頓してみようと調べ物をしていた。カーネルのソースを読んだり、libaudit のソースを読んだり。
audit ログはカーネル内部で生成されるもの ( 例えばシステムコールの監査 ) と、ユーザ空間から書き込むものとあるが ( 例えば pam モジュール )、後者は libaudit を使ってログを出せる。
Proof of Concept
libaudit での audit ログ書き出しは割と簡単なコードで扱える。試しに小さいコードを書いた。
#include <stdio.h> #include <libaudit.h> int main() { int fd = audit_open(); if (fd == -1) { perror("failed to audit_open"); return 1; } if (audit_log_user_message(fd, AUDIT_USYS_CONFIG, "audit_log_user_message", "example.com", NULL, NULL, 1) < 0) { perror("failed to audit_log_user_message"); audit_close(fd); return 1; } audit_close(fd); return 0; }
root ユーザで実行すると /var/log/audit/audit.log に以下のようなログが残る
type=USYS_CONFIG msg=audit(1728036027.707:474): pid=6119 uid=0 auid=1000 ses=48 subj=unconfined msg='audit_log_user_message exe="/vagrant/main" hostname=example.com addr=93.184.215.14 terminal=pts/2 res=success'UID="root" AUID="vagrant"
type の指定は適当である。
audit_open(3)
audit ログを書き出すためのファイルデスクリプタを返す関数であるが、socket(PF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_AUDIT); なソケットを扱っている。
/* * This function opens a connection to the kernel's audit * subsystem. You must be root for the call to succeed. On error, * a negative value is returned. On success, the file descriptor is * returned - which can be 0 or higher. */ int audit_open(void) { int fd = socket(PF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_AUDIT); if (fd < 0) { if (errno == EINVAL || errno == EPROTONOSUPPORT || errno == EAFNOSUPPORT) audit_msg(LOG_ERR, "Error - audit support not in kernel"); else audit_msg(LOG_ERR, "Error opening audit netlink socket (%s)", strerror(errno)); } return fd; }
netlink と audit のアーキテクチャは、検索すれば色々な文章が見つかるのでそちらを参考にされたし
audit_close(3)
audit_open(3) で得たファイルデスクリプタを close(2) する。すごい、エラーハンドリングがない!
void audit_close(int fd) { if (fd >= 0) close(fd); }
(カーネルのバグがない限りは ... ) netlink のソケットの close(2) は必ず成功するんだろうかな?
audit_log_user_message(3)
audit ログに書き出す文字列を snprintf(3) で組み立てて、libaudit 内部の audit_send_user_message() を呼び出す。 (最終的に netlink のソケットに書き出す。この辺は深追いししすぎになるので触れない )
以下のような処理が入ってるあたりが面白かった
- 引数の addr の 名前解決 (DNS ) をしてくれる
- audit_log_user_message(3) を呼び出したプロセスを実行するバイナリ名を解決してくれる
- audit_log_user_message(3) を呼び出したプロセスに付いた TTY 名を解決してくれる
/* * This function will log a message to the audit system using a predefined * message format. This function should be used by all console apps that do * not manipulate accounts or groups. * * audit_fd - The fd returned by audit_open * type - type of message, ex: AUDIT_USER, AUDIT_USYS_CONFIG, AUDIT_USER_LOGIN * message - the message being sent * hostname - the hostname if known * addr - The network address of the user * tty - The tty of the user * result - 1 is "success" and 0 is "failed" * * It returns the sequence number which is > 0 on success or <= 0 on error. */ int audit_log_user_message(int audit_fd, int type, const char *message, const char *hostname, const char *addr, const char *tty, int result) { char buf[MAX_AUDIT_MESSAGE_LENGTH]; char addrbuf[INET6_ADDRSTRLEN]; static char exename[PATH_MAX*2]=""; char ttyname[TTY_PATH]; const char *success; int ret; if (audit_fd < 0) return 0; if (result) success = "success"; else success = "failed"; /* If hostname is empty string, make it NULL ptr */ if (hostname && *hostname == 0) hostname = NULL; /* See if we can deduce addr */ addrbuf[0] = 0; if (addr == NULL || strlen(addr) == 0) _resolve_addr(addrbuf, hostname); else strncat(addrbuf, addr, sizeof(addrbuf)-1); /* Fill in exec name if needed */ if (exename[0] == 0) _get_exename(exename, sizeof(exename)); /* Fill in tty if needed */ if (tty == NULL) tty = _get_tty(ttyname, TTY_PATH); else if (*tty == 0) tty = NULL; /* Get the local name if we have a real tty */ if (hostname == NULL && tty) hostname = _get_hostname(tty); snprintf(buf, sizeof(buf), "%s exe=%s hostname=%s addr=%s terminal=%s res=%s", message, exename, hostname ? hostname : "?", addrbuf, tty ? tty : "?", success ); errno = 0; ret = audit_send_user_message( audit_fd, type, HIDE_IT, buf ); if ((ret < 1) && errno == 0) errno = ret; return ret; }
audit_log_user_message(3) 以外にも audit ログを書き出す関数はあるが、内部で audit_send_user_message() を呼び出すのは一緒で、扱う文字列のフォーマットに差異があるだけっぽい
- audit_log_user_comm_message(3) - Linux man page,
- audit_log_acct_message(3) - Linux man page
- audit_log_user_avc_message(3) - Linux man page
- audit_log_semanage_message(3) - Linux man page
参考
OOM キラーの "Memory cgroup out of memory: OOM victim *** (***) is already exiting. Skip killing the task" を調べた
業務中に Linux の OOM キラーの dmesg を見ていたら、見慣れない形式のログがあったので調べてみた
ログ
こんなログです
May 23 02:06:26 **** kernel: [40872.663481] Memory cgroup out of memory: OOM victim 46452 (nginx) is already exiting. Skip killing the task
〜 is already exiting. Skip killing the task というメッセージは見たことがなかったので調べました。
ソースを調べる
該当のメッセージを grep するとすぐに見つかりました。__oom_kill_process() で出力しています。
static void __oom_kill_process(struct task_struct *victim, const char *message) { struct task_struct *p; struct mm_struct *mm; bool can_oom_reap = true; p = find_lock_task_mm(victim); if (!p) { pr_info("%s: OOM victim %d (%s) is already exiting. Skip killing the task\n", message, task_pid_nr(victim), victim->comm); put_task_struct(victim); return; } else if (victim != p) { get_task_struct(p); put_task_struct(victim); victim = p; } ... 略
OOM でプロセスを止める際、task_struct -> mm_struct が参照できない場合に出力されるログのようですね。プロセスが止まる処理と OOM キラーで止めるタイミングがかぶると出るんだろう。
コミットを探す
git blame ですぐに見つかりました。コミットで追加されている。
5.9 で入ったのかな
コミットログは下記のとおりです
When the OOM killer finds a victim and tryies to kill it, if the victim is already exiting, the task mm will be NULL and no process will be killed. But the dump_header() has been already executed, so it will be strange to dump so much information without killing a process. We'd better show some helpful information to indicate why this happens.
翻訳しておきます
OOMキラーが犠牲者を見つけ、それを殺そうとするとき、犠牲者がすでに終了していれば、タスクmmはNULLになり、プロセスは殺されない。 しかし、dump_header()はすでに実行されているので、プロセスを殺さずに多くの情報をダンプするのは奇妙である。 なぜこのようなことが起こるのかを示すために なぜこのようなことが起こるのかを示すために、有益な情報を示す必要がある。
DeepL での翻訳
なるほどなぁ。
dump_header()
下記のような実装です
static void dump_header(struct oom_control *oc, struct task_struct *p) { pr_warn("%s invoked oom-killer: gfp_mask=%#x(%pGg), order=%d, oom_score_adj=%hd\n", current->comm, oc->gfp_mask, &oc->gfp_mask, oc->order, current->signal->oom_score_adj); if (!IS_ENABLED(CONFIG_COMPACTION) && oc->order) pr_warn("COMPACTION is disabled!!!\n"); dump_stack(); if (is_memcg_oom(oc)) mem_cgroup_print_oom_meminfo(oc->memcg); else { __show_mem(SHOW_MEM_FILTER_NODES, oc->nodemask, gfp_zone(oc->gfp_mask)); if (should_dump_unreclaim_slab()) dump_unreclaimable_slab(); } if (sysctl_oom_dump_tasks) dump_tasks(oc); if (p) dump_oom_summary(oc, p); }
おそらくは、dmesg に invoked oom-killer のログが出ていたが、その後にプロセスの詳細が出ていなくて混乱を招いたケースがあったのでしょうね。
下戸倉沢の右岸 〜 上富士入之防線 🏃
塩原ダム近くの、特に名前やルートがあるわけでもないただの山道を走ってきた。
場所はここです。
走ったコースはこんな感じです。2時間かからないくらいで 走る + 登る ができる低山トレーニングなコースになりそう。
ちょうど ↓ で走ったルートの南側になります。
箒川の駐車場をスタート地点にします。釣人さんもいました。

どんな魚が釣れるんじゃろうか〜

途中通った、和田山隧道です。夕方ごろの日差しで ちょっと神秘的な雰囲気でした。

和田山隧道(トンネル) をすぐ出た先の谷から尾根に登って行きます。左側は小石でガレ(ザレ?) てますが、右側が石も落ち葉も少なくて登りやすい。谷の窪み ( V字の底 ) には枯れ葉が溜まってて、ズボっと足を取られるので注意。

ここが山に入った地点。緑の多い季節だとしんどそうだな
尾根道はこんな感じです。はっきりと踏み固められた道になってますね。ピンクテープも貼ってあるので迷いにくいです。人間だけでなく鹿の通り道でもあるようです ( 💩 で分かる)

三角点を発見

道は送電鉄塔を結ぶように続いています。おそらく、電力会社の人が通るのでしょう。

眺めはぼちぼち良いところが多くて、塩原ダム(左) や那須の山々(右)に見えます。

木々が成長して送電線を邪魔しないよう木が伐採されているみたいで、尾根道なのに不自然に原っぱが広がる。おかげで展望は良い。人間の都合で山の木を植えたり刈ったり〜

獣が登らないように "返し" がついていますね。人間も登ってはだめです 🚫

小1時間ほど登ったあとは、薄暗い谷を下って帰り道に付きます。ずいぶんと昔に切り倒された風な大木 (木の名前が分からない)

管理された針葉樹(桧かな?) の森に出ると危険なところは控えめで、安心して走れます。

さらに下って整った林道に出ました。車も入ってる雰囲気。

舗装された道に出れば安心安全で、ほっと一息。ここからは 3km くらい走ってゴール (スタート地点) まで戻りました。

上富士入之防線という林道らしいですが、インターネットには情報がありません。
帰り際に、走った山を遠くから眺めておしまい。
