AUDIT_LOGIN ( type=LOGIN) , /proc/self/loginuid についての調べ物

/proc/self/loginuid に write すると記録される Audit ログについて調べた

type=LOGIN msg=audit(1727786101.781:162): pid=4232 uid=0 subj=unconfined old-auid=4294967295 auid=0 tty=(none) old-ses=4294967295 ses=3 res=1UID="root" OLD-AUID="unset" AUID="root"

include/uapi/linux/audit.h

AUDIT_LOGIN 定数から追いかけていく。

#define AUDIT_LOGIN             1006    /* Define the login id and information */

AUDIT_LOGIN は audit_set_loginuid() で登場する (後述)。ユーザランドがら追う場合は /proc/self/loginuid の実装から潜っていくのがいいか

ユーザランド

----- /proc/self/loginuid ------------------

Linux カーネル 🐧 ( システムコール や VFS のレイヤは省略 )

-> proc_loginuid_write
  -> audit_set_loginuid
  .. audit_set_loginuid_perm
   -> audit_log_set_loginuid // AUDIT_LOGIN

/proc/self/loginuid

$ ls -hal /proc/self/loginuid 
-rw-r--r-- 1 hiboma hiboma 0 Oct  1 22:33 /proc/self/loginuid

fs/proc/base.c に loginuid の定義がある

REG("loginuid",  S_IWUSR|S_IRUGO, proc_loginuid_operations),

loginuid の file_operations は下記の通り

static const struct file_operations proc_loginuid_operations = {
        .read           = proc_loginuid_read,
        .write          = proc_loginuid_write,
        .llseek         = generic_file_llseek,
};

proc_loginuid_write 以下に潜っていく

proc_loginuid_write()

procfs レイヤと audit のレイヤを橋渡しするような役割になっている。write されたバッファから loginuid を読み取る。

static ssize_t proc_loginuid_write(struct file * file, const char __user * buf,
                   size_t count, loff_t *ppos)
{
    struct inode * inode = file_inode(file);
    uid_t loginuid;
    kuid_t kloginuid;
    int rv;

    /* Don't let kthreads write their own loginuid */
    if (current->flags & PF_KTHREAD)
        return -EPERM;

    rcu_read_lock();
    if (current != pid_task(proc_pid(inode), PIDTYPE_PID)) {
        rcu_read_unlock();
        return -EPERM;
    }
    rcu_read_unlock();

    if (*ppos != 0) {
        /* No partial writes. */
        return -EINVAL;
    }

    rv = kstrtou32_from_user(buf, count, 10, &loginuid);
    if (rv < 0)
        return rv;

    /* is userspace tring to explicitly UNSET the loginuid? */
    if (loginuid == AUDIT_UID_UNSET) {
        kloginuid = INVALID_UID;
    } else {
        kloginuid = make_kuid(file->f_cred->user_ns, loginuid);
        if (!uid_valid(kloginuid))
            return -EINVAL;
    }

    rv = audit_set_loginuid(kloginuid); ⬇️
    if (rv < 0)
        return rv;
    return count;
}

audit_set_loginuid()

  • 権限の確認
  • sessionid の生成
  • current->sessionid をセット
  • current->logind をセット
/**
 * audit_set_loginuid - set current task's loginuid
 * @loginuid: loginuid value
 *
 * Returns 0.
 *
 * Called (set) from fs/proc/base.c::proc_loginuid_write().
 */
int audit_set_loginuid(kuid_t loginuid)
{
    unsigned int oldsessionid, sessionid = AUDIT_SID_UNSET;
    kuid_t oldloginuid;
    int rc;

    oldloginuid = audit_get_loginuid(current); ✍️
    oldsessionid = audit_get_sessionid(current); ✍️

    rc = audit_set_loginuid_perm(loginuid); ✍️
    if (rc)
        goto out;

    /* are we setting or clearing? */
    if (uid_valid(loginuid)) {
        sessionid = (unsigned int)atomic_inc_return(&session_id); ✍️
        if (unlikely(sessionid == AUDIT_SID_UNSET))
            sessionid = (unsigned int)atomic_inc_return(&session_id);
    }

    current->sessionid = sessionid;
    current->loginuid = loginuid;
out:
    audit_log_set_loginuid(oldloginuid, loginuid, oldsessionid, sessionid, rc); ⬇️
    return rc;
}

✍️ audit_get_sessionid, audit_get_sessionid はインライン関数

static inline kuid_t audit_get_loginuid(struct task_struct *tsk)
{
        return tsk->loginuid;
}

static inline unsigned int audit_get_sessionid(struct task_struct *tsk)
{
        return tsk->sessionid;
}

✍️ audit_set_loginuid_perm は 権限の確認を入れている。値がセットされてなければ権限が必要ない。

  • AUDIT_FEATURE_LOGINUID_IMMUTABLE
  • CAP_AUDIT_CONTROL
  • AUDIT_FEATURE_ONLY_UNSET_LOGINUID
static int audit_set_loginuid_perm(kuid_t loginuid)
{
    /* if we are unset, we don't need privs */
    if (!audit_loginuid_set(current))
        return 0;
    /* if AUDIT_FEATURE_LOGINUID_IMMUTABLE means never ever allow a change*/
    if (is_audit_feature_set(AUDIT_FEATURE_LOGINUID_IMMUTABLE))
        return -EPERM;
    /* it is set, you need permission */
    if (!capable(CAP_AUDIT_CONTROL))
        return -EPERM;
    /* reject if this is not an unset and we don't allow that */
    if (is_audit_feature_set(AUDIT_FEATURE_ONLY_UNSET_LOGINUID)
                 && uid_valid(loginuid))
        return -EPERM;
    return 0;
}

✍️ session_id は static atomic_t で管理されてる

/* global counter which is incremented every time something logs in */
static atomic_t session_id = ATOMIC_INIT(0);

audit_log_set_loginuid()

audit_log_start() 〜 audit_log_format 〜 audit_log_end() で audit_buffer バッファに文字列を書き込んでいく。バッファを扱う実装は複雑なので、別記する

static void audit_log_set_loginuid(kuid_t koldloginuid, kuid_t kloginuid,
                   unsigned int oldsessionid,
                   unsigned int sessionid, int rc)
{
    struct audit_buffer *ab;
    uid_t uid, oldloginuid, loginuid;
    struct tty_struct *tty;

    if (!audit_enabled)
        return;

err ab = audit_log_start(audit_context(), GFP_KERNEL, AUDIT_LOGIN);
    if (!ab)
        return;

    uid = from_kuid(&init_user_ns, task_uid(current));
    oldloginuid = from_kuid(&init_user_ns, koldloginuid);
    loginuid = from_kuid(&init_user_ns, kloginuid);
    tty = audit_get_tty();

    audit_log_format(ab, "pid=%d uid=%u", task_tgid_nr(current), uid);
    audit_log_task_context(ab);
    audit_log_format(ab, " old-auid=%u auid=%u tty=%s old-ses=%u ses=%u res=%d",
             oldloginuid, loginuid, tty ? tty_name(tty) : "(none)",
             oldsessionid, sessionid, !rc);
    audit_put_tty(tty);
    audit_log_end(ab);
}

派生で調べ物

pam_loginuid(8) - Linux manual page

The pam_loginuid module sets the loginuid process attribute forthe process that was authenticated. This is necessary forapplications to be correctly audited. This PAM module should onlybe used for entry point applications like: login, sshd, gdm,vsftpd, crond and atd. There are probably other entry pointapplications besides these. You should not use it forapplications like sudo or su as that defeats the purpose bychanging the loginuid to the account they just switched to.

pam_loginuidモジュールは、認証されたプロセスのloginuidプロセス属性を設定する。これは、アプリケーションを正しく監査するために必要です。このPAMモジュールは、login、sshd、gdm、vsftpd、crond、atdなどのエントリーポイントアプリケーションにのみ使用されるべきである。これら以外にもエントリーポイントのアプリケーションはあるでしょう。sudoやsuのようなアプリケーションには使用しないでください。loginuidを切り替えたアカウントに変更することで、目的が達成されないからです。

root@develop-hiboma:~# grep -R pam_loginuid /etc/
/etc/pam.d/login:session    required     pam_loginuid.so
/etc/pam.d/cron:session    required     pam_loginuid.so
/etc/pam.d/sshd:session    required     pam_loginuid.so

kernel.org/doc/Documentation/ABI/stable/procfs-audit_loginuid

  • AUDIT_FEATURE_LOGINUID_IMMUTABLE
  • AUDIT_FEATURE_ONLY_UNSET_LOGINUID
What:        Audit Login UID
Date:       2005-02-01
KernelVersion:  2.6.11-rc2 1e2d1492e178 ("[PATCH] audit: handle loginuid through proc")
Contact:    linux-audit@redhat.com
Users:      audit and login applications
Description:
        The /proc/$pid/loginuid pseudofile is written to set and
        read to get the audit login UID of process $pid as a
        decimal unsigned int (%u, u32).  If it is unset,
        permissions are not needed to set it.  The accessor must
        have CAP_AUDIT_CONTROL in the initial user namespace to
        write it if it has been set.  It cannot be written again
        if AUDIT_FEATURE_LOGINUID_IMMUTABLE is enabled.  It
        cannot be unset if AUDIT_FEATURE_ONLY_UNSET_LOGINUID is
        enabled.

What:       Audit Login Session ID
Date:       2008-03-13
KernelVersion:  2.6.25-rc7 1e0bd7550ea9 ("[PATCH] export sessionid alongside the loginuid in procfs")
Contact:    linux-audit@redhat.com
Users:      audit and login applications
Description:
        The /proc/$pid/sessionid pseudofile is read to get the
        audit login session ID of process $pid as a decimal
        unsigned int (%u, u32).  It is set automatically,
        serially assigned with each new login.

CAP_AUDIT_CONTROL

       CAP_AUDIT_CONTROL (since Linux 2.6.11)
              Enable and disable kernel auditing; change auditing filter
              rules; retrieve auditing status and filtering rules.

一般ユーザで write しようとすると EPERM を返す

hiboma@pcamp-develop-hiboma:~$ echo 1000 > /proc/self/loginuid 
-bash: echo: write error: Operation not permitted

Linux audit ログの調べ物 - libaudit を試す

Linux の audit ログを 内部の実装も調べもせず 何となくな知識まま扱っていたので、いっちょ整頓してみようと調べ物をしていた。カーネルのソースを読んだり、libaudit のソースを読んだり。

kernel.org

github.com

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() を呼び出すのは一緒で、扱う文字列のフォーマットに差異があるだけっぽい

参考

rheb.hatenablog.com

5ecure.medium.com

security-plus-data-science.blogspot.com

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 ですぐに見つかりました。コミットで追加されている。

github.com

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 のログが出ていたが、その後にプロセスの詳細が出ていなくて混乱を招いたケースがあったのでしょうね。