背景
在分配file descriptors时, POSIX标准规定了内核必须从所有可被使用的fd数值中最小的一个, 参考alloc_fd,如果代码里没有正确的处理好fd的open/close等操作,就可能会带来以下2个副作用:
- use-after-close
- double-close
示例: double-close问题
1 | void thread_one() { |
上面的代码可能产生下面的行为,导致线程2写入失败:
1 | // 线程1 线程2 |
Fd Sanitizer
Aosp在Android 10.0里,引入了一个fdsan机制,在发生前一节描述的异常行为时,可以选择让进程终止执行,并打印出发生错误线程的调用栈,这样开发者可以根据调用栈,快速了解出问题的模块,后面再去修正它。
先思考一个问题,设计一种检测前面的fd误操作问题的机制,需要考虑哪些东西?
- api向前兼容
不能修改libc已有的api - 鉴别一个fd的合法性
我的fd真的是我的吗,我能使用它吗? - 友好的错误提示
别人用了我的fd,或我用了别人的fd时,如何确认这个fd的拥有者?我们还需要backtrace!
double-close 或 use-after-close问题的本质
double-close和use-after-close本质上都是使用了一个已经被关闭的fd,只不过这里面有几种不同情况:
- 线程1连续对同一个fd关闭2次,那么可能会因为EBADF而导致进程abort
- 线程1对同一个fd关闭了>=2次,而如果在第一次和第二次关闭期间,线程2此时刚刚打开一个文件或socket,那么
就可能出现:线程1的第二次关闭的fd,正好等于线程2新打开的fd,而线程1的第二次关闭操作(意外)错误的把
线程2创建的fd给关闭了,此后线程2如果对这个fd进行读写等操作就会失败 - 线程1关闭了一个fd后,线程2立即新打开了一个fd,如果线程2新打开的fd对应的是一个结构化的文件,例如数据库文件、xml文件等,
线程1意外的在关闭fd后,又尝试向这个fd写入数据,就有可能线程2操作的文件的结构被破坏!
我们可以看到问题的核心就是对fd的控制权(ownership)问题:
当我们对1个fd拥有控制权时,我们期望其别人不能对我们的fd进行操作,反过来也是,当一个fd的控制权在别人那时,也期望我们不会去操作他们的fd!
unique_fd!
类似智能指针(unique_ptr),用户代码里使用unique_fd,在各个函数间进行参数传递时,也是使用unique_fd来进行,而不是原先的传递raw fd,这样这个fd从open到close,都会有个唯一的unique_fd来标明它的控制权。
而且因为unique_fd重载了operator int(),所以它可以完美兼容已有的libc接口。
1 | class unique_fd_impl final { |
使用方式:
1 |
|
下面先通过分析unique_fd的代码来一窥fdsan的检测原理:
1 | // bionic/libc/bionic/fdsan.cpp |
流程很简单,以上创建unique_fd时的tag操作系列调用,就是对fd控制权记录的检查和更新:
unique_fd创建的时候,对传入的参数fd进行检查,如果ownership不匹配,便会输出warning日志或abort(可配置);
同样的,unique_fd销毁时,也会检查内部保存的fd的控制权是否依然还属于当前的unique_fd,如果是,则可以将其关闭;
如果控制权丢失了,那么通过fdsan_error打印相应的错误信息,并根据配置再采取是否需要终止当前进程的操作。
1 | // bionic/libc/bionic/fdsan.cpp |
通过分析以上的关键代码,可以发现fdsan本身是很简洁的,我们已经基本掌握了fdsan是如何通过unique_fd这个工具类来对fd控制权的创建和监控的。
自定义fd类型
自定义fd类型主要是为了便于详细区分不同模块间创建的fd,例如zip文件的fd,sqlite3的fd,java代码里的FileInputStream和FileOutputStream等,下面看一下这几类fd的控制权是如何创建和管理的。
zip类型的文件
1 | // system/core/libziparchive/zip_archive.cc |
数据库文件
1 | // external/sqlite/dist/sqlite3.c |
fopen/fclose
请参考bionic/libc/stdio/stdio.cpp#__FILE_close,不再详细描述
java代码里打开的fd
- ParcelFileDescriptor
1 | public class ParcelFileDescriptor implements Parcelable, Closeable { |
libcore.io.Linux.close方法的实现:
1 | // libcore/luni/src/main/native/libcore_io_Linux.cpp |
- FileInputStream & FileOutputStream
跟ParcelableFileDescriptor的实现类似,不再详述 - PlainSocketImpl & AbstractPlainDatagramSocketImpl
略 - RandomAccessFile
略
如何查看进程的fdan记录
1 | $ adb shell |
如何启用fdsan
fdsan在Q上是默认启用的,只不过在遇到fd误操作问题时,预设的行为仅是打印一些警示日志。
比较让人高兴的是,fdsan也定义了不同的安全级别:
- disabled:禁用
- warn-once:只在第一次打印一条警告日志,并在/data/tombstone/目录下生成异常进程的调用栈
- warn-always:跟warn-once一样,只不过每次出现fd误操作都会打印警告日志和生成tombstone文件
- fatal:abort进程,并生成tombstone文件
fd也开放了对应的api可以让我们去调整一个进程在遇到此类异常时的行为:android_fdsan_set_error_level
& android_fdsan_get_error_level
,这样可以配置进程在遇到此类错误时,选择立即abort进程,并配置生成core文件,然后就可以愉快的去debug了!
兼容性问题
由于fdsan是在Q上libc里才引入的机制,对于Android旧版本是没有做支持的,而且我也check了一下20.0.5594570版的ndk,也没有把unique_fd工具类给开发出来,这对于系统开发人员到没有太大影响,但对于app开发,就需要自己实现一个unique_fd类,另外老版本Android系统上并没有这些fdsan api,我自己也写了一个unique_fd_compat类,用来解决编译问题,仅供参考。
另外一个小tip:libc里预定义的fd owner类型比较少:
1 | // bionic/libc/include/android/fdsan.h |
但从前面的分析,我们发现,其实它是可以支持到255个不同类型的,这样的话在我们自己实现的unique_fd里就可以做到支持不同模块设置不同的子类型,例如audio,video模块可以各自预设不同的子类型,以便于出现问题时快速区分。
Aosp官方介绍
https://android.googlesource.com/platform/bionic/+/master/docs/fdsan.md