不是吧不是吧,fdsan 都不知道?

1,964 阅读7分钟

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 的使用,特别是所有权的迁移,另外,在操作涉及到 localsocketsharedmemory 时,要慎之又慎,系统会为每个进程记录一份 fd table,会记录每个fd 对应的所有权。如果长时间不释放并且又在不断分配,会出现fd 超限问题,报错提示 cannot open fd

image.png

来看看 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两个闪退案例:

  1. fd 超限问题
W zygote64: ashmem_create_region failed for 'indirect ref table': Too many open files

这个时候我们去查看 系统侧对应 fd 情况,可以发现,fd table 中出现了非常多的 socket 且所有者均显示为unix domain socket,很明显是跨进程通信的 socket 未被释放的原因

  1. 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);
  }
}