地方エンジニアの学習日記

興味ある技術の雑なメモだったりを書いてくブログ。たまに日記とガジェット紹介。

【Linux】Open済みのファイルのパーミッションが変えられてもファイルへの書き込みは続けられる

この記事は「Linux Advent Calendar 2022」の21日目です!空いていたのでいれさせていただきました!!:waiwai:

qiita.com

概要

タイトルが全てですがある一般ユーザで実行されているプロセスがwrite権限を持ってい状態でオープンしたファイルへは別ターミナルから権限を落とすようにchownされても書き込みを続けることができるという話です。

実験

$ id
uid=1000(ryu) gid=1000(ryu) groups=1000(ryu)

というユーザーが

ls -l test
-rw------- 1 ryu ryu 5 Dec 20 23:14 test

というファイルに対して

package main

import (
    "fmt"
    "os"
    "time"
)

func main() {

    filename := "./test"
    f, err := os.OpenFile(filename, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600)
    if err != nil {
        panic(err)
    }

    defer f.Close()

    fmt.Printf("pid=%d, fd=%d", os.Getpid(), f.Fd())

    for {
        i, err := f.WriteString("text\n")
        if err != nil {
            panic(err)
        }
        if i == 5 {
            fmt.Println("ok")
            continue
        }
        break
    }
}

を実行しているときにファイルのパーミッションを変えてみる。OKが出続けてwriteが失敗している様子は確認できなかった。同じようなコードでread版も試してみたが同じく失敗はしなかった

vfsあたりの実装を見てみる

VFS 層の手続きであるvfs_readあたりを眺めてみる。chownが実行されればinodeの情報が更新されるはずだが確かにinode自体を見に行っているわけでは無さそうというのがわかる。vfs_writeの方でも同様の実装となっていてopen(2)で行われた権限チェックで成功していれば以降はreopenしない限りは最初の権限のままファイルオペレーションを実施できる。もちろんその先のオペレーションでEPERMを返す事もできる(ssize_tはsignedなので)。

ssize_t vfs_read(struct file *file, char __user *buf, size_t count, loff_t *pos)
{
    ssize_t ret;

    if (!(file->f_mode & FMODE_READ)) // ファイルがREADモードであるかを見ている
        return -EBADF;
    if (!(file->f_mode & FMODE_CAN_READ)) // 
        return -EINVAL;
    if (unlikely(!access_ok(buf, count)))
        return -EFAULT;

    ret = rw_verify_area(READ, file, pos, count);
    if (ret)
        return ret;
    if (count > MAX_RW_COUNT)
        count =  MAX_RW_COUNT;

    if (file->f_op->read)
        ret = file->f_op->read(file, buf, count, pos); // fs固有の処理を呼び出すまでにpermissionは見ていない
    else if (file->f_op->read_iter)
        ret = new_sync_read(file, buf, count, pos);
    else
        ret = -EINVAL;
    if (ret > 0) {
        fsnotify_access(file);
        add_rchar(current, ret);
    }
    inc_syscr(current);
    return ret;
}

ext2の実装を追っている記事を見てもこの先でpermissionを見ていることは無さそうであった。このへんまで頑張るなら拡張属性とかSELinuxの出番になるのだろうか?

leavatail.hatenablog.com

yuuki0xff.jp

truncateは権限チェックをする

inode自体を書き換えるような操作は当然行うことができない。chownとかchmodができないのは感覚的にもあっていそうだがvfs_truncateでもEPERMが返ってくるケースがあるようなので少し???となった。先程のコードの中でtruncate(2)をする処理を追記するとchownしたタイミングから失敗するようになる。

truncate(2)ではチェックしてるのにread/writeではしてないのは何故だろうと考えてみたがよくわからなかった。truncate(2)はinodeを更新することで実現しているからチェックが必要なのはわかるがwriteでチェックしてないのは何故だろう?(mmapしてメモリで操作して〜みたいなことをやればvfs_writeのチェックは回避できるのでそこでやる意味が少ないとかでしょうか...セキュリティよくわからない...)

long vfs_truncate(const struct path *path, loff_t length)
{
    struct user_namespace *mnt_userns;
    struct inode *inode;
    long error;

    inode = path->dentry->d_inode;

    /* For directories it's -EISDIR, for other non-regulars - -EINVAL */
    if (S_ISDIR(inode->i_mode))
        return -EISDIR;
    if (!S_ISREG(inode->i_mode))
        return -EINVAL;

    error = mnt_want_write(path->mnt);
    if (error)
        goto out;

    mnt_userns = mnt_user_ns(path->mnt);
    error = inode_permission(mnt_userns, inode, MAY_WRITE); // inode自体のパーミッションを見に行っている
    if (error)
        goto mnt_drop_write_and_out;

    error = -EPERM; // 別ターミナルで操作されていれば権限エラーで返す
    if (IS_APPEND(inode))
        goto mnt_drop_write_and_out;

    error = get_write_access(inode);
    if (error)
        goto mnt_drop_write_and_out;

    /*
    * Make sure that there are no leases.  get_write_access() protects
    * against the truncate racing with a lease-granting setlease().
    */
    error = break_lease(inode, O_WRONLY);
    if (error)
        goto put_write_and_out;

    error = security_path_truncate(path);
    if (!error)
        error = do_truncate(mnt_userns, path->dentry, length, 0, NULL);

put_write_and_out:
    put_write_access(inode);
mnt_drop_write_and_out:
    mnt_drop_write(path->mnt);
out:
    return error;
}

POSIX

POSIX 1003.1 では、権限が不十分な場合に [EACCES] エラーを返す必要があるとのこと。読み込みや書き込み時の場合は特に言及が無いらしい。(ちゃんと読めてないので後で読む)

superuser.com

nfsはopen成功してもreadとかwriteで権限エラーになる

manに書いてあるがUIDマッピング使ってるファイルシステムの場合はそうなるという話。

UID マッピングを使用している NFS ファイルシステムでは、 open() がファイルディスクリプターを返した場合でも read(2) が EACCES で拒否される場合がある。 これはクライアントがアクセス許可のチェックを行って open() を実行するが、読み込みや書き込みの際には サーバーで UID マッピングが行われるためである。  

参考