• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# fdsan使用指导
2<!--Kit: NDK Development-->
3<!--Subsystem: CommonLibrary-->
4<!--Owner: @liyiming13-->
5<!--Designer: @huang_huijin-->
6<!--Tester: @kirl75; @zsw_zhushiwei-->
7<!--Adviser: @fang-jinxu-->
8
9## 1. 功能介绍
10
11fdsan主要用于检测不同使用者对相同文件描述符的错误操作,如多次关闭(double-close)和关闭后使用(use-after-close)。这些文件描述符可以是操作系统中的文件、目录、网络套接字或其他I/O设备等。在程序中,打开文件或套接字会生成一个文件描述符。如果此文件描述符在使用后出现反复关闭或关闭后使用等情形,会导致内存泄露或文件句柄泄露等安全隐患。这类问题非常隐蔽,难以排查。为此,引入了fdsan这种检测工具。
12
13## 2. 实现原理
14
15设计思路:当打开已有文件或创建一个新文件的时候,在得到返回fd后,设置一个关联的tag,来标记fd的属主信息;关闭文件前,检测fd关联的tag,判断是否符合预期(属主信息一致),符合就继续走正常文件关闭流程;如果不符合就是检测到异常,根据设置,调用对应的异常处理。
16
17tag由两部分组成,最高位的8-bit构成type,后面的56-bit构成value。
18
19type标识fd通过何种封装形式进行管理,例如`FDSAN_OWNER_TYPE_FILE`表示fd通过普通文件进行管理。类型在`fdsan_owner_type`中定义。
20
21value用于标识实际的owner tag。
22
23 tag构成图示
24
25![](./figures/tag.PNG)
26
27
28
29## 3. 接口说明
30
31### fdsan_set_error_level
32
33```
34enum fdsan_error_level fdsan_set_error_level(enum fdsan_error_level new_level);
35```
36
37**描述:** 可以通过`fdsan_set_error_level`设定error_level,error_level用于控制检测到异常后的处理行为。默认error_level为FDSAN_ERROR_LEVEL_WARN_ALWAYS。
38
39**参数:** fdsan_error_level
40
41| 名称                       | 说明                                                         |
42| -------------------------- | ------------------------------------------------------------ |
43| `FDSAN_ERROR_LEVEL_DISABLED` | disabled,此level代表什么都不处理。                         |
44| `FDSAN_ERROR_LEVEL_WARN_ONCE` | warn-once,第一次出现错误时在hilog中发出警告,然后将级别降低为disabled(FDSAN_ERROR_LEVEL_DISABLED)。 |
45| `FDSAN_ERROR_LEVEL_WARN_ALWAYS` | warn-always,每次出现错误时都在hilog中发出警告。 |
46| `FDSAN_ERROR_LEVEL_FATAL` | fatal,出现错误时调用abort异常退出。 |
47
48**返回值:** 返回旧的error_level。
49
50### fdsan_get_error_level
51
52```
53enum fdsan_error_level fdsan_get_error_level();
54```
55
56**描述:** 可以通过`fdsan_get_error_level`获取error level。
57
58**返回值:** 当前的error_level。
59
60### fdsan_create_owner_tag
61```
62uint64_t fdsan_create_owner_tag(enum fdsan_owner_type type, uint64_t tag);
63```
64**描述:** 通过传入的type和tag字段,拼接成一个有效的文件描述符的关闭tag。
65
66**参数:** fdsan_owner_type
67
68| 名称                       | 说明                                                         |
69| -------------------------- | ------------------------------------------------------------ |
70| `FDSAN_OWNER_TYPE_GENERIC_00` | 默认未使用fd对应的type值。     |
71| `FDSAN_OWNER_TYPE_GENERIC_FF` | 默认非法fd对应的type值。 |
72| `FDSAN_OWNER_TYPE_FILE` | 默认普通文件对应的type值,使用fopen或fdopen打开的文件具有该类型。 |
73| `FDSAN_OWNER_TYPE_DIRECTORY` | 默认文件夹对应的type值,使用opendir或fdopendir打开的文件具有该类型。 |
74| `FDSAN_OWNER_TYPE_UNIQUE_FD` | 默认unique_fd对应的type值,保留暂未使用。 |
75| `FDSAN_OWNER_TYPE_ZIPARCHIVE` | 默认zip压缩文件对应的type值,保留暂未使用。 |
76
77**返回值:** 返回创建的tag,可以用于fdsan_exchange_owner_tag函数的输入。
78
79### fdsan_exchange_owner_tag
80
81```
82void fdsan_exchange_owner_tag(int fd, uint64_t expected_tag, uint64_t new_tag);
83```
84**描述:** 修改文件描述符的关闭tag。
85
86通过fd找到对应的FdEntry,判断close_tag值与expected_tag是否一致。如果一致,说明符合预期,可以使用new_tag值重新设定对应的FdEntry。
87
88如果不符合,说明检测到异常,后续进行对应的异常处理。
89
90**参数:**
91
92| 名称                       | 类型               | 说明                                                         |
93| -------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ |
94| `fd` | int | fd句柄,作为FdEntry的索引。 |
95| `expected_tag` | uint64_t | 期望的ownership tag值。     |
96| `new_tag` | uint64_t | 设置新的ownership tag值。   |
97
98
99
100### fdsan_close_with_tag
101
102```
103int fdsan_close_with_tag(int fd, uint64_t tag);
104```
105**描述:** 根据tag描述符关闭文件描述符。
106
107通过fd找到匹配的FdEntry。如果close_tag与tag相同,则符合预期,可以继续执行文件描述符关闭流程;否则,表示检测到异常。
108
109**参数:**
110
111| 名称                       | 类型               | 说明                                                         |
112| -------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ |
113| `fd` | int | 待关闭的fd句柄。 |
114| `tag` | uint64_t | 期望的ownership tag。     |
115
116**返回值:** 0或者-1,0表示close成功,-1表示close失败。
117
118### fdsan_get_owner_tag
119```
120uint64_t fdsan_get_owner_tag(int fd);
121```
122**描述:** 根据文件描述符获取tag信息。
123
124通过fd找到匹配的FdEntry,并获取其close_tag。
125
126**参数:**
127
128| 名称                       | 类型               | 说明                                                         |
129| -------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ |
130| `tag` | uint64_t | ownership tag。     |
131
132**返回值:** 返回对应fd的tag。
133
134### fdsan_get_tag_type
135```
136const char* fdsan_get_tag_type(uint64_t tag);
137```
138**描述:** 根据tag计算出对应的type类型。
139
140获取tag信息后,计算并获取对应tag的type信息。
141
142**参数:**
143
144| 名称                       | 类型               | 说明                                                         |
145| -------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ |
146| `tag` | uint64_t | ownership tag。     |
147
148**返回值:** 返回对应tag的type。
149
150### fdsan_get_tag_value
151```
152uint64_t fdsan_get_tag_value(uint64_t tag);
153```
154**描述:** 根据tag计算出对应的owner value。
155
156通过获取到的tag信息,通过偏移计算获取对应tag中的value信息。
157
158**参数:**
159
160| 名称                       | 类型               | 说明                                                         |
161| -------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ |
162| `tag` | uint64_t | ownership tag。     |
163
164**返回值:** 返回对应tag的value。
165
166## 4. 使用示例
167
168如何使用fdsan?这是一个简单的double-close问题:
169
170```
171#include <unistd.h>
172#include <fcntl.h>
173#include <hilog/log.h>
174#include <vector>
175#include <thread>
176
177void good_write()
178{
179    sleep(1);
180    int fd = open("log", O_WRONLY | O_APPEND);
181    sleep(3);
182    ssize_t ret = write(fd, "fdsan test", 11);
183    if (ret == -1) {
184        OH_LOG_ERROR(LOG_APP, "good write but failed?!");
185    }
186    close(fd);
187}
188
189void bad_close()
190{
191    int fd = open("/dev/null", O_RDONLY);
192    close(fd);
193    sleep(2);
194    // This close expected to be detect by fdsan
195    close(fd);
196}
197
198void functional_test()
199{
200    std::vector<std::thread> threads;
201    for (auto function : { good_write, bad_close }) {
202        threads.emplace_back(function);
203    }
204    for (auto& thread : threads) {
205        thread.join();
206    }
207}
208
209int main()
210{
211    functional_test();
212    return 0;
213}
214```
215上述代码中的`good_write`函数会打开一个文件并写入一些字符串,而`bad_close`函数中也会打开一个文件同时包含double-close问题,这两个线程同时运行执行情况如下图。
216
217![](./figures/fdsan-error-2.png)
218
219由于每次open返回的文件描述符(fd)是顺序分配的,进入主函数后第一个可用的fd是43。在`bad_close` 函数中,第一次open返回的fd也是43。关闭之后,43变成可用的fd。在`good_write`函数中,open返回了第一个可用的fd,即43。然而,由于`bad_close`函数中存在重复关闭问题,错误地关闭了另一个线程中打开的文件,导致写入失败。
220
221引入fdsan后,有两种检测方法:使用标准库接口或实现带有fdsan的函数接口。
222
223### 使用标准库接口
224
225标准库接口中,fopen、fdopen、opendir、fdopendir已集成fdsan。使用这些接口而非直接使用open有助于检测问题。例如,可以使用fopen替代open。
226
227```c
228#include <stdio.h>
229#include <errno.h>
230#define TEMP_FILE "/data/local/tmp/test.txt"
231
232void good_write()
233{
234    // fopen is protected by fdsan, replace open with fopen
235    // int fd = open(TEMP_FILE, O_RDWR);
236    FILE *f = fopen(TEMP_FILE, "w+");
237    if (f == NULL) {
238        printf("fopen failed errno=%d\n", errno);
239        return;
240    }
241    // ssize_t ret = write(fd, "fdsan test\n", 11);
242    int ret = fprintf(f, "fdsan test %d\n", 11);
243    if (ret < 0) {
244        printf("fprintf failed errno=%d\n", errno);
245    }
246    // close(fd);
247    fclose(f);
248}
249```
250### 日志信息
251使用fopen打开的每个文件描述符都需要有一个与之对应的 `tag` 。`fdsan` 在 `close` 时会检查关闭的 `fd` 是否与 `tag` 匹配,不匹配就会默认提示相关日志信息。下面是上述代码的日志信息:
252
253```
254# hilog | grep MUSL-FDSAN
25504-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
256```
257
258从这里的错误信息中可以看出,FILE接口的文件被其他人错误地关闭了。FILE接口的地址可以协助进一步定位。
259
260此外,可以在代码中使用`fdsan_set_error_level`设置错误等级error_level。设置为Fatal之后,如果fdsan检测到错误,会提示日志信息并crash生成堆栈信息,用于定位。下面是 error_level 设置为Fatal之后生成的crash堆栈信息:
261
262```
263Reason:Signal:SIGABRT(SI_TKILL)@0x0000076e from:1902:20010043
264Fault thread info:
265Tid:15312, Name:e.myapplication
266#00 pc 000e65bc /system/lib/ld-musl-arm.so.1(raise+176)(3de40c79448a2bbced06997e583ef614)
267#01 pc 0009c3bc /system/lib/ld-musl-arm.so.1(abort+16)(3de40c79448a2bbced06997e583ef614)
268#02 pc 0009de4c /system/lib/ld-musl-arm.so.1(fdsan_error+116)(3de40c79448a2bbced06997e583ef614)
269#03 pc 0009e2e8 /system/lib/ld-musl-arm.so.1(fdsan_close_with_tag+836)(3de40c79448a2bbced06997e583ef614)
270#04 pc 0009e56c /system/lib/ld-musl-arm.so.1(close+20)(3de40c79448a2bbced06997e583ef614)
271#05 pc 000055d8 /data/storage/el1/bundle/libs/arm/libentry.so(bad_close()+96)(f3339aac824c099f449153e92718e1b56f80b2ba)
272#06 pc 00006cf4 /data/storage/el1/bundle/libs/arm/libentry.so(decltype(std::declval<void (*)()>()()) std::__n1::__invoke[abi:v15004]<void (*)()>(void (*&&)())+24)(f3339aac824c099f449153e92718e1b56f80b2ba)
273#07 pc 00006c94 /data/storage/el1/bundle/libs/arm/libentry.so(f3339aac824c099f449153e92718e1b56f80b2ba)
274#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)
275#09 pc 00105a6c /system/lib/ld-musl-arm.so.1(start+248)(3de40c79448a2bbced06997e583ef614)
276#10 pc 000700b0 /system/lib/ld-musl-arm.so.1(3de40c79448a2bbced06997e583ef614)
277```
278此时,从crash信息中可以看到bad_close存在问题,同时crash中包含所有打开的文件,协助定位问题,提升效率。
279
280```
281OpenFiles:
2820->/dev/null native object of unknown type 0
2831->/dev/null native object of unknown type 0
2842->/dev/null native object of unknown type 0
2853->socket:[28102] native object of unknown type 0
2864->socket:[28103] native object of unknown type 0
2875->anon_inode:[eventpoll] native object of unknown type 0
2886->/sys/kernel/debug/tracing/trace_marker native object of unknown type 0
2897->anon_inode:[eventpoll] native object of unknown type 0
2908->anon_inode:[eventpoll] native object of unknown type 0
2919->/dev/console native object of unknown type 0
29210->pipe:[95598] native object of unknown type 0
29311->pipe:[95598] native object of unknown type 0
29412->socket:[18542] native object of unknown type 0
29513->pipe:[96594] native object of unknown type 0
29614->socket:[18545] native object of unknown type 0
29715->pipe:[96594] native object of unknown type 0
29816->anon_inode:[eventfd] native object of unknown type 0
29917->/dev/binder native object of unknown type 0
30018->/data/storage/el1/bundle/entry.hap native object of unknown type 0
30119->anon_inode:[eventpoll] native object of unknown type 0
30220->anon_inode:[signalfd] native object of unknown type 0
30321->socket:[29603] native object of unknown type 0
30422->anon_inode:[eventfd] native object of unknown type 0
30523->anon_inode:[eventpoll] native object of unknown type 0
30624->anon_inode:[eventfd] native object of unknown type 0
30725->anon_inode:[eventpoll] native object of unknown type 0
30826->anon_inode:[eventfd] native object of unknown type 0
30927->anon_inode:[eventpoll] native object of unknown type 0
31028->anon_inode:[eventfd] native object of unknown type 0
31129->anon_inode:[eventpoll] native object of unknown type 0
31230->anon_inode:[eventfd] native object of unknown type 0
31331->anon_inode:[eventpoll] native object of unknown type 0
31432->anon_inode:[eventfd] native object of unknown type 0
31533->anon_inode:[eventpoll] native object of unknown type 0
31634->anon_inode:[eventfd] native object of unknown type 0
31735->socket:[97409] native object of unknown type 0
31836->socket:[94716] native object of unknown type 0
31938->socket:[94720] native object of unknown type 0
32040->/data/storage/el1/bundle/entry_test.hap native object of unknown type 0
32141->socket:[95617] native object of unknown type 0
32242->/sys/kernel/debug/tracing/trace_marker native object of unknown type 0
32343->/dev/null FILE* 4155724704
32444->socket:[94737] native object of unknown type 0
32545->pipe:[95634] native object of unknown type 0
32646->pipe:[95634] native object of unknown type 0
32747->pipe:[95635] native object of unknown type 0
32849->pipe:[95636] native object of unknown type 0
32950->pipe:[95636] native object of unknown type 0
330```
331
332
333### 实现具有fdsan的函数接口
334
335除了使用标准库函数,还可以实现具有fdsan的函数接口。fdsan机制通过`fdsan_exchange_owner_tag`和`fdsan_close_with_tag`实现。`fdsan_exchange_owner_tag`设置fd的tag,fdsan_close_with_tag检查关闭文件时的tag。
336
337下面是一个实现具有fdsan功能的函数接口的示例:
338
339```cpp
340#include <errno.h>
341#include <stdio.h>
342#include <fcntl.h>
343#include <unistd.h>
344
345#include <utility>
346
347struct fdsan_fd {
348    fdsan_fd() = default;
349
350    explicit fdsan_fd(int fd)
351    {
352        reset(fd);
353    }
354
355    fdsan_fd(const fdsan_fd& copy) = delete;
356    fdsan_fd(fdsan_fd&& move)
357    {
358        *this = std::move(move);
359    }
360
361    ~fdsan_fd()
362    {
363        reset();
364    }
365
366    fdsan_fd& operator=(const fdsan_fd& copy) = delete;
367    fdsan_fd& operator=(fdsan_fd&& move)
368    {
369        if (this == &move) {
370            return *this;
371        }
372        reset();
373        if (move.fd_ != -1) {
374            fd_ = move.fd_;
375            move.fd_ = -1;
376            // Acquire ownership from the moved-from object.
377            exchange_tag(fd_, move.tag(), tag());
378        }
379        return *this;
380    }
381
382    int get()
383    {
384        return fd_;
385    }
386
387    void reset(int new_fd = -1)
388    {
389        if (fd_ != -1) {
390            close(fd_, tag());
391            fd_ = -1;
392        }
393        if (new_fd != -1) {
394            fd_ = new_fd;
395            // Acquire ownership of the presumably unowned fd.
396            exchange_tag(fd_, 0, tag());
397        }
398    }
399
400  private:
401    int fd_ = -1;
402
403    // Use the address of object as the file tag
404    uint64_t tag()
405    {
406        return reinterpret_cast<uint64_t>(this);
407    }
408
409    static void exchange_tag(int fd, uint64_t old_tag, uint64_t new_tag)
410    {
411        if (&fdsan_exchange_owner_tag) {
412            fdsan_exchange_owner_tag(fd, old_tag, new_tag);
413        }
414    }
415
416    static int close(int fd, uint64_t tag)
417    {
418        if (&fdsan_close_with_tag) {
419            return fdsan_close_with_tag(fd, tag);
420        }
421    }
422};
423```
424
425这里的实现中使用`fdsan_exchange_owner_tag`在开始时将fd与结构体对象地址绑定,然后在关闭文件时使用`fdsan_close_with_tag`进行检测,预期tag是结构体对象地址。
426
427在实现具有fdsan的函数接口后,可以使用该接口包装fd。
428
429```cpp
430#define TEMP_FILE "/data/local/tmp/test.txt"
431
432void good_write()
433{
434    // int fd = open(DEV_NULL_FILE, O_RDWR);
435    fdsan_fd fd(open(TEMP_FILE, O_CREAT | O_RDWR));
436    if (fd.get() == -1) {
437        printf("fopen failed errno=%d\n", errno);
438        return;
439    }
440    ssize_t ret = write(fd.get(), "fdsan test\n", 11);
441    if (ret == -1) {
442        printf("write failed errno=%d\n", errno);
443    }
444    fd.reset();
445}
446```
447
448此时运行该程序可以检测到另一个线程的double-close问题,详细信息可以<a href="#日志信息">参考日志</a>。同样也可以设置error_level为fatal,这样可以使fdsan在检测到crash之后主动crash以获取更多信息。
449
450## 5. close函数信号安全性说明
451在POSIX标准中,`close`函数原本被定义为信号安全函数(async-signal-safe),这意味着它可以安全地在信号处理函数(signal handler)中调用。然而,在集成了fdsan(File Descriptor Sanitizer)机制的系统实现中,这一性质发生了变化。
452
453由于fdsan的实现依赖于mmap系统调用,而`mmap`本身不是信号安全函数,这会导致close函数也不再是信号安全的。因此,在信号处理函数中避免使用 `close`,可以通过其他系统调用来实现相同功能。
454
455