はじめに

shebangからexecveに潜るやつ、過去に何度かやっていた気がするけどまとめていなかったのでメモっておきます

#!

shebang、おまじない。

#!/bin/echo helloって書いたshebangってファイル実行すると、

$ ./shebang
hello ./shebang

なるんですよ。まぁそうですよね。

でもよく考えると不思議じゃないですか。

#!って書いただけでインタプリタが選択されるんですよ。

じゃあshebangをハンドリングするbinfmtがあるんか?

調べてみました。

search_binary_handler

過去のPostにもある通り、search_binary_handlerに答えがあります。

これはdo_execveの中で呼ばれる関数(厳密にはdo_execveat_common->bprm_execve->exec_binprm->search_binary_handlerの順)です。名前的にはexecveの第一引数をbinaryとし、そのハンドラーを探してくれそうですよね。

探してくれるんですよ。

#define printable(c) (((c)=='\t') || ((c)=='\n') || (0x20<=(c) && (c)<=0x7e))
/*
 * cycle the list of binary formats handler, until one recognizes the image
 */
static int search_binary_handler(struct linux_binprm *bprm)
{
    bool need_retry = IS_ENABLED(CONFIG_MODULES);
    struct linux_binfmt *fmt;
    int retval;

    retval = prepare_binprm(bprm);
    if (retval < 0)
        return retval;

    retval = security_bprm_check(bprm);
    if (retval)
        return retval;

    retval = -ENOENT;
 retry:
    read_lock(&binfmt_lock);
    list_for_each_entry(fmt, &formats, lh) {
        if (!try_module_get(fmt->module))
            continue;
        read_unlock(&binfmt_lock);

        retval = fmt->load_binary(bprm);

        read_lock(&binfmt_lock);
        put_binfmt(fmt);
        if (bprm->point_of_no_return || (retval != -ENOEXEC)) {
            read_unlock(&binfmt_lock);
            return retval;
        }
    }
    read_unlock(&binfmt_lock);

    if (need_retry) {
        if (printable(bprm->buf[0]) && printable(bprm->buf[1]) &&
            printable(bprm->buf[2]) && printable(bprm->buf[3]))
            return retval;
        if (request_module("binfmt-%04x", *(ushort *)(bprm->buf + 2)) < 0)
            return retval;
        need_retry = false;
        goto retry;
    }

    return retval;
}

list_for_each_entryformatsを見ていて、これはstruct linux_binfmtのListになっている。

で、fs/binfmt_elf.cとかfs/binfmt_script.cとか見るとregister_binfmtしているんですよね。

registerされたそれぞれのbinfmtに対してENOEXEC以外が返ってくるまでfmt->load_binaryを試行している。

じゃあload_binaryはどうなってるんだって話。

今回はShebangが知りたいのでbinfmt_script.cを読む。

static struct linux_binfmt script_format = {
    .module		= THIS_MODULE,
    .load_binary	= load_script,
};

すげぇシンプルな構造体。

肝心のload_script

static int load_script(struct linux_binprm *bprm)
{
    const char *i_name, *i_sep, *i_arg, *i_end, *buf_end;
    struct file *file;
    int retval;

    /* Not ours to exec if we don't start with "#!". */
    if ((bprm->buf[0] != '#') || (bprm->buf[1] != '!'))
        return -ENOEXEC;

    /*
     * This section handles parsing the #! line into separate
     * interpreter path and argument strings. We must be careful
     * because bprm->buf is not yet guaranteed to be NUL-terminated
     * (though the buffer will have trailing NUL padding when the
     * file size was smaller than the buffer size).
     *
     * We do not want to exec a truncated interpreter path, so either
     * we find a newline (which indicates nothing is truncated), or
     * we find a space/tab/NUL after the interpreter path (which
     * itself may be preceded by spaces/tabs). Truncating the
     * arguments is fine: the interpreter can re-read the script to
     * parse them on its own.
     */
    buf_end = bprm->buf + sizeof(bprm->buf) - 1;
    i_end = strnchr(bprm->buf, sizeof(bprm->buf), '\n');
    if (!i_end) {
        i_end = next_non_spacetab(bprm->buf + 2, buf_end);
        if (!i_end)
            return -ENOEXEC; /* Entire buf is spaces/tabs */
        /*
         * If there is no later space/tab/NUL we must assume the
         * interpreter path is truncated.
         */
        if (!next_terminator(i_end, buf_end))
            return -ENOEXEC;
        i_end = buf_end;
    }
    /* Trim any trailing spaces/tabs from i_end */
    while (spacetab(i_end[-1]))
        i_end--;

    /* Skip over leading spaces/tabs */
    i_name = next_non_spacetab(bprm->buf+2, i_end);
    if (!i_name || (i_name == i_end))
        return -ENOEXEC; /* No interpreter name found */

    /* Is there an optional argument? */
    i_arg = NULL;
    i_sep = next_terminator(i_name, i_end);
    if (i_sep && (*i_sep != '\0'))
        i_arg = next_non_spacetab(i_sep, i_end);

    /*
     * If the script filename will be inaccessible after exec, typically
     * because it is a "/dev/fd/<fd>/.." path against an O_CLOEXEC fd, give
     * up now (on the assumption that the interpreter will want to load
     * this file).
     */
    if (bprm->interp_flags & BINPRM_FLAGS_PATH_INACCESSIBLE)
        return -ENOENT;

    /*
     * OK, we've parsed out the interpreter name and
     * (optional) argument.
     * Splice in (1) the interpreter's name for argv[0]
     *           (2) (optional) argument to interpreter
     *           (3) filename of shell script (replace argv[0])
     *
     * This is done in reverse order, because of how the
     * user environment and arguments are stored.
     */
    retval = remove_arg_zero(bprm);
    if (retval)
        return retval;
    retval = copy_string_kernel(bprm->interp, bprm);
    if (retval < 0)
        return retval;
    bprm->argc++;
    *((char *)i_end) = '\0';
    if (i_arg) {
        *((char *)i_sep) = '\0';
        retval = copy_string_kernel(i_arg, bprm);
        if (retval < 0)
            return retval;
        bprm->argc++;
    }
    retval = copy_string_kernel(i_name, bprm);
    if (retval)
        return retval;
    bprm->argc++;
    retval = bprm_change_interp(i_name, bprm);
    if (retval < 0)
        return retval;

    /*
     * OK, now restart the process with the interpreter's dentry.
     */
    file = open_exec(i_name);
    if (IS_ERR(file))
        return PTR_ERR(file);

    bprm->interpreter = file;
    return 0;
}

これも結構シンプル

まず#!で始まっている事を検証し、その次に出てくるインタプリタのパスを取得、引数を設定している。

そして最後にopen_execしてbprm->interpreterを設定している。

まとめ

というわけでshebangもbinfmtの一つであるbinfmt_scriptでいい感じにされてた訳です。

まぁそうですよね。

終わりに

この記事はn01e0 Advent Calendar 2024の19日目の記事とします。