Android 中的dm-verity

Android 中的Verified Boot之dm-verity

之前做了一個Verified Boot模塊相關的工作,但是在網上只有找到google的文檔和一個nexus的patch。雖然有patch,但在不同版本的代碼上實現起來卻可能有一些bug,所以特此記錄一下debug這個東西的過程。之前debug的過程一直沒找到問題,歸根到底還是這個原理沒搞清楚就下手,所以我分成原理,接口和應用來說明dm-verity verified boot是怎麼去實現。其實接口部分也大多是原理,之所以說接口,是因爲我覺得裏面含有一些應用的成分。那麼本文使用的是6.0的android,4.1的kernel的代碼。

1.相關原理

相關的代碼
build/tools/releasetools/build_image.py
system/extras/verity/build_verity_tree.cpp
system/extras/verity/build_verity_metadata.py

Android中Verified Boot有分好多種,這裏只涉及到了dm-verity這個東西,所以直接從dm-verity那裏開始入手,至於bootloader等其他的就不多說。

爲什麼要使用dm-verity

Due to its large size, the system partition typically cannot be verified similarly to previous parts but must be verified as it’s being accessed instead using the dm-verity kernel driver or a similar solution.
Android官網上一段話就說明了這個問題,可見dm-verity的速度很快。

Dm-verity的工作流程

這裏寫圖片描述
Android 官網上有這麼一張圖片說明了dm-verity的流程。當bootloader的verify過了之後就進入到system分區等的verify,這個時候就是dm-verity出場的時候。此時就到了圖中start的這個問題,先判斷dm-verity是否是enforcing的狀態,如果是,就去掃描一把需要verity的分區,具體實現就是驗證一下這個對應分區的metadata。如果掃描出沒問題,ok,mount系統,如果掃描出有問題,dm-verity會向kernel發一個reboot的信號,並且將dm-verity的狀態設置成loggin。重啓之後,再回到start的地方,此時dm-verity的狀態已經是loggin了,所以走向紅色的那一塊區域,此時就會顯示出一個警告的界面,讓用戶去選擇mount還是不mount。後面的工作都會圍繞這張流程圖展開。在不同的板塊去分解這張流程圖的實現。

Dm-verity的實現

在瞭解dm-verity的工作流程之後,我們簡要的看一下dm-verity的實現。dm-verity實現的關鍵在於metadata是如何創建並且存儲的。在Android官網上定義了這麼一串步驟,如何去創建dm-verity所需要用的步驟。
- Generate an ext4 system image.
- Generate a hash tree for that image.
- Build a dm-verity table for that hash tree.
- Sign that dm-verity table to produce a table signature.
- Bundle the table signature and dm-verity table into verity metadata.
- Concatenate the system image, the verity metadata, and the hash tree.

如何實現Dm-verity
上面給出官網解釋瞭如何去實現這幾個步驟,偏原理的解釋,在這通過代碼來描述一下Dm-verity是如何去實現的。這些實現的步驟都在build_image.py 這個build腳本中。
Generate an ext4 system image

def BuildImage(in_dir, prop_dict, out_file, target_out=None):
    ......
    try:
       if reserved_blocks and fs_type.startswith("ext4"):
         (ext4fs_output, exit_code) = RunCommand(build_command)
       else:
         (_, exit_code) = RunCommand(build_command)
    ......

可以在build腳本中通過RunCommand(build_command)生成了ext4的文件系統。至於這個build_command是什麼有興趣的朋友可以去看一看build_image.py,比較長,這裏就不貼出來了。

Generate a hash tree for that image

def BuildVerityTree(sparse_image_path, verity_image_path, prop_dict):
      cmd = "build_verity_tree -A %s %s %s" % (
          FIXED_SALT, sparse_image_path, verity_image_path)
      print cmd
      status, output = commands.getstatusoutput(cmd)

在build腳本中有這麼一段是描述瞭如何去構建hashtree的。實際上,這棵hash tree的作用就是用來描述system.img的變化。

通過build_verity_tree 這個程序去構建hash tree,所以構建hash tree 的算法就在這個build_verity_tree.cpp 文件中, 那麼算法怎麼實現的,就不去管它了。有點複雜。至於使用hash tree的原因官網上也提到了,最初他使用hash table來實現,後來數據太大之後,效率不好,所以使用hash tree,hash tree在處理大量數據的時候效率就非常高。產生的hash tree的結構就如下所示, 最後只有一個根hash,葉節點就是dm-verity所需要verify的分區的劃分的一個個小塊,它這裏規定了每個塊以4k的大小來劃分。所以舉個例子,要驗證的system分區如果有800M,那麼就有200萬個塊。所以說通過葉節點以及他的父hash到根hash就是描述了system.img的變化情況。這樣的話用hash table來存效率就很差,所以使用hash tree來存速度更快。最後呢再主要保存這個root hash。

                             [   root    ]
                            /    . . .    \
                 [entry_0]                 [entry_1]
                /  . . .  \                 . . .   \
     [entry_0_0]   . . .  [entry_0_127]    . . . .  [entry_1_127]
       / ... \             /   . . .  \             /           \
 blk_0 ... blk_127  blk_16256   blk_16383      blk_32640 . . . blk_32767

Build a dm-verity table for that hash tree
Sign that dm-verity table to produce a table signature.
Bundle the table signature and dm-verity table into verity metadata

def BuildVerityMetadata(image_size, verity_metadata_path, root_hash, salt,
                        block_device, signer_path, key):
  cmd_template = (
      "system/extras/verity/build_verity_metadata.py %s %s %s %s %s %s %s")
  cmd = cmd_template % (image_size, verity_metadata_path, root_hash, salt,
                        block_device, signer_path, key)
  print cmd
  status, output = commands.getstatusoutput(cmd)
  return True

Ok,可以看到這段代碼最終調用到了build_verity_metadata.py這個腳本。看一下他是如何建立metadata的。正好三部曲根上面的三部對應起來。

def build_verity_metadata(data_blocks, metadata_image, root_hash,
                            salt, block_device, signer_path, signing_key):
    # build the verity table
    verity_table = build_verity_table(block_device, data_blocks, root_hash, salt)
    # build the verity table signature
    signature = sign_verity_table(verity_table, signer_path, signing_key)
    # build the metadata block
    metadata_block = build_metadata_block(verity_table, signature)
    # write it to the outfile
    with open(metadata_image, "wb") as f:
        f.write(metadata_block)

看一下如何建立verity-table。實際上,verity-table就是爲了去描述之前生成的hash tree。

def build_verity_table(block_device, data_blocks, root_hash, salt):
    table = "1 %s %s %s %s %s %s sha256 %s %s"
    table %= (  block_device,
                block_device,
                BLOCK_SIZE,
                BLOCK_SIZE,
                data_blocks,
                data_blocks,
                root_hash,
                salt)
    return table

所以建立起來的verity table形如下面這樣。說白了,verity-table只是一個描述hashtree的字符串,看一看他是如何描述hash tree的。下面這個例子選自linux 文檔,並非實際的system.img的hash tree的verity table。
第一個參數是版本,只有0和1,大多數情況下填1,不去深究。
第二個,第三個參數描述的是所保護的分區,這個例子中dm-verity保護的分區是/dev/sda1。
第四,第五個參數描述的該分區的每個block即hash tree的葉節點的大小,可以看到這裏是4k,就是說,以4k爲大小劃分/dev/sda1爲若干個區域。
第六,第七個參數描述了總共有多少個block,也就是hash tree有多少個葉節點。
第八個參數是hash 加密算法,這裏使用sha256算法。
第九個參數是hash tree的根hash。
第十個參數是加密算法加的鹽。
Ok,到這我們可以看到verity-table描述了葉節點和根hash以及hash的算法等。這樣就通過一個字符串就把整棵樹的形狀就描繪出來了。

1 /dev/sda1 /dev/sda1 4096 4096 262144 262144 sha256 4392712ba01368efdf14b05c76f9e4df0d53664630b5d48632ed17a137f39076 
1234000000000000000000000000000000000000000000000000000000000000

verity table建立完後,對他進行簽名,簽名的格式可以參看Implementing verify boot那一章。簽完名就把verity-table,簽名信息和hash tree 一同寫入到metadata中,最後返回給build腳本。

Concatenate the system image, the verity metadata, and the hash tree.

def BuildVerifiedImage(data_image_path, verity_image_path,
                       verity_metadata_path):
  if not Append2Simg(data_image_path, verity_image_path,
                     "Could not append verity tree!"):
    return False
  if not Append2Simg(data_image_path, verity_metadata_path,
                     "Could not append verity metadata!"):
    return False
  return True

最後通過append2img這個命令吧metadata.img和metadata_verity.img附在system.img後面,這樣就算是實現了dm-verity的metadata建立的過程。

至此,還未分析到流程圖的具體流程的實現,那麼在下一個段落開始就會去看一下流程圖的具體實現。

2.接口

相關的代碼
/kernel/include/upai/linux/dm-ioctl.h
/kernel/include/linux/device-mapper.h
/kernel/driver/md/dm-verity.c
/kernel/driver/md/dm-table.c
/kernel/dirver/md/dm-ioctl.c
/system/core/fs_mgr/fs_mgr_verity.c

Deveice Mapper框架

要知道如何使用dm-verity,先需要了解一下Device mapeer框架,device mapper爲何物呢?Device mapper 是 能讓用戶自己定製管理塊設備的策略的一套框架,使用這套框架去寫管理塊設備的策略比直接去管理塊設備肯定要輕鬆許多。這裏就不多講Device Mapper原理。下面這篇文章對Device Mapper做了詳細的分析。下面僅就簡單介紹一下一些需要在後面實現verified boot中需要用到的接口。
Linux 內核中的 Device Mapper 機制

一些重要的結構體
我們知道Device Mapper對用戶控件提供的接口就是文件系統的接口,open,read,write啊什麼的,和ioctl接口。這裏就羅列幾個ioctl的命令可以看一下。其他都類似。

    ......
    DM_VERSION //獲取device Mapper的版本
    DM_TABLE_LOAD //向device mapper中load一個設備
    DM_TABLE_STATUS //獲取device mapper的狀態
    ......

dm_ioctl這個結構體定義在dm-ioctl.h中。截取幾個字段可以看一下,他主要是用於ioctl的時候傳入給內核的參數的集合。其中io就是一個dm-ioctl的對象。用戶空間使用ioctl的步驟是,先創建一個dm-ioctl和dm_target_spec對象,然後配置一下他們的參數,然後在dm_target_spec後面跟一個特定設備的特定參數(special param),將三者結合到dm-ioctl上,通過調用一下命令就可以在device mapper中load一個dm設備了。

ioctl(fd, DM_TABLE_LOAD, io)
struct dm_ioctl {
    __u32 version[3];   
    __u32 data_size;    
    __u32 data_start;   
    __u32 target_count; /* in/out */
    char name[DM_NAME_LEN]; /* device name */
    char uuid[DM_UUID_LEN]; /* unique identifier for
                 * the block device */
    char data[7];       /* padding or data */
};

那麼問題來了,ioctl是向device mapper發命令,如何去找到具體的dm設備呢。這個時候另外一個結構就出場了。同樣定義在dm-ioctl.h中。

struct dm_target_spec {
    __u64 sector_start;
    __u64 length;
    __s32 status;   
    char target_type[DM_MAX_TYPE_NAME];
};

可以看到他有四個字段,那最重要的是target_type[DM_MAX_TYPE_NAME]這個字符串,device mapper就是通過這個字符串從他的設備表中找到相應的dm設備。舉個例子,我們這裏需要找的是dm-verity,那麼這個字符串就是”verity”。那麼這個字符串必須是這個嘛?不是的,後面會講這個字符串的起源地。

target_type 定義在include/linux/device-mapper.h 中,這個結構體就代表一個真實的dm設備。也就是說dm設備最終就是去實現這個接口就ok了,那麼對於開發一個dm設備就非常簡單了,只需要去實現這個接口,並向device mapper框架註冊一把就可以了。

    struct target_type {
        uint64_t features;
        const char *name;
        struct module *module;
        unsigned version[3];
        dm_ctr_fn ctr;
        dm_dtr_fn dtr;
        ......
        dm_status_fn status;
        ......
        /* For internal device-mapper use. */
        struct list_head list;
    };

他裏面羅列了許多字段,這裏就列出了幾個字段。ctr就是create函數,dtr:destory,跟Activity中的oncreate和ondestory簡直一模一樣。其中的name字段就是對應到之前dm_target_spec 的target_type字段。也就是說這個設備的名字的發源地就是在這。你這定義了“a”,那麼在用戶空間傳參數的時候target_type就定義成“a”。

Device mapper 框架下的dm-verity驅動實現

之前簡要的介紹了一下Device mapper一些結構體和ioctl的使用,那接下來看看dm-verity在Device Mapper下是如何實現的。其實就是實現一個target_type 這個接口就行了
/kernel/driver/md/dm-verity.c

static struct target_type verity_target = {
        .name       = "verity",
        .version    = {1, 2, 0},
        .module     = THIS_MODULE,
        .ctr        = verity_ctr,
        .dtr        = verity_dtr,
        .map        = verity_map,
        .status     = verity_status,
        .ioctl      = verity_ioctl,
        .merge      = verity_merge,
        .iterate_devices = verity_iterate_devices,
        .io_hints   = verity_io_hints,
    };

    static int __init dm_verity_init(void)
    {
        int r;
        r = dm_register_target(&verity_target);
        if (r < 0)
            DMERR("register failed %d", r);
        return r;
    }

果然,在verity_target 中name字段定了成了verity,所以我們在用戶空間設置dm_target_spec的target_type字段的時候就設置成verity。後面還要涉及到的一個重要的接口就是ctr,他在ioctl load table的時候會去調用到這個接口。當然這個verity_target 對象是在內核模塊初始化的時候就被註冊到device mapper中。

那麼dm-verity的mode的改變是在什麼時候進行呢?首先要知道dm-verity有三種工作模式,如下:

EIO:不掛載被破壞的分區。0
LOGGIN:忽略破壞的分區,繼續掛載該分區。1
RESTART:發現分區被破壞,直接重啓系統。2

那麼暫且不談用戶空間如何去對dm-verity進行控制,先看在kernel裏dm-verity狀態的改變。dm-verity狀態的改變是在dm-verity被load的時候發生的,dm-verity load一個dm設備的時候會調用該設備的target type的crt接口。在這裏整理下dm-verity的調用流程。ioctl DM_TABLE_LOAD下去,調用到device mapper,device mapper通過傳下來的的參數找到dm-verity,最終調用到ctr接口。

ioctl(fd, DM_TABLE_LOAD, io)—–>Device Mapper—通過target_type=“verity”–>dm-verity.ctr()
截取部分代碼來看一看dm-verity中crt接口的實現。

static int verity_ctr(struct dm_target *ti, unsigned argc, char **argv)
{
    /* Optional parameters */
    if (argc) {
        as.argc = argc;
        as.argv = argv;

        r = dm_read_arg_group(_args, &as, &opt_params, &ti->error);
        if (r)
            goto bad;

        while (opt_params) {
            opt_params--;
            opt_string = dm_shift_arg(&as);
            if (!opt_string) {
                ti->error = "Not enough feature arguments";
                r = -EINVAL;
                goto bad;
            }

            if (!strcasecmp(opt_string, DM_VERITY_OPT_LOGGING))
                v->mode = DM_VERITY_MODE_LOGGING;
            else if (!strcasecmp(opt_string, DM_VERITY_OPT_RESTART))
                v->mode = DM_VERITY_MODE_RESTART;
            else {
                ti->error = "Invalid feature arguments";
                r = -EINVAL;
                goto bad;
            }
        }

}

在上述代碼之前,在crt函數裏會先處理之前生成的verity-table,驗證其有效性,具體可以參考源代碼中完整的代碼。那麼從上述代碼可以看到,dm-verity狀態v->mode的改變是由傳進去的特定參數決定,之前就講到用戶空間對dm-verity進行控制的話需要傳三個東西進去,一個dm-ioctl,一個是dm_target_spec, 還有一個就是特定設備的特定參數,這個特定參數在這裏就派上用場了。那麼這個參數該如何定義呢,我們可以看一看dm-verity文檔中的定義。

Construction Parameters
=============================================
    <version> <dev> <hash_dev>
    <data_block_size> <hash_block_size>
    <num_data_blocks> <hash_start_block>
    <algorithm> <digest> <salt>
    [<#opt_params> <opt_params>]

在opt_params之前就是之前所提到的由build腳本生成的verity table正好能對起來。再來看一遍verity-table。

1 /dev/sda1 /dev/sda1 4096 4096 262144 262144 sha256 4392712ba01368efdf14b05c76f9e4df0d53664630b5d48632ed17a137f39076 
1234000000000000000000000000000000000000000000000000000000000000

那麼#opt_params是什麼呢,#opt_params代表在verity table 後面需要跟的參數的個數,也就是argc,opt_params代表後面跟的參數,也就是argv,後面的參數有許多種,這裏有可選參數的詳細介紹
根據kernel裏crt函數中的實現看到,dm-verity對於opt_params一個一個遍歷,然後做出相應的控制。dm-verity的mode是根據傳進來的參數設置的。在dm-verity也只有在ctr這個地方纔能去設置dm-verity的狀態。當然如果不設置,他的默認mode是0,也就是EIO模式。

 if (!strcasecmp(opt_string, DM_VERITY_OPT_LOGGING))
                v->mode = DM_VERITY_MODE_LOGGING;
            else if (!strcasecmp(opt_string, DM_VERITY_OPT_RESTART))
                v->mode = DM_VERITY_MODE_RESTART;

再看一下dm-verity中對應三種狀態對是如何工作的。當dm-verity掃描出分區有損壞的時候,會觸發dm-verity.c中一個回調函數–verity_handle_err。同樣截取部分代碼來看一看這個回調函數裏如何處理不同的狀態。

static int verity_handle_err(struct dm_verity *v, enum verity_block_type type,
                 unsigned long long block){
    out:
    if (v->mode == DM_VERITY_MODE_LOGGING)
        return 0;

    if (v->mode == DM_VERITY_MODE_RESTART)
        kernel_restart("dm-verity device corrupted");

    return 1;
}

可以看到,如果是默認狀態,就返回1,返回1之後,系統在mount的那邊的代碼就會不去掛載該被破壞的分區。如果是DM_VERITY_MODE_LOGGING的狀態1,就返回0,那麼系統在mount那塊的代碼就會忽略破壞,繼續掛載該分區。如果是DM_VERITY_MODE_RESTART,就是狀態2, 那麼就重啓系統。OK,至此就可以看到dm-verity中對於三種狀態如何處理了。
OK,這裏就解釋了流程圖這兩段邏輯是如何實現的,圖1中dm-verity在處於restart模式發生corruption之後重啓的邏輯對應於

 if (v->mode == DM_VERITY_MODE_RESTART)
        kernel_restart("dm-verity device corrupted");

這裏寫圖片描述
圖2中dm-verity在處於loggin模式發生corruption之後忽略corruption繼續掛載系統的邏輯對應於

  if (v->mode == DM_VERITY_MODE_LOGGING)
      return 0;

這裏寫圖片描述
默認的EIO模式沒在這張流程圖裏看到,顯然google沒有推薦使用這種模式。其實我覺得這種模式確實沒什麼用,這種模式在發生corruption之後直接選擇不掛載,這樣會導致系統hang住,也沒什麼意義。

用戶空間Dm-verity的使用

知道了dm-verity的相關接口後,就可以在用戶空間對他進行初始化了。在Android中fs_mgr_verity負責用戶空間對Dm-verity進行控制的,其中有個load_verity_table這個函數展示瞭如何從用戶空間去使用dm-verity。先貼出代碼。

static int load_verity_table(struct dm_ioctl *io, char *name, char *blockdev, int fd, char *table)
{
    char *verity_params;
    char *buffer = (char*) io;
    uint64_t device_size = 0;

    if (get_target_device_size(blockdev, &device_size) < 0) {
        return -1;
    }

    verity_ioctl_init(io, name, DM_STATUS_TABLE_FLAG);

    struct dm_target_spec *tgt = (struct dm_target_spec *) &buffer[sizeof(struct dm_ioctl)];

    // set tgt arguments here
    io->target_count = 1;
    tgt->status=0;
    tgt->sector_start=0;
    tgt->length=device_size/512;
    strcpy(tgt->target_type, "verity");

    // build the verity params here
    verity_params = buffer + sizeof(struct dm_ioctl) + sizeof(struct dm_target_spec);

 if (mode == VERITY_MODE_EIO) {
        // allow operation with older dm-verity drivers that are unaware
        // of the mode parameter by omitting it; this also means that we
        // cannot use logging mode with these drivers, they always cause
        // an I/O error for corrupted blocks
        strcpy(verity_params, table);
    } else {
        char *modeStr = mode == VERITY_MODE_LOGGING ? "ignore_corruption" : "restart_on_corruption";
        if (snprintf(verity_params, bufsize, "%s %d %s", table, 1, modeStr) < 0) {
            return -1;
        }
    }


    // set next target boundary
    verity_params += strlen(verity_params) + 1;
    verity_params = (char*) (((unsigned long)verity_params + 7) & ~8);
    tgt->next = verity_params - buffer;

    // send the ioctl to load the verity table
    if (ioctl(fd, DM_TABLE_LOAD, io)) {
        ERROR("Error loading verity table (%s)", strerror(errno));
        return -1;
    }

    return 0;
}

static int resume_verity_table(struct dm_ioctl *io, char *name, int fd)
{
    verity_ioctl_init(io, name, 0);
    if (ioctl(fd, DM_DEV_SUSPEND, io)) {
        ERROR("Error activating verity device (%s)", strerror(errno));
        return -1;
    }
    return 0;
}

根據之前講的,在device mapper框架下使用dm設備首先要創建一個dm-ioctl對象,並且初始化了這個對象。這個函數就在fs_mgr_verity.c 中可以找到。

    verity_ioctl_init(io, name, DM_STATUS_TABLE_FLAG);

緊接着創建一個dm_target_spec對象,把它append在dm-iotcl的後面,初始化一下他的字段,其中可以看到將target_type設置成verity,這樣通過ioctl就可以在device mapper框架下找到dm-verity這個設備了。

struct dm_target_spec *tgt = (struct dm_target_spec *) &buffer[sizeof(struct dm_ioctl)];

    // set tgt arguments here
    io->target_count = 1;
    tgt->status=0;
    tgt->sector_start=0;
    tgt->length=device_size/512;
    strcpy(tgt->target_type, "verity");

這個時候已經完成了ioctl dm-verity的兩部曲了,最後一步就是加上特定設備的特定參數,在這裏就是dm-verity需要的參數。將verity table,opt_params和mode值拼接給
verity_params,就是之前講的在dm-verity的kernel中的特定設備的參數。

    char *modeStr = mode == VERITY_MODE_LOGGING ? "ignore_corruption" : "restart_on_corruption";
        if (snprintf(verity_params, bufsize, "%s %d %s", table, 1, modeStr) < 0) {
            return -1;
        }

我在這就不詳細介紹了,那麼我後面需要用到兩個參數,一個是ignore_corruption,還有一個restart_on_corruption,這兩個值分別代表了之前提到的dm-verity的loggin mode 和 enforcing mode/restart mode。這樣我們就可以在用戶空間控制dm-verity的運行模式了。這兩種不同的參數可以對應到之前kernel裏dm-verity ctr中的相應邏輯。
至此,就解釋瞭如何在用戶空間設置dm-verity,也就解釋了流程圖中Setup dm-verity這段邏輯
這裏寫圖片描述

3.實現

相關的代碼
/system/extras/slideshow/slideshow.cpp
/system/core/fs_mgr/fs_mgr_verity.c
/device/(vendor)/init.rc
/device/(vendor)/fstab.vendor
/system/core/init/builtins.c

有了之前的原理和接口的介紹後,如何去實現就變得非常簡單了。AOSP中有一個patch就是去enable它的,當時就是直接使用了這個patch,但是由於之前原理不清楚,加上kernel跟android代碼對不上號,導致調試的時候出了一些bug,也不知道怎麼搞,所以還是要把官網的文檔看熟練,再去實現就很簡單了。那這裏我就以這個patch來描述一下這個事是如何去實現那張流程圖的。先貼出這個patch關鍵的代碼。後面一段段解釋相關的代碼。

diff --git a/device.mk b/device.mk
index 3e14931..fe5ed92 100644
--- a/device.mk
+++ b/device.mk
@@ -336,6 +336,10 @@
 PRODUCT_SYSTEM_VERITY_PARTITION := /dev/block/platform/msm_sdcc.1/by-name/system
 $(call inherit-product, build/target/product/verity.mk)

+PRODUCT_PACKAGES += \
+    slideshow \
+    verity_warning_images
+
 # setup scheduler tunable
 PRODUCT_DEFAULT_PROPERTY_OVERRIDES += \
     ro.qualcomm.perf.cores_online=2
diff --git a/fstab.shamu b/fstab.shamu
index 4a9989a..f07daf8 100644
--- a/fstab.shamu
+++ b/fstab.shamu
@@ -3,7 +3,7 @@
 # specify MF_CHECK, and must come before any filesystems that do specify MF_CHECK
 #
 #<src>                                                <mnt_point>  <type>  <mnt_flags and options>                     <fs_mgr_flags>
-/dev/block/platform/msm_sdcc.1/by-name/system         /system      ext4    ro,barrier=1                                wait
+/dev/block/platform/msm_sdcc.1/by-name/system         /system      ext4    ro,barrier=1                                wait,verify=/dev/block/platform/msm_sdcc.1/by-name/metadata
 /dev/block/platform/msm_sdcc.1/by-name/userdata    /data        ext4    rw,nosuid,nodev,noatime,nodiratime,noauto_da_alloc,nobarrier    wait,check,formattable,forceencrypt=/dev/block/platform/msm_sdcc.1/by-name/metadata
 /dev/block/platform/msm_sdcc.1/by-name/cache       /cache       ext4    rw,noatime,nosuid,nodev,barrier=1,data=ordered   wait,check,formattable
 /dev/block/platform/msm_sdcc.1/by-name/modem       /firmware    ext4    ro,barrier=1,context=u:object_r:firmware_file:s0    wait
diff --git a/init.shamu.rc b/init.shamu.rc
index 3513760..f6ebbf7 100644
--- a/init.shamu.rc
+++ b/init.shamu.rc
@@ -25,6 +25,9 @@
     chown system system /sys/kernel/debug/kgsl/proc

 on init
+    # Load persistent dm-verity state
+    verity_load_state
+
     mkdir /oem 0550 root root

     # Set permissions for persist partition
@@ -60,6 +63,12 @@
     setprop persist.data.wda.enable true
     setprop persist.data.df.agg.dl_pkt 10
     setprop persist.data.df.agg.dl_size 4096
+
+    # Adjust parameters for dm-verity device
+    write /sys/block/dm-0/queue/read_ahead_kb 2048
+
+    # Update dm-verity state and set partition.*.verified properties
+    verity_update_state

 on post-fs-data
     mkdir /tombstones/modem 0771 system system
@@ -568,6 +577,9 @@
 on property:vold.decrypt=trigger_reset_main
     stop gnss-svcd

+on verity-logging
+    exec u:r:slideshow:s0 -- /sbin/slideshow warning/verity_red_1 warning/verity_red_2
+

先看patch中fstab一段,在system分區的fs_mgr_flags中添加
verify=/dev/block/platform/msm_sdcc.1/by-name/metadata,那麼這個path表示什麼呢?這個位置用於保存dm-verity的mode。也就是在流程圖中,dm-verity重啓之後怎麼知道要進入loggin mode 的,就是將這個值存在了這個metadata分區中,metadata分區並不是之前所說的metadata.img,metadata.img是append在system.img之後的,也就是metadata.img是在system分區下,需要注意

-/dev/block/platform/msm_sdcc.1/by-name/system         /system      ext4    ro,barrier=1                                wait
+/dev/block/platform/msm_sdcc.1/by-name/system         /system      ext4    ro,barrier=1                                wait,verify=/dev/block/platform/msm_sdcc.1/by-name/metadata

fstab的這個patch的工作原理非常簡單,如果不加這個地址的話,那麼dm-verity會一直工作在EIO狀態下,在這個狀態下,如果dm-verity檢測出分區有毛病,他就不會掛載分區,不會重啓和忽略崩潰的分區。那麼我們就來看一看如果不加地址他爲什麼只會工作在EIO狀態下,導致流程不會按照既定的流程進行呢?這個不屬於這個patch之內實現的東西,但其實也是實現dm-verity流程圖相當重要的一部分,所以還是拿出來分析一下。真正設置dm-verity工作模式的代碼在fs_mgr_verity.c中的fs_mgr_setup_verity這個函數。不貼出所有代碼了,只截取一些關鍵的代碼來分析一下。

   if (load_verity_state(fstab, &params.mode) < 0) {
        /* if accessing or updating the state failed, switch to the default
         * safe mode. This makes sure the device won't end up in an endless
         * restart loop, and no corrupted data will be exposed to userspace
         * without a warning. */
        params.mode = VERITY_MODE_EIO;
    }

進到load_verity_state函數中看一看

static int load_verity_state(struct fstab_rec *fstab, int *mode)
{
    return read_verity_state(fstab->verity_loc, offset, mode);
}

再往下調用到read_verity_state,可以看到第一個參數fstab->verity_loc,這個值就是之前從fstab裏解析出來的verity後面的地址。如果fstab裏沒有verity或者verity後面滅有相應地址,那麼這個值是null。

static int read_verity_state(const char *fname, off64_t offset, int *mode)
{
    fd = TEMP_FAILURE_RETRY(open(fname, O_RDONLY | O_CLOEXEC));
    if (fd == -1) {
        ERROR("Failed to open %s (%s)\n", fname, strerror(errno));
        goto out;
    }
    if (TEMP_FAILURE_RETRY(pread64(fd, &s, sizeof(s), offset)) != sizeof(s)) {
        ERROR("Failed to read %zu bytes from %s offset %" PRIu64 " (%s)\n",
            sizeof(s), fname, offset, strerror(errno));
        goto out;
    }
    *mode = s.mode;
    rc = 0;

out:
    if (fd != -1) {
        close(fd);
    }

    return rc;

}

可以看到如果路徑不存在,直接報錯,返回-1,此時load_verity_state(fstab, &params.mode)就小於0,此時mode就設置爲0。如果路徑存在,ok,從中讀取mode的值。
在回過頭來看fs_mgr_setup_verity函數中拿到這個mode後幹了些什麼。

INFO("Enabling dm-verity for %s (mode %d)\n", mount_point, params.mode);

    // load the verity mapping table
    if (load_verity_table(io, mount_point, verity.data_size, fd, &params,
            format_verity_table) == 0) {
        goto loaded;
    }

這時候load完mode之後就會走到上面這段代碼中,可以看到最終調用到了load_verity_table這個函數。Ok, 這個函數我們之前在接口一章中提到了,去初始化dm_ioctl,dm_target_type兩個結構體,並且綁定一個verity_params然後通過一個load table的ioctl命令傳給kernel,去設置kernel中dm-verity的工作狀態。kernel中dm-verity在handle error的時候就會根據不同的狀態作出不同的動作。在上一個段落中已經分析了dm-verity.c中dm verity的ctr函數和handle error函數根據不同的mode如何工作的實現。再一次詳細的解釋瞭如何在用戶空間實現流程圖的這段邏輯。
這裏寫圖片描述

接着看init.rc中的patch。

on init
+    # Load persistent dm-verity state
+    verity_load_state

on fs   
+     # Update dm-verity state and set partition.*.verified properties
+    verity_update_state

+on verity-logging
+    exec u:r:slideshow:s0 -- /sbin/slideshow warning/verity_red_1 warning/verity_red_2
+

第一段是在 init這個action中去調用這個verity_load_state,這個命令最終會調用到中builtins.cpp中的do_verity_load_state,裏面去判斷dm-verity的mode是不是loggin,如果是loggin in就會觸發verity-logging這個action。這個action調用了slideshow程序。不熟悉的可以看一看init.rc的語法和init進程相關的原理。如果不是那麼就走其他的流程。
看代碼實現

static int do_verity_load_state(const std::vector<std::string>& args) {
    int mode = -1;
    int rc = fs_mgr_load_verity_state(&mode);
    if (rc == 0 && mode != VERITY_MODE_DEFAULT) {
        ActionManager::GetInstance().QueueEventTrigger("verity-logging");
    }
    return rc;
}

那麼do_verity_load_state完成的就是調用的fs_mgr_verity.c中的函數。這個函數就是根據fstab裏那個地址結合一些pstore的狀態去讀取並設置dm-verity的工作模式。這段代碼比較簡單,不對他進行分析了。這段代碼就解釋了流程圖中第一個判斷dm-verity工作模式的判斷邏輯。拋開具體代碼,就看init.rc中的verity_load_state目的就是爲了實現它
這裏寫圖片描述
那麼之前講到trigger了verity-logging這個action,最後他就去調用到了slideshow 這個程序,那麼其實也就是實現了上面這個判斷框爲N的時候的路徑。通過打開slideshow這個app來警告用戶system分區已經被破壞,是否要設置loggin模式繼續掛載。
這裏寫圖片描述

那麼執行的slideshow程序的功能就是顯示兩幅警告的圖片,一副用來提示你讓你選擇要不要繼續mount文件系統,另一幅提示你你已經忽略警告了。其實這個slideshow只能算一個皮包公司,原生代碼裏面基本是什麼都不幹,就顯示了兩幅圖片,但是廠商可以定製這個app,起到一些控制的效果,或者直接替換掉這個slideshow app,直接去實現一個類似recovery的界面都沒問題。有興趣的朋友可以去看一看/system/extras/slideshow/slideshow.cpp 是怎麼寫的,非常簡單,就一個文件,代碼也就100多行。slideshow最後實現效果圖如下面這樣。也就是對應與上面這段流程中的具體實現。
這裏寫圖片描述

dm-verity的verified boot的流程就分析到這,還有許多不完善的地方,也有許多地方都略過代碼分析,主要要把所有相關源代碼拿出來分析的話,篇幅就太長,而且太過於繁瑣,只是說大致的分析了了一遍dm-verity的那張流程圖的實現,當然真要透徹的理解dm-verity的話肯定還是需要去仔細的研究dm-verity和fs_mgr的代碼,在每一個段落的開頭貼出了相關的代碼文件可以參考。這也算是工作到現在唯一一次碰到的沒有java的關於Android的代碼了,紀念一把。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章