fdsan使用指导
=========

#### 功能介绍

fdsan主要用于检测不同使用者对相同文件描述符的错误操作，如多次关闭（double-close）和关闭后使用（use-after-close）。这些文件描述符可以是操作系统中的文件、目录、网络套接字或其他I/O设备等。在程序中，打开文件或套接字会生成一个文件描述符。如果此文件描述符在使用后出现反复关闭或关闭后使用等情形，会导致内存泄露或文件句柄泄露等安全隐患。这类问题非常隐蔽，难以排查。为此，引入了fdsan这种检测工具。

#### 实现原理

设计思路：当打开已有文件或创建一个新文件的时候，在得到返回fd后，设置一个关联的tag，来标记fd的属主信息；关闭文件前，检测fd关联的tag，判断是否符合预期(属主信息一致)，符合就继续走正常文件关闭流程；如果不符合就是检测到异常，根据设置，调用对应的异常处理。

tag由两部分组成，最高位的8-bit构成type，后面的56-bit构成value。

type标识fd通过何种封装形式进行管理，例如FDSAN\_OWNER\_TYPE\_FILE表示fd通过普通文件进行管理。类型在fdsan\_owner\_type中定义。

value用于标识实际的owner tag。

tag构成图示

![](https://contentcenter-vali-drcn.dbankcdn.cn/pvt_2/DeveloperAlliance_scene_100_1/02/v3/mUsh5tjMR4q561nfWti5fg/zh-cn_image_0000002594829602.png?HW-CC-KV=V1&HW-CC-Date=20260612T073806Z&HW-CC-Expire=86400&HW-CC-Sign=CB79A591D52A18F35DB1D60587BB4CFB8D5AB4595393769F26D285B64A092C72)

#### 接口说明

#### \[h2\]fdsan\_set\_error\_level

enum fdsan\_error\_level fdsan\_set\_error\_level(enum fdsan\_error\_level new\_level);

**描述：** 可以通过fdsan\_set\_error\_level设定error\_level，error\_level用于控制检测到异常后的处理行为。默认error\_level为FDSAN\_ERROR\_LEVEL\_WARN\_ALWAYS。

**参数：** fdsan\_error\_level

| 名称 | 说明 |
| :-- | :-- |
| FDSAN\_ERROR\_LEVEL\_DISABLED | disabled，此level代表什么都不处理。 |
| FDSAN\_ERROR\_LEVEL\_WARN\_ONCE | warn-once，第一次出现错误时在hilog中发出警告，然后将级别降低为disabled(FDSAN\_ERROR\_LEVEL\_DISABLED)。 |
| FDSAN\_ERROR\_LEVEL\_WARN\_ALWAYS | warn-always，每次出现错误时都在hilog中发出警告。 |
| FDSAN\_ERROR\_LEVEL\_FATAL | fatal，出现错误时调用abort异常退出。 |

**返回值：** 返回旧的error\_level。

#### \[h2\]fdsan\_get\_error\_level

enum fdsan\_error\_level fdsan\_get\_error\_level();

**描述：** 可以通过fdsan\_get\_error\_level获取error level。

**返回值：** 当前的error\_level。

#### \[h2\]fdsan\_create\_owner\_tag

uint64\_t fdsan\_create\_owner\_tag(enum fdsan\_owner\_type type, uint64\_t tag);

**描述：** 通过传入的type和tag字段，拼接成一个有效的文件描述符的关闭tag。

**参数：** fdsan\_owner\_type

| 名称 | 说明 |
| :-- | :-- |
| FDSAN\_OWNER\_TYPE\_GENERIC\_00 | 默认未使用fd对应的type值。 |
| FDSAN\_OWNER\_TYPE\_GENERIC\_FF | 默认非法fd对应的type值。 |
| FDSAN\_OWNER\_TYPE\_FILE | 默认普通文件对应的type值，使用fopen或fdopen打开的文件具有该类型。 |
| FDSAN\_OWNER\_TYPE\_DIRECTORY | 默认文件夹对应的type值，使用opendir或fdopendir打开的文件具有该类型。 |
| FDSAN\_OWNER\_TYPE\_UNIQUE\_FD | 默认unique\_fd对应的type值，保留暂未使用。 |
| FDSAN\_OWNER\_TYPE\_ZIPARCHIVE | 默认zip压缩文件对应的type值，保留暂未使用。 |

**返回值：** 返回创建的tag，可以用于fdsan\_exchange\_owner\_tag函数的输入。

#### \[h2\]fdsan\_exchange\_owner\_tag

void fdsan\_exchange\_owner\_tag(int fd, uint64\_t expected\_tag, uint64\_t new\_tag);

**描述：** 修改文件描述符的关闭tag。

通过fd找到对应的FdEntry，判断close\_tag值与expected\_tag是否一致。如果一致，说明符合预期，可以使用new\_tag值重新设定对应的FdEntry。

如果不符合，说明检测到异常，后续进行对应的异常处理。

**参数：**

| 名称 | 类型 | 说明 |
| :-- | :-- | :-- |
| fd | int | fd句柄，作为FdEntry的索引。 |
| expected\_tag | uint64\_t | 期望的ownership tag值。 |
| new\_tag | uint64\_t | 设置新的ownership tag值。 |

#### \[h2\]fdsan\_close\_with\_tag

int fdsan\_close\_with\_tag(int fd, uint64\_t tag);

**描述：** 根据tag描述符关闭文件描述符。

通过fd找到匹配的FdEntry。如果close\_tag与tag相同，则符合预期，可以继续执行文件描述符关闭流程；否则，表示检测到异常。

**参数：**

| 名称 | 类型 | 说明 |
| :-- | :-- | :-- |
| fd | int | 待关闭的fd句柄。 |
| tag | uint64\_t | 期望的ownership tag。 |

**返回值：** 0或者-1，0表示close成功，-1表示close失败。

#### \[h2\]fdsan\_get\_owner\_tag

uint64\_t fdsan\_get\_owner\_tag(int fd);

**描述：** 根据文件描述符获取tag信息。

通过fd找到匹配的FdEntry，并获取其close\_tag。

**参数：**

| 名称 | 类型 | 说明 |
| :-- | :-- | :-- |
| fd | int | 文件描述符。 |

**返回值：** 返回对应fd的tag。

#### \[h2\]fdsan\_get\_tag\_type

const char\* fdsan\_get\_tag\_type(uint64\_t tag);

**描述：** 根据tag计算出对应的type类型。

获取tag信息后，计算并获取对应tag的type信息。

**参数：**

| 名称 | 类型 | 说明 |
| :-- | :-- | :-- |
| tag | uint64\_t | ownership tag。 |

**返回值：** 返回对应tag的type。

#### \[h2\]fdsan\_get\_tag\_value

uint64\_t fdsan\_get\_tag\_value(uint64\_t tag);

**描述：** 根据tag计算出对应的owner value。

通过获取到的tag信息，通过偏移计算获取对应tag中的value信息。

**参数：**

| 名称 | 类型 | 说明 |
| :-- | :-- | :-- |
| tag | uint64\_t | ownership tag。 |

**返回值：** 返回对应tag的value。

#### 使用示例

如何使用fdsan？这是一个简单的double-close问题：

#include <unistd.h>
#include <fcntl.h>
#include <hilog/log.h>
#include <vector>
#include <thread>

void good\_write()
{
    sleep(1);
    int fd = open("log", O\_WRONLY | O\_APPEND);
    sleep(3);
    ssize\_t ret = write(fd, "fdsan test", 11);
    if (ret == -1) {
        OH\_LOG\_ERROR(LOG\_APP, "good write but failed?!");
    }
    close(fd);
}

void bad\_close()
{
    int fd = open("/dev/null", O\_RDONLY);
    close(fd);
    sleep(2);
    // This close expected to be detect by fdsan
    close(fd);
}

void functional\_test()
{
    std::vector<std::thread> threads;
    for (auto function : { good\_write, bad\_close }) {
        threads.emplace\_back(function);
    }
    for (auto& thread : threads) {
        thread.join();
    }
}

int main()
{
    functional\_test();
    return 0;
}

上述代码中的good\_write函数会打开一个文件并写入一些字符串，而bad\_close函数中也会打开一个文件同时包含double-close问题，这两个线程同时运行执行情况如下图。

![](https://contentcenter-vali-drcn.dbankcdn.cn/pvt_2/DeveloperAlliance_scene_100_1/fc/v3/IekclE59RKG7azZy5l7JaA/zh-cn_image_0000002625189049.png?HW-CC-KV=V1&HW-CC-Date=20260612T073806Z&HW-CC-Expire=86400&HW-CC-Sign=2B18B170A17C9FD366E523E6B224EFCC48AB0EE01BEF10E3C812A2A78831F8F9)

由于每次open返回的文件描述符（fd）是顺序分配的，进入主函数后第一个可用的fd是43。在bad\_close 函数中，第一次open返回的fd也是43。关闭之后，43变成可用的fd。在good\_write函数中，open返回了第一个可用的fd，即43。然而，由于bad\_close函数中存在重复关闭问题，错误地关闭了另一个线程中打开的文件，导致写入失败。

引入fdsan后，有两种检测方法：使用标准库接口或实现带有fdsan的函数接口。

#### \[h2\]使用标准库接口

标准库接口中，fopen、fdopen、opendir、fdopendir已集成fdsan。使用这些接口而非直接使用open有助于检测问题。例如，可以使用fopen替代open。

#include <stdio.h>
#include <errno.h>
#define TEMP\_FILE "/data/local/tmp/test.txt"

void good\_write()
{
    // fopen is protected by fdsan, replace open with fopen
    // int fd = open(TEMP\_FILE, O\_RDWR);
    FILE \*f = fopen(TEMP\_FILE, "w+");
    if (f == NULL) {
        printf("fopen failed errno=%d\\n", errno);
        return;
    }
    // ssize\_t ret = write(fd, "fdsan test\\n", 11);
    int ret = fprintf(f, "fdsan test %d\\n", 11);
    if (ret < 0) {
        printf("fprintf failed errno=%d\\n", errno);
    }
    // close(fd);
    fclose(f);
}

#### \[h2\]日志信息

使用fopen打开的每个文件描述符都需要有一个与之对应的 tag 。fdsan 在 close 时会检查关闭的 fd 是否与 tag 匹配，不匹配就会默认提示相关日志信息。下面是上述代码的日志信息：

\# hilog | grep MUSL-FDSAN
04-30 15:03:41.760 10933  1624 E C03f00/MUSL-FDSAN: attempted to close file descriptor 43,                             expected to be unowned, actually owned by FILE\* 0x00000000f7b90aa2

从这里的错误信息中可以看出，FILE接口的文件被其他人错误地关闭了。FILE接口的地址可以协助进一步定位。

此外，可以在代码中使用fdsan\_set\_error\_level设置错误等级error\_level。设置为Fatal之后，如果fdsan检测到错误，会提示日志信息并crash生成堆栈信息，用于定位。下面是 error\_level 设置为Fatal之后生成的crash堆栈信息：

Reason:Signal:SIGABRT(SI\_TKILL)@0x0000076e from:1902:20010043
Fault thread info:
Tid:15312, Name:e.myapplication
#00 pc 000e65bc /system/lib/ld-musl-arm.so.1(raise+176)(3de40c79448a2bbced06997e583ef614)
#01 pc 0009c3bc /system/lib/ld-musl-arm.so.1(abort+16)(3de40c79448a2bbced06997e583ef614)
#02 pc 0009de4c /system/lib/ld-musl-arm.so.1(fdsan\_error+116)(3de40c79448a2bbced06997e583ef614)
#03 pc 0009e2e8 /system/lib/ld-musl-arm.so.1(fdsan\_close\_with\_tag+836)(3de40c79448a2bbced06997e583ef614)
#04 pc 0009e56c /system/lib/ld-musl-arm.so.1(close+20)(3de40c79448a2bbced06997e583ef614)
#05 pc 000055d8 /data/storage/el1/bundle/libs/arm/libentry.so(bad\_close()+96)(f3339aac824c099f449153e92718e1b56f80b2ba)
#06 pc 00006cf4 /data/storage/el1/bundle/libs/arm/libentry.so(decltype(std::declval<void (\*)()>()()) std::\_\_n1::\_\_invoke\[abi:v15004\]<void (\*)()>(void (\*&&)())+24)(f3339aac824c099f449153e92718e1b56f80b2ba)
#07 pc 00006c94 /data/storage/el1/bundle/libs/arm/libentry.so(f3339aac824c099f449153e92718e1b56f80b2ba)
#08 pc 000067b8 /data/storage/el1/bundle/libs/arm/libentry.so(void\* std::\_\_n1::\_\_thread\_proxy\[abi:v15004\]<std::\_\_n1::tuple<std::\_\_n1::unique\_ptr<std::\_\_n1::\_\_thread\_struct, std::\_\_n1::default\_delete<std::\_\_n1::\_\_thread\_struct>>, void (\*)()>>(void\*)+100)(f3339aac824c099f449153e92718e1b56f80b2ba)
#09 pc 00105a6c /system/lib/ld-musl-arm.so.1(start+248)(3de40c79448a2bbced06997e583ef614)
#10 pc 000700b0 /system/lib/ld-musl-arm.so.1(3de40c79448a2bbced06997e583ef614)

此时，从crash信息中可以看到bad\_close存在问题，同时crash中包含所有打开的文件，协助定位问题，提升效率。

OpenFiles列出所有打开的文件

**字段说明：**

fd->对象描述：文件描述符fd关联的内核对象标识。

\[方括号内容\]：对象内部标识：

*   对于socket/pipe：内核分配的伪文件系统ID；
*   对于普通文件：文件系统inode编号（操作系统用于管理该文件元数据及数据块的数据结构）；
*   对于anon\_inode：对象类型名称。

native object of unknown type 0：该fd对应的tag标签值为0。

OpenFiles:
0->/dev/null native object of unknown type 0
1->/dev/null native object of unknown type 0
2->/dev/null native object of unknown type 0
3->socket:\[28102\] native object of unknown type 0
4->socket:\[28103\] native object of unknown type 0
5->anon\_inode:\[eventpoll\] native object of unknown type 0
6->/sys/kernel/debug/tracing/trace\_marker native object of unknown type 0
7->anon\_inode:\[eventpoll\] native object of unknown type 0
8->anon\_inode:\[eventpoll\] native object of unknown type 0
9->/dev/console native object of unknown type 0
10->pipe:\[95598\] native object of unknown type 0
11->pipe:\[95598\] native object of unknown type 0
12->socket:\[18542\] native object of unknown type 0
13->pipe:\[96594\] native object of unknown type 0
14->socket:\[18545\] native object of unknown type 0
15->pipe:\[96594\] native object of unknown type 0
16->anon\_inode:\[eventfd\] native object of unknown type 0
17->/dev/binder native object of unknown type 0
18->/data/storage/el1/bundle/entry.hap native object of unknown type 0
19->anon\_inode:\[eventpoll\] native object of unknown type 0
20->anon\_inode:\[signalfd\] native object of unknown type 0
21->socket:\[29603\] native object of unknown type 0
22->anon\_inode:\[eventfd\] native object of unknown type 0
23->anon\_inode:\[eventpoll\] native object of unknown type 0
24->anon\_inode:\[eventfd\] native object of unknown type 0
25->anon\_inode:\[eventpoll\] native object of unknown type 0
26->anon\_inode:\[eventfd\] native object of unknown type 0
27->anon\_inode:\[eventpoll\] native object of unknown type 0
28->anon\_inode:\[eventfd\] native object of unknown type 0
29->anon\_inode:\[eventpoll\] native object of unknown type 0
30->anon\_inode:\[eventfd\] native object of unknown type 0
31->anon\_inode:\[eventpoll\] native object of unknown type 0
32->anon\_inode:\[eventfd\] native object of unknown type 0
33->anon\_inode:\[eventpoll\] native object of unknown type 0
34->anon\_inode:\[eventfd\] native object of unknown type 0
35->socket:\[97409\] native object of unknown type 0
36->socket:\[94716\] native object of unknown type 0
38->socket:\[94720\] native object of unknown type 0
40->/data/storage/el1/bundle/entry\_test.hap native object of unknown type 0
41->socket:\[95617\] native object of unknown type 0
42->/sys/kernel/debug/tracing/trace\_marker native object of unknown type 0
43->/dev/null FILE\* 4155724704
44->socket:\[94737\] native object of unknown type 0
45->pipe:\[95634\] native object of unknown type 0
46->pipe:\[95634\] native object of unknown type 0
47->pipe:\[95635\] native object of unknown type 0
49->pipe:\[95636\] native object of unknown type 0
50->pipe:\[95636\] native object of unknown type 0

#### \[h2\]实现具有fdsan的函数接口

除了使用标准库函数，还可以实现具有fdsan的函数接口。fdsan机制通过fdsan\_exchange\_owner\_tag和fdsan\_close\_with\_tag实现。fdsan\_exchange\_owner\_tag设置fd的tag，fdsan\_close\_with\_tag检查关闭文件时的tag。

下面是一个实现具有fdsan功能的函数接口的示例：

#include <errno.h>
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>

#include <utility>

struct fdsan\_fd {
    fdsan\_fd() = default;

    explicit fdsan\_fd(int fd)
    {
        reset(fd);
    }

    fdsan\_fd(const fdsan\_fd& copy) = delete;
    fdsan\_fd(fdsan\_fd&& move)
    {
        \*this = std::move(move);
    }

    ~fdsan\_fd()
    {
        reset();
    }

    fdsan\_fd& operator=(const fdsan\_fd& copy) = delete;
    fdsan\_fd& operator=(fdsan\_fd&& move)
    {
        if (this == &move) {
            return \*this;
        }
        reset();
        if (move.fd\_ != -1) {
            fd\_ = move.fd\_;
            move.fd\_ = -1;
            // Acquire ownership from the moved-from object.
            exchange\_tag(fd\_, move.tag(), tag());
        }
        return \*this;
    }

    int get()
    {
        return fd\_;
    }

    void reset(int new\_fd = -1)
    {
        if (fd\_ != -1) {
            close(fd\_, tag());
            fd\_ = -1;
        }
        if (new\_fd != -1) {
            fd\_ = new\_fd;
            // Acquire ownership of the presumably unowned fd.
            exchange\_tag(fd\_, 0, tag());
        }
    }

  private:
    int fd\_ = -1;

    // Use the address of object as the file tag
    uint64\_t tag()
    {
        return reinterpret\_cast<uint64\_t>(this);
    }

    static void exchange\_tag(int fd, uint64\_t old\_tag, uint64\_t new\_tag)
    {
        if (&fdsan\_exchange\_owner\_tag) {
            fdsan\_exchange\_owner\_tag(fd, old\_tag, new\_tag);
        }
    }

    static int close(int fd, uint64\_t tag)
    {
        if (&fdsan\_close\_with\_tag) {
            return fdsan\_close\_with\_tag(fd, tag);
        }
    }
};

这里的实现中使用fdsan\_exchange\_owner\_tag在开始时将fd与结构体对象地址绑定，然后在关闭文件时使用fdsan\_close\_with\_tag进行检测，预期tag是结构体对象地址。

在实现具有fdsan的函数接口后，可以使用该接口包装fd。

#define TEMP\_FILE "/data/local/tmp/test.txt"

void good\_write()
{
    // int fd = open(DEV\_NULL\_FILE, O\_RDWR);
    fdsan\_fd fd(open(TEMP\_FILE, O\_CREAT | O\_RDWR));
    if (fd.get() == -1) {
        printf("fopen failed errno=%d\\n", errno);
        return;
    }
    ssize\_t ret = write(fd.get(), "fdsan test\\n", 11);
    if (ret == -1) {
        printf("write failed errno=%d\\n", errno);
    }
    fd.reset();
}

此时运行该程序可以检测到另一个线程的double-close问题，详细信息可以[参考日志](#日志信息)。同样也可以设置error\_level为fatal，这样可以使fdsan在检测到crash之后主动crash以获取更多信息。

#### 多线程场景下的注意事项

在多线程环境中使用fdsan时，由于文件描述符（fd）的分配和回收是全局性的，fdsan检测到的tag不匹配错误信息可能存在与实际根因不一致的情况。开发者需要注意以下场景：

**fd快速回收导致报错指向错误属主：** 当线程A关闭一个fd后，该fd可能立即被线程B回收并绑定新的tag。此时如果线程A（或系统中其他模块）对该fd执行了非法close或double close，fdsan报错信息中显示的owner将是线程B的tag，而非原始属主的信息。这并不意味着线程B的tag设置有误，而是当前进程内其他业务逻辑存在非法close或double close的问题。

**检测与执行的竞态窗口：** fdsan\_close\_with\_tag内部在"校验tag"与"执行close"之间存在极小的时间窗口。在多线程并发场景下，fd可能在该窗口内被回收并重新分配给其他线程，导致校验结果指向的属主并非当前fd的实际使用者。

**排查建议：** 当看到fdsan报错时，不应直接认定日志中显示的owner就是问题的直接责任方。建议结合fd的生命周期、调用栈信息以及系统中其他模块对fd的使用情况进行综合排查，确认是否存在其他模块的非法close或double close行为。

#### close函数信号安全性说明

在POSIX标准中，close函数原本被定义为信号安全函数（async-signal-safe），这意味着它可以安全地在信号处理函数（signal handler）中调用。然而，在集成了fdsan（File Descriptor Sanitizer）机制的系统实现中，这一性质发生了变化。

由于fdsan的实现依赖于mmap系统调用，而mmap本身不是信号安全函数，这会导致close函数也不再是信号安全的。因此，在信号处理函数中避免使用 close，可以通过其他系统调用来实现相同功能。