修改bio完成加解密文件系统

修改bio完成加解密文件系统

背景

bio是block io,它是一个描述硬盘里面的位置与page cache的页对应关系的数据结构,每个bio对应的是硬盘里面一块或多块连续的位置,每一块硬盘里面连续的位置,可能对应着page cache的多页或者一页,bio中bio_vec *bi_io_vec的表保存着内存和硬盘位置的关系。
而每个bio_vec都只能描述一个页内数据的连续的数据的偏移和长度,当一次IO长度大于一页的时候需要一个数组来表示他们的关系:

images通过bv_offset和bv_len来描述硬盘中起始位置和长度。

一个bio的处理:一个bio通常通过submit_bio来提交给设备队列,下面会经过bio聚合,转化成request,磁盘调度队列,块设备驱动,磁盘处理;对于bio来说大部分都是异步,这样提交完bio请求之后CPU就可以去完成其他工作,在bio完成后通过bio->bi_end_io来作为callback获取结果,外部的接口使用bio_endio。同步方式的读写请求是通过等待callback事件到来从而完成同步读写。

实践操作bio进行加解密

下面通过对块设备数据加解密来实践一下bio的操作,磁盘中保存的数据是密文,但是page cache中保存的数据时明文,对于文件系统来说是透明的,通过这样hook bio的操作实际上就完成了一次软加解密,和ecryptfs比较相似完成一个简单的加密文件系统,这里加密算法选用最简单的sm4(ecb)加解密。

设计

submit_bio是发起读写IO的起点,也是bio和request层的临界点,非常适合来进行做一些小动作,所以通过hook submit_bio来进行写加密,保证提交给下面的数据是经过加密的就好了。而对于读IO可以通过在IO完成之后通知给文件系统之前进行解密,可以hook bio_endio来进行数据解密,确保page cache上的数据是经过解密后的明文。

写加密

bio只是管理的数据结构,数据仍然是存放在page cache中的,最终的结果要求是page cache中的内容仍然是明文,提交给磁盘的数据是密文。写的内容是保存在page cache中的,我们不能直接修改它否则page cache中的内容就变成密文了,一块数据是搞不定这件事了,我们需要重新clone一个bio,也就是申请一块内存来存放密文数据,将clone后的bio提交给通用块设备。

long origin_submit_bio(int rw, struct bio *bio);
void hook_submit_bio(int rw, struct bio *bio)		//hook submit_bio,先完成加密操作然后将加密后的bio提交给block层
{
    ret = try_crypt_bio(rw, bio);		//克隆bio并对其中的内容进行加密
    if (ret != 0) {
        origin_submit_bio(rw, bio);    //对于不需要进行加密的bio通过原路径提交给block
    }
    return;
}
int bfx_bio_alloc_pages(struct bio *bio, gfp_t gfp)
{
    int i;
    struct bio_vec *bv;

    bio_for_each_segment(bv, bio, i) {
        bv->bv_page = alloc_page(gfp);
        if (!bv->bv_page) {
            while (bv-- != bio->bi_io_vec + bio->bi_idx)
                __free_page(bv->bv_page);
            return -ENOMEM;
        }
    }

    return 0;
}

void bfx_crypt_callback(struct bio *bio, int error)
{
    unsigned int i;
    struct bio_vec *bv;
    struct bio *origin = (struct bio *)bio->bi_private;

    bio_for_each_segment_all(bv, bio, i) {
        BUG_ON(!bv->bv_page);
        __free_page(bv->bv_page);
        bv->bv_page = NULL;
    }

    bio_endio(origin, error);

    bio_put(bio);
}
struct bio* bfx_bio_clone(struct bio *origin)
{
    struct bio *new = NULL;
    new = bio_clone(origin, GFP_NOIO);
    if (!new)
        return NULL;
    if (bfx_bio_alloc_pages(new, GFP_NOIO)) {
        bio_put(new);
        return NULL;
    }
    new->bi_private = origin;				//利用bi_private成员引用原始的bio
    new->bi_end_io = bfx_crypt_callback;	//新的clone bio操作完成后通知原始的bio
    return new;
}
int try_crypt_bio(int rw, struct bio *bio)
{
    sm4_key_t key;
    int i, j;                                                                                                                                  
    char buffer[SM4_BLOCK_SIZE * 2 + 1];
    struct bio_vec *bv;
    struct bio *new;

    if (!(rw & WRITE)) {
        return -1;
    }

    new = bfx_bio_clone(bio);
    if (!new) {
        return -ENOMEM;
    }
    new->bi_rw |= rw;
    bio->bi_rw |= rw;

    sm4_set_key1(&key, mykey, 16);

    bio_for_each_segment(bv, bio, i) {	//遍历bio对其内容进行原地加密操作
        void *p1 = kmap(bv->bv_page);
        void *p2 = kmap(new->bi_io_vec[i].bv_page);
        /* Encrypt your bio data */
        for (j = bv->bv_offset; j < bv->bv_offset + bv->bv_len; j+= SM4_BLOCK_SIZE) {
            sm4_do_crypt(key.rkey_enc, (u32 *)(p2 + j), (u32 *)(p1 + j));
        }
        kunmap(bv->bv_page);
        kunmap(new->bi_io_vec[i].bv_page);
    }
    generic_make_request(new);	//将新的bio而不是原始的bio提交给block层
    return 0;
}

读解密

相对于写操作,读IO就比较简单了,从磁盘中读取到了密文数据到page cache中,我们只需要在文件系统看到这块page cache前解密好,不需要倒腾内存,只需要原地解密,这里我们利用bio完成之后的callback,在其中进行原地解密之后再通知原始的bio。

遍历bio指向的内存数据:

void origin_bio_endio(struct bio *bio, int error);
void hook_bio_endio(struct bio *bio, int error)	//hook正常的bio callbak:bio_endio
{
    try_decrypt_bio(bio, error);	//将读bio中的内容原地解密
    origin_bio_endio(bio, error);   	//不需要加密的bio通过正常路径通知完成                                                                                                        
    return;
}
void try_decrypt_bio(struct bio *bio, int error)
{
    sm4_key_t key;
    int i, j;
    char buffer[SM4_BLOCK_SIZE];
    char *ptr = NULL;
    unsigned long flags;
    struct bio_vec *bv;

    if (bio_data_dir(bio) == WRITE)
        return;

    sm4_set_key1(&key, mykey, 16);

    bio_for_each_segment(bv, bio, i) {
        ptr = bvec_kmap_irq(bv, &flags);
        for (j = 0; j < bv->bv_len; j+= SM4_BLOCK_SIZE) {
            /* Decrypt ptr pointer buffer */
            sm4_do_crypt(key.rkey_dec, (u32 *)buffer, (u32 *)(ptr + j));
            memcpy(ptr + j, buffer, SM4_BLOCK_SIZE);
        }
        bvec_kunmap_irq(ptr, &flags);
    }   
}

上面只是一个简单的加解密文件系统,存在着诸多缺点。这时候加解密完全使用cpu来进行,速度肯定比较慢,最终的结果就是IO发送给磁盘的速度下降,瓶颈是在cpu这,可以考虑加个专用的片子进行加解密操作。另一个因为加解密操作和原来的bio流程是串行的,一条流水线多加了一段肯定会多耗一点实践,这时候可以考虑异步的加解密,也就是将加解密操作放在一个专用线程中,先将bio缓存下来慢慢加解密,另外用户对于读速度比较敏感,可以让读操作串行,写操作放在后台线程慢慢整吧。

参考

https://lwn.net/Articles/26404/
www.eeworld.com.cn/mp/ymc/a52704.jspx
drivers/md/dm-crypt.c

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值