2015年8月27日 星期四

error: manifest required for this command -- please run init

在做repo sync時,遇過一次這樣的問題 (error: manifest required for this command -- please run init),如果這個message有特別顯示在某一行的話,像是 (301/665),可能在checkout第301個project出問題,可以將那個project的目錄整個砍到,再重做一次repo sync,但是如果所有的project都fetch完了才出現這問題,也沒特別指明是哪個project,則可以把所有的project目錄砍掉(不要動到.repo目錄),再重做一次repo sync即可。.repo/project.list裡有所有project的列表。

2015年8月23日 星期日

Disk encryption process

在Android L裡,有兩種方式可以啟動Disk Encryption,一種是系統第一次開機時自動做encryption,這種方式是由OEM所設定,另一種則是經由Settings UI,這種方式是由end user決定是否要做encryption。當然,你也可以使用vdc直接要求vold將disk做encryption。目前Android只支援一個partition的encryption,也就是/data這個partition,/system partition的encryption目前並不支援,但其實要支援好像也不是什麼難事,只是有沒有這個需要而己。

並不是每個partition都可以做encryption,基本上,OEM必須在fstab裡對該partition標註forceencrypt或encryptable,該partition才能做encryption。如果標註forceencrypt的話,在Android第一次開機時就會將該partition做encrypt,如果是encryptable的話,則交由user決定。

針對被encrypt的partition,系統必須記錄一些資料(像是key相關資料)才能正常的管理它,這些資料稱meta data。Meta data可以存放在該Partition裡,也可以儲存在不同的partition。前者Android稱為Footer,因為這些資料是存放在該partition的最後16K,也就說user可使用的空間必須扣掉16K。

下面是fstab裡的UDA partition的描述,Android在第一次開機時,會強制對UDA這個partition做encrypt, meta data會存放在MD1這個partition。這個UDA partition的file system是f2fs

/dev/block/platform/sdhci-tegra.3/by-name/UDA  /data  f2fs  noatime,nosuid,nodev,errors=recover wait,check,forceencrypt=/dev/block/platforom/sdhci-tegra.3/by-name/MD1

forceencrpyt可以使用encryptable取代,encryptable代表這個partition可以被encrypt,但是不會在第一次開機就強制執行,而是交由使用者去選擇。如果OEM不想用另一個partition儲存meta data,可以用"footer"這個字(forceencrypt=footer),Android會將UDA partition的最後16K保留,用來存放meta data.

Service Class

在開始了解Encryption process之前,我們要先知道Init process,因為不管是Encryption Process或Encryption Booting,都跟Init Process有很大的關係。大部份用來Customize Init Process的設定都是寫入rc file。rc file可以定義Service,而每一個Service可以屬於某個class。在Android裡,定義了三種class,分別是core, main與late_start。一般來講, 屬於core class的service在啟動之後就不會被stop或restart,屬於main或late_start這兩個class的service則是有可能視情況需要而stop或restart。像是healthd, vold或surfaceflinger是屬於core class,所以除非這些service自己crash,Android並不會故意將它stop或restart,但是main與late_start這兩個class就不一定,像是encryption process或encryption booting就會將這兩個class停止或重啟。至於一個service應該定義在哪個class,則是應該視該service的用途來做判斷。以目前Android的設定,當main class啟動完成之後,重要的service都己經啟動了,user也可以看到launcher的畫面。稍後會看到encryption或encryption booting時為了要顯示UI,會先將main class啟動,而late_start則是在/data己經mount好之後才啟動。

Triggering Encryption & Encryption Booting

Encryption Process的觸發可分別兩種,一種是OEM在fstab裡指定forceencrypt參數,這是由Init Process在第一次開機(或factory reset後的第一次開機)後自動觸發。另一種則是開機後由App或User從Settings裡去觸發。不管是哪一種,最後Encryption都是由vold, init process與app/framework來共同完成。init process與vold及framework之間的溝通是透過property。

當init process執行mount_all時,它會fork一個process根據fstab內容去做mount,這是為了避免mount出現任何問題以致於init本身crash。child process做完mount之後就會結束,並將mount結果傳回給init process, init本身在fork之後則是會等child process結束才繼續。child process的結果有以下幾種

FS_MGR_MNTALL_DEV_NEEDS_ENCRYPTION
表示某個partition (其實就是指/data)被標註forceencryption,但是還沒被encrypt,init process會將vold.decrypt這個property內容設為trigger_encryption,表示要啟動Encryption process。

FS_MGR_MNTALL_DEV_MIGHT_BE_ENCRYPTED
表示Encryption process己經做過了,init process會設定下面幾個property的內容進行Encryption booting的程序

ro.crypto.state = encrypted
vold.decrypt = trigger_default_encryption


FS_MGR_MNTALL_DEV_NOT_ENCRYPTED
表示沒有任何一個partition被encrypt,而且也沒有任何一個partition被標註forceencrypt (有可能有標註encryptable)。這就是一般沒做partition encryption的開機流程。init process會將ro.crypto.state設為unencrypted,然後觸發nonencrypted的action。nonencrypted的action就是啟動main與late_start這兩個class的service。

< from rc file >
on nonencrypted
    class_start main
    class_start late_start

FS_MGR_MNTALL_DEV_NEEDS_RECOVERY
這表示child process沒辦法將partition正常mount上,有可能是encryption發生不正常的現象,init process必須wipe partition (將partition內容清除,也就是factory reset),系統會進入recovery mode。

除了由init process觸發的encryption (或factory reset)之外,也可以由Setting裡的CryptKeeperConfirm來觸發。User可以從Setting選擇要做disk encryption,最後會由CryptKeeperConfirm呼叫MountService.encryptStorage(),MountService再要求vold執行"cryptfs enablecrypto inplace "命令開始做disk encryption。

如果我們比較forceencrypt與user從Setting觸發的encryption command,可以發現一點差異,稍後看到encryption的過程再來討論。
  • forceencrypt - "cryptfs enablecrypto inplace default"
  • Settings - "cryptfs enablecrypto inplace "

Encryption Process

前面提到Encryption process可以由init process觸發,也可以由Settings觸發,兩者其都是要求vold去執行"cryptfs enablecrypto inplace" command,差別在於參數略有不同而己。

下圖是一張粗略的Encryption流程,過程大致了解再來深入幾個比較有意思的地方。星星標註的地方代表可以觸發Encryption,也就是可以從Settting與開機時由Init process觸發 (forceencrypt)。不管哪一個,都是下command到vold。Vold command分成幾個類別,cryptfs這一類用來處理跟encryption相關的。



vold在做encryption時主要是有下面幾個步驟
    1. shutdown framework
    2. unmount all asec
    3. unmount sdcard
    4. unmount /data
    5. mount tmpfs on /data
    6. initialize /data through init post_fs section
    7. set encryption progress property to 0
    8. Show progress UI
    9. encrypt each sector while updating progress property
   10. Reboot system when all sectors encrypted

Encryption的做法在概念上很簡單,以ext4來講,只要將/data這個partition的每個sector加密過就完成了,簡單講就是像下面這個樣子,也就是上面step 9。

for (int i = 0; i < sector_count; ++i) {
    encrypt(sector[i]);
}

之所以要有step1 ~ step8主要就是要避免有process正在使用/data,因為/data這個partition就是我們要做encryption的對像。另一個原因就是希望將Encryption的進度以UI的方式顯示,讓user知道系統還活著,Encryption會花點時間,Step 5~9主要就是為了UI的關係。

Step 1 - Shutdown framework
Shutdown framework是將vold.decrypt設為trigger_shutdown_framework,init process收到後會將main與late_start這兩個class services停止,這是為了避免framework或app正在使用/data partition。

Step 2 - Unmount all asec
Asec是android secure container,這是加密過的資料,當user將app移到sdcard時,系統會在sdcard上產生一個asec檔案,framework會將它mount在/data裡,如果不將asec unmount的話,最後/data也無法unmount,encryption就無法進行。

Step 3 - Unmount sdcard
Sdcard的access是透過FUSE的架構,有一個sdcard service用來處理sdcard files的access,基本上user process是access /storage/emulated/,透過bind mount的方式,這些access會被導引到/mnt/shell/emulated/,而這是/dev/fuse的一個mount point,因此這些access會由/dev/fuse的driver處理,而/dev/fuse會將這些access再導引至sdcard service。sdcard service會再將這些access導引到真正sdcard的mount point,也就是/data/media。因此,如果sdcard沒有unmount的話,/data也沒辦法unmount。

Step 4 - Unmount /data
做encryption前,/data必須被unmount,避免一邊使用一邊Encryption的事情發生。

Step 5 - Mount tmpfs on /data
tmpfs是用在RAM的file system,也就是割出一塊RAM,initialize成tmpfs format,並mount在/data,這主要是為了顯示UI做準備。目前顯示UI的方式是啟動framework,執行一個特殊的Launcher,它的用途就是顯示目前Encryption的進度,當然它還有其它用途,但以Encryption而言,這是它其中一個用途。可是framework的運作是需要/data存在,而且還必須有一些資料設定好,因此,vold才會產生一個暫時的/data供framework使用。

Step 6 - Initialize /data through post_fs
上面提到vold將一塊RAM mount在/data供framework使用,但是不是只要提供/data就好了,/data裡面還是要有一些設定,這些設定就由rc file的post_fs section來處理,因此,vold會將vold.decrypt設為trigger_post_fs_data,init process就會開始執行rc file的post_fs section,進而觸發其它section的命令。

Step 7 - Set encryption progress to 0
由於Encryption還沒開始,所以它的progress是0,這個progress要能讓framework app/launcher可以看到,目前是使用一個property (vold.encrypt_progress)當作彼此溝通的媒介。

Step 8 - Show progress UI
vold會將vold.decrypt設為trigger_restart_min_framework,init process會將main class services啟動,launcher也會被啟動,但被啟動的launcher是一個特殊的launcher,在Encryption Booting這個section,會有比較詳細的討論。簡單的講,這個Launcher會根據vold.encrypt_progress內容顯示encryption進度。

Step 9 - Encryption for each sector
這個步驅主要就是產生一個crypto device (dm-crypt driver),並將它map到原本的/data partition,把原本的/data partition的每個sector讀出來,寫到crypto device,dm-crypt driver會將寫入的sector做encryption,再寫入原來的/data partition相同的sector,這樣就完成了一個sector的encryption,每個sector都按照一樣的方式處理過就完成了encryption,稍後我們會再進一步討論這部份。在Encrypt每個sector的時候,vold也會持續更新vold.encrypt_progress內容,因此Launcher也可以讓user知道最新的進度。

Step 10 - Reboot system
當encryption完成後,vold會直接reboot system,它會留個幾秒讓Launcher顯示到100%才reboot system。


Encryption Password

在KK,password有兩種,一個是screen password,一個是encryption password,前者用來解除screen lock,後者用在開機時驗證encryption password。但是到的L的時候,這兩個password被合而為一。在KK,當user啟動disk encryption時,framework會要求user提供一組encryption password,但是在L的時候,就不再詢問user了,它會直接使用usr設定的screen password,如果user也沒設定screen password,就會直接用預設的password,可是當user更改screen password時,encryption password也會跟著更改。

有一個容易造成誤解的是encryption password並不是用來做為disk encryption的key。當disk encryption開始時,framework會產生一個key,這個key才是disk encryption時所要用的,我們稱為master key,user所輸入的password是用來加密master key而己。而加密後的master key會儲存在Meta data裡,因此,每次開機,framework都需要向user詢問encryption key,這樣它才能從meta data裡讀出加密過的key,再將它解密,然後交給dm-crypt,系統才能正常booting。


dm-crypt

dm-crypt是Linux device mapper的一種target,它可以做到transparent encryption/decryption,device mapping是一種mapping的架構,它會產生一個virtual device,並設定一個mapping table,所以當user在access這個virtual device時,它會將這個access轉介到另一個device,並根據mapping table改變要access的區域。以dm-crypt來講,假設真正儲存/data的device是B,我們會產生一個dm-crypt device,假設名字是dm-0,而mapping table會設定為dm-0的第N個sector對應到B的第N的sector。Disk encryption就會變成,將B的每個sector讀出,並寫入dm-0的每個sector。B的內容讀出後是還沒做過encryption的,但是寫到dm-0之後,dm-crypt會對寫入的資料做encryption,並寫入B的同樣sector,這樣一來,B的內容就是加密過的,如果這個時候我們直接從B再讀取一次,就會得到加密過的資料,但是如果我們透過dm-0去讀,dm-crypt會先讀B的內容,再將它解密,然後再回傳。如果我們寫入資料到dm-0,dm-crypt就會將寫入的資料加密,再寫到B的相同sector。所以,在encryption過的系統,/data是mount在dm-crypt的device,而不是原始的device,我們可以透過adb shell mount來觀察。

下面這段code是vold cryptfs的function,vold將dm-crypt的device產生出來後,會將它的path儲存在ro.crypto.fs_crypto_blkdev這個property裡,這個function再將它讀出,mount到/data下。

static int cryptfs_restart_internal(int restart_main) {
   ...
     property_get("ro.crypto.fs_crypto_blkdev", crypto_blkdev, "");
     if (strlen(crypto_blkdev) == 0) {
         SLOGE("fs_crypto_blkdev not set\n");
         return -1;
     }

     if (! (rc = wait_and_unmount(DATA_MNT_POINT, true)) ) {
         /* If that succeeded, then mount the decrypted filesystem */
         int retries = RETRY_MOUNT_ATTEMPTS;
         int mount_rc;
         while ((mount_rc = fs_mgr_do_mount(fstab, DATA_MNT_POINT,
                                            crypto_blkdev, 0))
                != 0) {
             ....
         }
         ....
     }
}


Encryption Booting

下面這張圖描述Encryption Booting的過程。Init process根據fstab在mount所有的partition時,如果disk己經被encrypt過,它會設定以下兩個property開始Encryption Booting。
  • ro.crypto.state = encrypted
  • vold.decrypt = trigger_default_encryption
trigger_default_encryption會啟動defaultcrypto這個service (one-shot service),而defaultcrypto則是純粹的使用vdc要求vold去執行"cryptfs mountdefaultencrypted"這個command。如果當初在做Encryption時,沒有指定password的話,vold這邊的動作比較單純,vold都會產生一個crypto device(dm-0),使用同樣的mapping table對應到userdata partition,並將dm-0 mount在/data目錄。因此,在Encryption之後,所有對/data partition的access都會在dm-0 device上,而dm-0則會將access導引之原本的data partition,並做encryption/decryption的動作。對user mode的process之言,並不會感覺有所變化,當然,performance或許有變,但使用上跟disk encryption之前沒有差別。

當crypto device被mount到/data後,vold會通知init process將persistent properties讀取進來(將vold.decrypt設為trigger_load_persistent_. Persistent properties (persist.*)是儲存在/data裡,因此,在Encryption Booting時,一直要等到/data被mount後才能將它讀出來。有一點要注意,這個時core class service都己經被啟動,如果core class service有用到persistent properties,它就讀不到真正的property內容。

接下來,vold會通知init process開始執行rc file裡post_fs這個section裡的command(將vold.decrypt設為trigger_post_fs_data),最後才要求init process啟動framework(將vold.decrypt設為trigger_restart_framework),而所謂啟動framework就是將main與late_start這兩個class裡的service啟動,main啟動完後,user就可以看到launcher的畫面了。




但是如果當初在做Encryption時,如果user有設定password的話,那狀況會比較複雜一點,因為必須將framework啟動,顯示UI詢問password。所以在vold收到cryptfs mountdefaultencrypted這個command時,它會要求init process先將main class service啟動(將vold.decrypt設為trigger_restart_min_framework)。前面有提到當main class啟動完成後,user就會看到Launcher畫面,也就是說user會看到UI詢問password,得到password後,UI會直接使用MountService.decryptStorage()來與vold確認password是否正確。decryptStorage()這裡會做兩件事,它會利用cryptfs checkpw這個command要求vold確認password是否正確,如果正確的的話,它會利用cryptfs restart這個command要求vold繼續Encryption Booting的程序。

這裡有一個比較tricky的地方,當main class啟動時,理論上user看到的第一個畫面是launcher,那為何在trigger_restart_min_framework時,會出現問user password的畫面? 原因在於這個系統裡其實至少有二個Launcher,一個是一般user使用的launcher,像是Launcher3或GoogleLauncher,另一個Launcher就是用來顯示詢問Password畫面,這個Launcher就是Settings app裡其中一個activity (CryptKeeper)




CryptKeeper有HOME這個category,所以它會被系統認定為Launcher類別,而且它的priority是10,高於一般的Launcher。因此,如果沒有特別的處理,CryptKeeper會被當做系統預設的Launcher,但事實上,除了在Encryption Booting時你會看到它詢問密碼,在其它時刻它並不會出現。原因在於這個Activity在第一次開機時就將自己本身Disable,所以framework會去使用第二順位的Launcher。但是為何它又能被執行顯示詢問password的畫面?因為當component被disable時,這個資料會儲存在/data裡,而framework被restart來顯示password晝面時,/data還沒被mount起來,因此這個時候相當於這個component從沒被disable過。

         final String state = SystemProperties.get("vold.decrypt");
         if (!isDebugView() && ("".equals(state) || DECRYPT_STATE.equals(state))) {
             // Disable the crypt keeper.
             PackageManager pm = getPackageManager();
             ComponentName name = new ComponentName(this, CryptKeeper.class);
             pm.setComponentEnabledSetting(name, PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
                     PackageManager.DONT_KILL_APP);
             // Typically CryptKeeper is launched as the home app.  We didn't
             // want to be running, so need to finish this activity.  We can count
             // on the activity manager re-launching the new home app upon finishing
             // this one, since this will leave the activity stack empty.
             // NOTE: This is really grungy.  I think it would be better for the
             // activity manager to explicitly launch the crypt keeper instead of
             // home in the situation where we need to decrypt the device
             finish();
             return;
         }


cryptfs restart這個command會要求vold將main class裡的service停止(reset main),因為這時候/data還沒mount起來,所以,可能有些service讀取到的設定並不正確(像Launcher就不是user所期待的),因此這個時候將main class reset,稍後等/data mount起來,再重新啟動main class才會讓main class的service讀取到正確的設定。reset完main class後,就跟前面提到的順序一樣,mount /data,讀取persistent properties,執行rc檔的post_fs命令,然後啟動framework。

Uncrypt (OTA Update)

在KK的時候,OTA package是download到/cache裡,因此在recovery mode時,只要從/cache讀出OTA package內容,就可以直接做OTA update。/cache並不會被encrypt,所以即使/data被encrypt,OTA update也不受影響。但是這會有一個問題,/cache的size必須夠大,要不然可能無法容納OTA package。OTA package內容主要就是/system的內容,因此,在最差的情況下,OTA package的大小可能會跟/system partiton的大小類似,其實這是有點浪費空間的,畢竟除了OTA package之外,/cache也用不了那麼多的空間。

在L之後,OTA package可以被放在/data裡,在recovery mode時再將它讀出做OTA update。這似乎沒什麼大問題,但其實有兩件事值得思索一下
     1. 由於OTA package放在/data,因此,recovery mode必須將/data mount起來。
     2. 如果/data被encrypt,recovery mode就得想辦法將/data decrypt才能讀出OTA package。

第一件事似乎沒什麼大不了,反正就mount /data就好,我想也是,但似乎Google不怎麼想在recovery mode將/data mount起來,或許有什麼原因我們不知道。第二件事就比較麻煩,因為,recovery mode必須知道encrypt key的內容並將dm-crypt的device mount起來,並設定好mapping table,但這些事都是在vold做的,Google大概不想再搬一份相同的code到recovery mode裡。因此,在L做OTA update時,它會產生一個map file放在/cache裡,把OTA package (zip file)所使用到的block記錄下來,內容類似下面這個樣子

      /dev/block/platform/msm_sdcc.1/by-name/userdata     # block device
      49652 4096                        # file size in bytes, block size
      3                                 # count of block ranges
      1000 1008                         # block range 0
      2100 2102                         # ... block range 1
      30 33                             # ... block range 2

因此,在做OTA update時,只要根據這個map file,直接讀取/data所屬的block device內容就可以了。但是/data如果被encrypt過怎麼辦?當系統要reboot到recovery mode之前,它會執行一個做uncrypt的service,這個service就是用來產生map file,如果/data是被encrypt的,它會先從/data將OTA package所使用的block讀出(這個時候/data是mount在dm-crypt device上,所以讀出來的內容是decrypt過的),再將讀出的block直接寫入原本/data所屬的block device的相同block。所以在recovery mode時,系統只要按照這個map file就可以讀出OTA package的內容,即使/data己經被encrypt過。

這樣還是有一個問題,它會造成OTA update在reboot到recovery mode前有一些delay,目前framework預設是5分鐘,但是,如果這個platform decrypt的速度不快的話,可能會造成uncrypt還沒處理完,系統就會被shutdown (因為ShutdownThread.rebootOrShutdown會呼叫PowerManagerService.lowLevelReboot來reboot系統,但是如果timeout的話,它會認為reboot失敗,它就會將系統shutdown),然後OTA update就會失敗。下面這段code是PowerManagerService.lowLelvelReboot,當它被要求要reboot到recovery mode,它會要求init process啟動uncrypt,並開始計時,它最多會等個5分鐘讓uncrypt把事情做完,如果做不完,系統會被shutdown。

     public static void lowLevelReboot(String reason) {
         if (reason == null) {
             reason = "";
         }
         long duration;
         if (reason.equals(PowerManager.REBOOT_RECOVERY)) {
             // If we are rebooting to go into recovery, instead of
             // setting sys.powerctl directly we'll start the
             // pre-recovery service which will do some preparation for
             // recovery and then reboot for us.
             //
             // This preparation can take more than 20 seconds if
             // there's a very large update package, so lengthen the
             // timeout.  We have seen 750MB packages take 3-4 minutes
             SystemProperties.set("ctl.start", "pre-recovery");
             duration = 300 * 1000L;
         } else {
             SystemProperties.set("sys.powerctl", "reboot," + reason);
             duration = 20 * 1000L;
         }
         try {
             Thread.sleep(duration);
         } catch (InterruptedException e) {
             Thread.currentThread().interrupt();
         }
     }

啟動uncrypt的方法是將sys.powerctl設為pre-recovery,在rc file裡,pre-recovery是一個oneshot service,它會直接執行uncrypt這個命令。uncrypt產生完map file後,它會直接reboot系統。

service pre-recovery /system/bin/uncrypt
    class main
    disabled
    oneshot