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