本文剖析根据Android T(13)

fdsan,全名为file descriptor sanitizer,是Android中的一种检测东西,用于检测fd的use-after-close和double-close过错。这两个过错会给设备留下安全漏洞,甚至造成数据走漏等安全问题。可是现实情况是这两种过错非常隐蔽,且难以排查。这才催生了fdsan的诞生。

fdsan由Google的工程师Josh Gao开发。最初在Android 10中引进,检测到过错时会打印log并持续运行。Android 11更改了运行模式,一旦检测到过错就当即abort,让问题露出得更加夺目。

要让这些问题能够被检测,关键是要构建fd的所有权体系。这套体系的建立有两个重要前提:

  1. Android中大多数fd并非直接经过open/close的原生接口进行办理,而是经过unique_fd,FileOutputStream,ParcelFileDescriptor之类的封装方式。
  2. fd是一个整数,且每个进程所能创立的fd都有上限,前期为1024,现在为32768。因而即便为每个fd创立独自的所有权办理数据也不会耗费多少空间。

根据这两个条件,fdsan运用一个64-bit的tag来标明一个fd由生到死的所有者。

Android Native | fdsan概述
tag由两部分组成,最高位的8-bit构成type,后面的56-bit构成value。Type表明fd经过何种封装方式进行办理,比如ANDROID_FDSAN_OWNER_TYPE_UNIQUE_FD就表明fd经过unique_fd进行办理。关于应用工程师而言,他们接触到的大多为Java/Kotlin代码,因而Java中常用的FileInputStream/FileOutputStream/ParcelFileDescriptor等封装方式也有相应的type。具体列表如下。

enum android_fdsan_owner_type {
  /*
   * Generic Java or native owners.
   *
   * Generic Java objects always use 255 as their type, using identityHashCode
   * as the value of the tag, leaving bits 33-56 unset. Native pointers are sign
   * extended from 48-bits of virtual address space, and so can have the MSB
   * set to 255 as well. Use the value of bits 49-56 to distinguish between
   * these cases.
   */
  ANDROID_FDSAN_OWNER_TYPE_GENERIC_00 = 0,
  ANDROID_FDSAN_OWNER_TYPE_GENERIC_FF = 255,
  /* FILE* */
  ANDROID_FDSAN_OWNER_TYPE_FILE = 1,
  /* DIR* */
  ANDROID_FDSAN_OWNER_TYPE_DIR = 2,
  /* android::base::unique_fd */
  ANDROID_FDSAN_OWNER_TYPE_UNIQUE_FD = 3,
  /* sqlite-owned file descriptors */
  ANDROID_FDSAN_OWNER_TYPE_SQLITE = 4,
  /* java.io.FileInputStream */
  ANDROID_FDSAN_OWNER_TYPE_FILEINPUTSTREAM = 5,
  /* java.io.FileOutputStream */
  ANDROID_FDSAN_OWNER_TYPE_FILEOUTPUTSTREAM = 6,
  /* java.io.RandomAccessFile */
  ANDROID_FDSAN_OWNER_TYPE_RANDOMACCESSFILE = 7,
  /* android.os.ParcelFileDescriptor */
  ANDROID_FDSAN_OWNER_TYPE_PARCELFILEDESCRIPTOR = 8,
  /* ART FdFile */
  ANDROID_FDSAN_OWNER_TYPE_ART_FDFILE = 9,
  /* java.net.DatagramSocketImpl */
  ANDROID_FDSAN_OWNER_TYPE_DATAGRAMSOCKETIMPL = 10,
  /* java.net.SocketImpl */
  ANDROID_FDSAN_OWNER_TYPE_SOCKETIMPL = 11,
  /* libziparchive's ZipArchive */
  ANDROID_FDSAN_OWNER_TYPE_ZIPARCHIVE = 12,
};

tag中的value主要用作唯一性标识。关于native层的封装(比如unique_fd)而言,value为封装目标的指针值;关于Java层的封装(比如FileInputStream)而言,value为封装目标的hash code。这两种值在同一个进程里都具有唯一性。

了解完tag的构成后,接下来就是tag的创立和检验过程。

经过封装目标创立相应fd时(通常在目标的结构函数内),tag便会创立。而所有的close都会去进行tag检验,不管该close是经过封装目标的析构调用还是直接调用。举个比如,当一个fd经过FileInputStream创立时,目标回收期间便会close fd。可是如果咱们不小心将此fd传到了JNI函数中,经过close函数直接去操作它,那么检测时咱们就会发现:希望的tag是一个非零值(由type和value构成),实际的tag为0(经过原生close函数进行操作时tag为0),继而报错。

典型的报错log如下(与上述比如无关),一个经过opendir打开的fd,最终却直接经过close封闭,而不是closedir。

pid: 610, tid: 610, name: lmkd  >>> /system/bin/lmkd <<<
uid: 1069
signal 6 (SIGABRT), code -1 (SI_QUEUE), fault addr --------
Abort message: 'fdsan: attempted to close file descriptor 15, expected to be unowned, actually owned by DIR* 0x745b003c00'
    x0  0000000000000000  x1  0000000000000262  x2  0000000000000006  x3  0000007fc8696f90
    x4  0000000000000000  x5  0000000000000000  x6  0000000000000000  x7  0000000000000010
    x8  00000000000000f0  x9  643eeef5a527dc04  x10 0000000000000001  x11 0000000000000000
    x12 0000000000000008  x13 0000000060345fc8  x14 000216096eb6f000  x15 000024dad07fc39e
    x16 000000745b715948  x17 000000745b6f4390  x18 000000745c0bc000  x19 0000000000000262
    x20 0000000000000262  x21 000000745bc34000  x22 000000745cfdeb5c  x23 0000000000000003
    x24 0000007fc8697080  x25 ffffff80ffffffc8  x26 0000007fc8696d00  x27 0000007fc8696cc0
    x28 0000000000000000  x29 0000007fc8697020
    lr  000000745b6abf4c  sp  0000007fc8696c40  pc  000000745b6abf6c  pst 0000000000001000
backtrace:
      #00 pc 000000000008df6c  /apex/com.android.runtime/lib64/bionic/libc.so (fdsan_error(char const*, ...)+588)
      #01 pc 000000000008dc68  /apex/com.android.runtime/lib64/bionic/libc.so (android_fdsan_close_with_tag+740)
      #02 pc 000000000008e3d0  /apex/com.android.runtime/lib64/bionic/libc.so (close+16)
      #03 pc 000000000000b110  /system/bin/lmkd (start_wait_for_proc_kill(int)+184)
      #04 pc 000000000000a45c  /system/bin/lmkd (find_and_kill_process(int, int, char const*, meminfo*, timespec*, bool)+744)
      #05 pc 00000000000098a0  /system/bin/lmkd (mp_event_common(int, unsigned int, polling_params*)+2140)
      #06 pc 000000000000ba1c  /system/bin/lmkd (call_handler(event_handler_info*, polling_params*, unsigned int)+64)
      #07 pc 00000000000053a8  /system/bin/lmkd (main+2440)
      #08 pc 000000000008506c  /apex/com.android.runtime/lib64/bionic/libc.so (__libc_init+108)

且慢,讨论了这么久的tag究竟存在什么地方?能够肯定的是存在一个大局的table中,由于fd是一个32768以内的整数,因而能够将fd作为table的索引。此外,Josh将这个table设计成可扩容的,初始巨细为128,扩容后能够存放下所有fd的tag。这样关于大多数fd创立数量较少的进程,便能够运用较小的table来节省内存。

fdsan自引进之际便大局打开,关于保障系统的安全至关重要。但受限于它的原理,仍然有一些fd的use-after-close和double-close过错无法被检出。它们是:

  1. 不运用任何封装方式,而仅仅经过open和close来操作fd。这样即便对同一个fd double-close,由于希望的tag和实际的tag都为0,因而过错无法检出。
  2. 运用自定义的封装方式来操作fd,而没有在自定义的方式中接入fdsan。无法检出的理由同上。
  3. 单纯的use-after-close,而并非由double-close引发的use-after-close也无法检出。严厉意义上来说,我认为这个东西只能检出fd的double-close。原因是检测代码只插入在close函数里,关于fd的其他操作并无检测。

除了检测fd的过错外,存在大局table中的tag数据在生成tombstone时也会输出,表明每个fd的ownership。unowned标明该fd的创立未接入fdsan。

open files:
...
fd 47: /data/app/~~TPUHOtvHOT-iL2f4uUrFSg==/com.tencent.mm-FKuS1oTvzRpPMOXX-SE_ZQ==/base.apk (owned by ZipArchive 0xfffd2ae7dd90)
fd 48: /data/app/~~TPUHOtvHOT-iL2f4uUrFSg==/com.tencent.mm-FKuS1oTvzRpPMOXX-SE_ZQ==/split_config.arm64_v8a.apk (owned by ZipArchive 0xfffd2ae82390)
fd 49: /data/app/~~TPUHOtvHOT-iL2f4uUrFSg==/com.tencent.mm-FKuS1oTvzRpPMOXX-SE_ZQ==/split_config.en.apk (owned by ZipArchive 0xfffd2ae83ac0)
fd 50: /data/app/~~TPUHOtvHOT-iL2f4uUrFSg==/com.tencent.mm-FKuS1oTvzRpPMOXX-SE_ZQ==/split_config.hdpi.apk (owned by ZipArchive 0xfffd2ae84e00)
fd 51: /data/data/com.tencent.mm/files/splitcompat/2103/verified-splits/delivery.config.arm64_v8a.apk (owned by ZipArchive 0xfffd2ae80b10)
fd 52: /data/data/com.tencent.mm/files/splitcompat/2103/verified-splits/delivery.apk (owned by ZipArchive 0xfffd2ae803a0)
fd 53: anon_inode:[eventfd] (owned by unique_fd 0xfffd7ae85e84)
fd 54: anon_inode:[eventpoll] (owned by unique_fd 0xfffd7ae85edc)
fd 55: anon_inode:[eventfd] (owned by unique_fd 0xfffd7ae99ce4)
fd 56: anon_inode:[eventpoll] (owned by unique_fd 0xfffd7ae99d3c)
fd 57: /data/app/~~TPUHOtvHOT-iL2f4uUrFSg==/com.tencent.mm-FKuS1oTvzRpPMOXX-SE_ZQ==/base.apk (unowned)
fd 58: /data/app/~~TPUHOtvHOT-iL2f4uUrFSg==/com.tencent.mm-FKuS1oTvzRpPMOXX-SE_ZQ==/split_config.arm64_v8a.apk (unowned)

最终值得注意的一点是,fdsan在vfork得到的子进程里是不工作的。原因是vfork得到的子进程虽然会拷贝父进程的fd,可是运用的地址空间仍然属于父进程。因而fd和存储fd tag的内存产生了错配,在子进程中操作fd不会被父进程感知到,而tag的修正则会直接影响父进程的table。

【参阅文档】

  • android.googlesource.com/platform/bi…