From 81999aaed380d8f76480fcb4287684e03df26d37 Mon Sep 17 00:00:00 2001 From: wuaoxiang Date: Fri, 5 Nov 2021 22:19:43 +0800 Subject: [PATCH] add P.UNS.MEM.02 add P.UNS.MEM.03 add P.UNS.MEM.04 add example to P.UNS.FFi.07 Signed-off-by: wuaoxiang --- .../coding_practice/unsafe_rust/ffi.md | 12 ++ .../coding_practice/unsafe_rust/mem.md | 125 ++++++++++++++++++ 2 files changed, 137 insertions(+) diff --git a/src/safe-guides/coding_practice/unsafe_rust/ffi.md b/src/safe-guides/coding_practice/unsafe_rust/ffi.md index ebc4e852..b1fca90f 100644 --- a/src/safe-guides/coding_practice/unsafe_rust/ffi.md +++ b/src/safe-guides/coding_practice/unsafe_rust/ffi.md @@ -149,7 +149,19 @@ FFi 接口使用的字符串要符合 C 语言约定,即使用 `\0` 结尾且 Rust 中字符串要求 `utf-8 `编码,而 C 字符串则没有这个要求。所以需要注意编码。 +【正例】 + +```rust +let f = libc::fopen("/proc/uptime\0".as_ptr().cast(), "r\0".as_ptr().cast()); +``` +【反例】 + +```rust +let f = libc::fopen("/proc/uptime".as_ptr().cast(), "r".as_ptr().cast()); +// 即使 /proc/uptime 文件存在,fopen 系统调用也会返回 NULL +// 并且将错误码 errno 标记为 2 ("No such file or directory") +``` ## P.UNS.FFi.08 从外部传入的不健壮类型的外部值要进行检查 diff --git a/src/safe-guides/coding_practice/unsafe_rust/mem.md b/src/safe-guides/coding_practice/unsafe_rust/mem.md index b729e8dd..6a280a36 100644 --- a/src/safe-guides/coding_practice/unsafe_rust/mem.md +++ b/src/safe-guides/coding_practice/unsafe_rust/mem.md @@ -158,3 +158,128 @@ pub unsafe trait Array: Sized { } ``` +## P.UNS.MEM.02 不能修改其它进程/动态库的存变量 + +### 【级别:必须】 + +必须按此规范执行。 + +### 【描述】 + +不要尝试修改其它进程/动态库的内存数据,否则会出现内存段错误(SIGSEGV)。 + +【反例】 + +`sqlite3_libversion()` 返回的 sqlite 版本信息指针指向 `/usr/lib/libsqlite3.so` 动态库的 static 字符串。 + +libsqlite3.so 中分配的静态字符串不属于进程的内存范围中。 + +当进程尝试修改 sqlite 动态库的静态字符串内容,操作系统就会发送 SIGSEGV 信号终止进程,以保证 sqlite 动态库的内存数据安全。 + +```rust +#[link(name = "sqlite3")] +extern "C" { + fn sqlite3_libversion() -> *mut std::os::raw::c_char; +} + +fn edit_sqlite_version() { + unsafe { + let mut sqlite_version = sqlite3_libversion(); + // SIGSEGV: invalid memory reference + *sqlite_version = 3; + } +} +``` + +## P.UNS.MEM.03 不能让 String/Vec 自动 Drop 其它进程/动态库的内存数据 + +### 【级别:必须】 + +必须按此规范执行。 + +### 【描述】 + +使用 String/Vec 指向其它进程/动态库的内存数据时,一定要手动禁止 String/Vec 的 Drop 方法(析构函数)的调用,避免 free 其它进程/动态库的内存数据。 + +【反例】 + +`sqlite3_libversion()` 返回的 sqlite 版本信息指针指向 `/usr/lib/libsqlite3.so` 动态库的 static 字符串。 + +当进程在 String drop 的时候尝试释放 sqlite 动态库的静态字符串内存时,操作系统就会发送 SIGABRT 信号终止进程,以保证 sqlite 动态库的内存数据安全。 + +```rust +#[link(name = "sqlite3")] +extern "C" { + fn sqlite3_libversion() -> *mut std::os::raw::c_char; +} + +fn print_sqlite_version() { + unsafe { + let ptr = sqlite3_libversion(); + let len = libc::strlen(ptr); + let version = String::from_raw_parts(ptr.cast(), len, len); + println!("found sqlite3 version={}", version); + // SIGABRT: invalid free + } +} +``` + +【正例】 + +除了用 mem::forget 或者 ManualDrop 禁止 String drop 其它动态库的内存,也可以用标准库 ptr/slice 的 copy 或者 `libc::strdup` 将 sqlite 的版本信息字符串**复制到当前进程的内存空间**再进行操作 + +```rust +fn print_sqlite_version() { + unsafe { + let ptr = sqlite3_libversion(); + let len = libc::strlen(ptr); + let version = String::from_raw_parts(ptr.cast(), len, len); + println!("found sqlite3 version={}", version); + // 手动禁止 String 的析构函数调用 + std::mem::forget(version); + } +} +``` + +## P.UNS.MEM.04 尽量用可重入(reentrant)版本的 C API/系统调用 + +### 【级别:必须】 + +必须按此规范执行。 + +### 【描述】 + +以 Linux 系统为例,在 **glibc**(/usr/lib/libc.so) 等知名 C 语言库中, + +很多 API 会既提供不可重入版本和**可重入(reentrant)**版本,例如 ctime 和 ctime_r 这对系统调用。 + +可重入版本的函数命名一般带 **_r** 的后缀,*_r* 也就是单词可重入 reentrant 的缩写。 + +libc 中不可重入函数的执行过程一般是将函数的输出写到动态库的某个 static 命令内,然后再返回指向该 static 变量的指针返回给调用方,因此是一种「有状态」的函数,多线程环境下可能有**线程安全问题**。 + +例如线程 A 正在将 glibc 动态库的 gmtime 数据逐个复制回来,结果复制到一半线程 B 调用 gmtime 把后半部分的 gmtime 输出数据给更新掉了导致线程 A 得到的数据有误。 + +而无重入版本例如 libc::localtime_r 会比 libc::localtime 多一个入参叫 result, + +允许调用方进程的内存空间内分配内存,再将调用方进程的可变指针传入到 glibc 中让 glibc 修改可知指针指向的数据。 + +应当通过工具搜索动态库的函数符号查找可重入版本的函数,或者通过 man 文档查询自己所用函数有没有可重入的版本。 + +``` +[w@ww repos]$ nm -D /usr/lib/libc.so.6 | grep "_r@" +00000000000bb030 W asctime_r@@GLIBC_2.2.5 +00000000000bb100 T ctime_r@@GLIBC_2.2.5 +0000000000040a30 T drand48_r@@GLIBC_2.2.5 +``` + +使用不可重入函数的危害例如 P.UNS.MEM.02 和 P.UNS.MEM.03 规范的反例中的 sqlite3_libversion() 会导致开发人员带来很大的心智负担,需要人工 code review 确保没有线程安全和内存安全问题,因此必须尽量使用可重入版本的函数。 + +【正例】 + +chrono 库中用 libc::localtime_r 获取本地时间而不用 libc::localtime + +ctime_r, gmtime_r, localtime_r, gethostbyname_r + +【反例】 + +ctime, gmtime, localtime, gethostbyname