fdsan is a file descriptor sanitizer added to Android in API level 29. In API level 29, fdsan warns when it finds a bug. In API level 30, fdsan aborts when it finds a bug.
背景
fd 是什么
In Unix and Unix-like computer operating systems, a file descriptor (FD, less frequently fildes) is a process-unique identifier (handle) for a file or other input/output resource, such as a pipe or network socket.
fd 一般用作进程内或进程间通讯,是进程独有的文件描述符表的索引,简单来说,就是体系内核为每个进程维护了一个 fd table,来记载进程中的fd,一般在android 体系上,每个进程所能最大读写的fd数量是有限的,假如超限,会呈现fd 无法创建/读取的问题。
fdsan 是什么
fdsan 全称其实是 file descriptor sanitizer
,是一种用于检测和铲除进程中未封闭的文件描述符(fd)的工具。它一般用于检测程序中的内存走漏和文件句柄走漏等问题。文件描述符是操作体系中用于拜访文件、网络套接字和其他I/O设备的机制。在程序中,打开文件或套接字会生成一个文件描述符,假如此文件描述符在运用后未封闭,就会造成文件句柄走漏,导致程序内存的不断添加。fd sanitizer会扫描进程的文件描述符表,检测未封闭的文件描述符,并将它们封闭,以防止进程内存走漏。
fdsan in Android
在 Android 上,fdsan(File Descriptor Sanitizer)是自 Android 11 开端引进的一项新功能。fdsan 旨在协助开发人员确诊和修正 Android 应用程序中的文件描述符走漏和运用过错。
fdsan 运用 Android Runtime (ART) 虚拟机中的功能来捕获应用程序的文件描述符运用情况。它会盯梢文件描述符的分配和释放,并在文件描述符走漏或过错运用时发出正告。fdsan 还支撑在应用程序溃散时生成详细的调试信息,以协助开发人员确诊问题的根本原因。
常见场景
void thread_one() {
int fd = open("/dev/null", O_RDONLY);
close(fd);
close(fd);
}
void thread_two() {
while (true) {
int fd = open("log", O_WRONLY | O_APPEND);
if (write(fd, "foo", 3) != 3) {
err(1, "write failed!");
}
}
}
同时运行上述两个线程,你会发现
thread one thread two
open("/dev/null", O_RDONLY) = 123
close(123) = 0
open("log", O_WRONLY | APPEND) = 123
close(123) = 0
write(123, "foo", 3) = -1 (EBADF)
err(1, "write failed!")
断语失利可能是这些过错中最无害的结果:也可能发生静默数据损坏或安全漏洞(例如,当第二个线程正在将用户数据保存到磁盘时,第三个线程进来并打开了一个连接到互联网的套接字)。
检测原理
fdsan 企图经过文件描述符所有权来强制检测或者防备文件描述符办理过错。与大多数内存分配可以经过std::unique_ptr
等类型来处理其所有权类似,几乎所有文件描述符都可以与负责封闭它们的仅有所有者相关联。fdsan供给了将文件描述符与所有者相关联的函数;假如有人企图封闭他们不拥有的文件描述符,根据装备,会发出正告或停止进程。
完成这个的办法是供给函数在文件描述符上设置一个64位的封闭符号。符号包括一个8位的类型字节,用于标识所有者的类型(在<android/fdsan.h>
中的枚举变量 android_fdsan_owner_type
),以及一个56位的值。这个值理想情况下应该是可以仅有标识目标的东西(原生目标的目标地址和Java目标的System.identityHashCode
),可是在难认为“所有者”推导出标识符的情况下,即便关于模块中的所有文件描述符都运用相同的值也很有用,因为它会捕捉封闭您的文件描述符的其他代码。
假如已符号符号的文件描述符运用过错的符号或没有符号封闭,咱们就知道出了问题,就可以生成确诊信息或停止进程。
在Android Q(11)中,fdsan的全局默认设置为单次正告。可以经过<android/fdsan.h>
中的android_fdsan_set_error_level
函数在运行时使 fdsan 愈加严厉或宽松。
fdsan捕捉文件描述符过错的可能性与在您的进程中符号所有者的文件描述符百分比成正比。
常见问题
E fdsan : failed to exchange ownership of file descriptor: fd xxx is owned by ParcelFileDescriptor 0xfffddddd was expected to be unowned
一般情况下,fd 所有权的误用并不会造成闪退,可是由于国内外厂商对 framework 的魔改,现在线上高频呈现对应的闪退,为了规避这类情况,咱们首先要标准 fd 的运用,特别是所有权的搬迁,另外,在操作涉及到 localsocket
、sharedmemory
时,要慎之又慎,体系会为每个进程记载一份 fd table,会记载每个fd 对应的所有权。假如长时间不释放而且又在不断分配,会呈现fd 超限问题,报错提示 cannot open fd
来看看 java 侧对文件描述符操作的注释
/**
* Create a new ParcelFileDescriptor that is a dup of the existing
* FileDescriptor. This obeys standard POSIX semantics, where the
* new file descriptor shared state such as file position with the
* original file descriptor.
*/
public ParcelFileDescriptor dup() throws IOException {
if (mWrapped != null) {
return mWrapped.dup();
} else {
return dup(getFileDescriptor());
}
}
/**
* Create a new ParcelFileDescriptor from a raw native fd. The new
* ParcelFileDescriptor holds a dup of the original fd passed in here,
* so you must still close that fd as well as the new ParcelFileDescriptor.
*
* @param fd The native fd that the ParcelFileDescriptor should dup.
*
* @return Returns a new ParcelFileDescriptor holding a FileDescriptor
* for a dup of the given fd.
*/
public static ParcelFileDescriptor fromFd(int fd) throws IOException {
final FileDescriptor original = new FileDescriptor();
original.setInt$(fd);
try {
final FileDescriptor dup = new FileDescriptor();
int intfd = Os.fcntlInt(original, (isAtLeastQ() ? F_DUPFD_CLOEXEC : F_DUPFD), 0);
dup.setInt$(intfd);
return new ParcelFileDescriptor(dup);
} catch (ErrnoException e) {
throw e.rethrowAsIOException();
}
}
/**
* Return the native fd int for this ParcelFileDescriptor and detach it from
* the object here. You are now responsible for closing the fd in native
* code.
* <p>
* You should not detach when the original creator of the descriptor is
* expecting a reliable signal through {@link #close()} or
* {@link #closeWithError(String)}.
*
* @see #canDetectErrors()
*/
public int detachFd() {
if (mWrapped != null) {
return mWrapped.detachFd();
} else {
if (mClosed) {
throw new IllegalStateException("Already closed");
}
int fd = IoUtils.acquireRawFd(mFd);
writeCommStatusAndClose(Status.DETACHED, null);
mClosed = true;
mGuard.close();
releaseResources();
return fd;
}
Share两个闪退事例:
- fd 超限问题
W zygote64: ashmem_create_region failed for 'indirect ref table': Too many open files
这个时候咱们去查看 体系侧对应 fd 情况,可以发现,fd table 中呈现了非常多的 socket 且所有者均显示为unix domain socket,很明显是跨进程通讯的 socket 未被释放的原因
- fd 所有权搬运问题
[DEBUG] Read self maps instead! map: 0x0
[]()****#00 pc 00000000000c6144 /apex/com.android.runtime/bin/linker64 (__dl_abort+168)
[]()****#01 pc 00000000000c6114 /apex/com.android.runtime/bin/linker64 (__dl_abort+120)
这个堆栈看得人一头雾水,因为蹦在 linker 里,咱们完全不知道发生了什么,可是经过调查咱们发现问题日志中都存在如下报错
E fdsan : failed to exchange ownership of file descriptor: fd xxx is owned by ParcelFileDescriptor 0xsssssss was expected to be unowned
根据上述常识,咱们有理由怀疑是代码中fd 的操作合法性存在问题,经过详尽整理,咱们得出了对应这两类问题的一些action:
所以有以下对应的action:
-
local socket 要及时封闭 connection,防止 fd 超限问题。
-
sharememory 从 进程A 搬运到 进程B 时,一定要
detachFd
进行 fd 所有权搬运,假如需要在进程 A 内进行缓存,那么 share 给进程B 时需要对 fd 进行dup
操作后再detachFd
版本差异
fdsan 在 Android 10 上开端引进,在Android 10 上会持续输出检测结果,在Android 11 及以上,fdsan 检测到过错后会输出过错日志并间断检测,在 Android 9 以下,没有对应的完成。所以,假如你需要在代码中引进fdsan 来进行 fd 校验检测。请参照以下完成:
extern "C" {
void android_fdsan_exchange_owner_tag(int fd,
uint64_t expected_tag,
uint64_t new_tag)
__attribute__((__weak__));
}
void CheckOwnership(uint64_t owner, int fd) {
if (android_fdsan_exchange_owner_tag) {
android_fdsan_exchange_owner_tag(fd, 0, owner);
}
}