d中编写@信任代码
原文
编程中,有内存安全概念,在一定程度上保证不会导致破坏内存.内存安全的极致是可机械验证不会破坏内存.来防护缓冲区溢出等攻击.D语言定义内存安全为:允许编写相当多有用代码,但保守禁止粗略的.实践中,编译器并不是万能的,它缺乏人类非常擅长,看到的环境,因此经常需要允许有风险的行为.
因为编译器在内存安全方面非常严格,所以需要@信任.
先讨论内存安全和D的@安全机制.
什么是内存安全代码?
最简单方法是检查导致不安全代码原因.在静态类型语言中,一般有3种主要方法可违反安全:
1,从有权访问的有效内存段外缓冲区读写.
2,允许按指针转换非指针内存值.
3,用悬挂或不再有效指针.
在D中,第一项很容易实现:
auto buf = new int[1];
buf[2] = 1;
使用默认检查边界,即使不检查安全代码,运行时也会导致异常.但是D允许访问数组指针来绕过它:
buf.ptr[2] = 1;
对第二点,只需要用转换:
*cast(int*)(0xdeadbeef) = 5;
第三个也相对简单:
auto buf = new int[1];
auto buf2 = buf;
delete buf;//设置`buf`为`null`
buf2[0] = 5;//但不是`buf2`.
悬挂指针也经常是指向不再使用的栈数据:
int[] foo()
{
int[4] buf;
int[] result = buf[];
return result;
}
总之,安全代码避免导致破坏内存.为此,必须遵守一些规则.
注意:在D中,解引用用户空间中的null指针不算内存安全问题.为什么呢?
因为这会触发硬件异常,且一般不会使程序处于破坏内存的未定义状态.它只是中止程序.对用户或程序员似乎是不可取的,但在防止利用漏洞方面非常好.如果null指针指向非常大的内存空间,则null指针有潜在内存问题.但对安全D,这需要异常大的结构才开始担心它.因而为检查null罕见情况,而检测解引用指针,导致的性能下降是不值得的.
D的@safe规则
D提供了标记编译器机械检查函数的@safe属性,来避免内存安全问题.当然,有时,需要异常处理.
如下规则旨在防止上述问题规范.
1,禁止更改原始指针值.如果@safeD代码有指针,它只能访问指向值,不能访问包括索引指针的其他值.
2,禁止转换指针为除void*外类型.禁止把非指针类型转换为指针类型.只要有效,允许其他强制转换(如从浮强制转换为整).也允许动态数转换为空[].
3,禁止访问与其他类型重叠的指针类型的联合.类似上面的1和2规则.
4,访问动态数组(a)中元素或从(a)中取切片,必须是编译器证明安全的,或运行时检查边界.甚至在忽略检查边界的发布模式下时(注意:dmd的选项-boundscheck=off(b)会覆盖它,所以使用(b)时要格外小心).
5,在普通D中,可切片指针来从指针创建动态数组.在@safeD中,这是禁止的,因为编译器不知道通过该指针实际有多少可用空间.
6,禁止取局部变量或(栈上变量的)函数参数的指针或取引用参数指针.例外是切片本地静态数组,包括上面的foo函数.这是已知问题(可能已修复).
7,禁止在是或包含引用间,显式转换不变和可变类型.在不变和可变间可隐式转换值类型且非常好.
8,禁止在是或包含引用间,显式转换线本和共享类型.同样,转换值类型很好(且可隐式完成).
9,@safe代码中禁止D的内联汇编功能.
10,禁止抓不是从异常类继承的对象.
11,D中,默认初化所有变量.但是,可用空初化器不初化它:
int *s = void;
@safeD中禁止该用法.上面指针会指向随机内存并成为明显的悬挂指针.
12,__gshared变量是仍在全局空间中的静态shared.一般用于与C代码交互.@safeD中禁止访问此类变量.
13,禁止使用动态数组的ptr属性(编译器在2.072版本中发布的新规则).
14,禁止赋值切片另一void[]来写入void[]数据(此规则也是在2.072中发布的新规则).
15,@safeD只能调用@安全函数或推导为@safe函数的函数.
需要@trusted
上述规则可很好防止破坏内存,但会阻止许多有效且安全代码.如,考虑想用read系统调用函数,原型如下:
ssize_t read(int fd, void* ptr, size_t nBytes);
该函数,会从给定文件描述符中读取数据,并把它其入ptr指向的缓冲区中,且期望为nBytes字节长.它返回实际读取字节数,如果错误,则返回负值.
使用此函数来读数据至栈分配的缓冲区类似:
ubyte[128] buf;
auto nread = read(fd, buf.ptr, buf.length);
如何在@safe函数中完成?在@safe代码中使用read的主要问题是指针只能传递一个值,这里是一个ubyte.read期望存储缓冲区的更多字节.在D中,一般按动态数组传递待读取数据.
但是,read不是D代码,并且使用了常见的C习惯用法,即分别传递缓冲区和长度,因此无法标记为@safe.考虑以下@safe代码调用:
auto nread = read(fd, buf.ptr, 10_000);
该调用绝对不安全.仅在理解read函数且调用环境,确保不会写缓冲区外内存时,是安全的.
为此,D提供了@trusted属性,告诉编译器函数内部代码假定为@safe,但不要机械检查.开发人员负责确保代码是@safe的.
解决问题的函数在D中可能如下所示:
auto safeRead(int fd, ubyte[] buf) @trusted
{
return read(fd, buf.ptr, buf.length);
}
//标记为@信任
每当标记整个函数为@trusted时,请考虑是否可从会危及内存安全的环境中调用它.是,则一定不要标记为@trusted.即使想只按安全方式调用它,编译器也不会阻止别人不安全使用它.safeRead应可从@safe环境中调用很好,所以标记为@trusted.
safeRead函数的更自由API可取void[]数组作为缓冲区.然而,在@safe代码中,可转换动态数组为包括指针数组的void[]数组.读文件数据进指针数组可能会导致悬挂指针数组.因此要使用ubyte[].
@trusted逃逸
@trusted逃逸是允许如不暴露不安全调用给程序其他部分的@系统(D不安全默认值)调用的单个表达式.无需编写safeRead函数,可在@safe函数这样:
auto nread = ( () @trusted => read(fd, buf.ptr, buf.length) )();
仔细看看逃逸,看看发生了什么.D允许用()=>expr语法,声明计算并返回单个表达式的λ函数.为了调用λ函数,附加括号到λ.但是,符号优先级会应用括号至表达式而不是λ,因此必须用()包装整个λ来调用.最后,用@trusted标记λ,因此外围@safe环境可调用它.
除了简单λ外,还可用整个嵌套函数或多语句λ.但是,要尽量减少这类代码.
@trusted的经验法则
示例表明,标记为@trusted有巨大影响.如果禁止检查内存安全,但允许@safe代码调用它,则你必须确保它不会破坏内存.如下规则指导何处放@trusted标记并避免陷入麻烦:
1,保持@trusted代码尽量小
从不机械检查@trusted代码安全,因此必须检查每一行的正确性.因而,始终建议保持@trusted代码尽量小.
2,不安全调用泄漏时,标记整个函数为@trusted
如果泄漏,最好把整个都标记为@trusted,这样更符合事实,再每行检查.这不是硬性规定;如,即使它会影响稍后按@safe模式函数使用的数据,前面示例中的read调用是完全安全的.
在函数开头用C的malloc分配的指针,然后在释放(free)前,可能已被复制到其他地方.这里,悬挂指针可能会违反@safe,即使机械检查也是如此.相反,按@trusted包装使用指针的整个部分,甚至整个函数.或,使用域保护来保证数据生命期,直到函数结束,域保护.
在接受任意类型的模板函数上永远不要用@trusted
D足够聪明,对遵循规则的模板函数,包括模板类型成员函数,可推导出@safe.
让编译器完成工作.为确保在正确环境中,该函数为@safe,请创建@safe的单元测试来调用它.@trusted标记函数,允许安全检查器忽略可能违反内存安全的重载符号或成员!特别是postblit和opCast.
在此用@trusted逃逸仍然可以,但要非常小心.在考虑如何滥用此类函数时,请特别考虑可能包含指针类型.常见错误是标记区间函数或域用法为@trusted.请记住,大多数区间都是模板,并且在迭代类型有@system级的后传递(postblit)或构造器/析构器,或从用户提供λ生成时,可很容易推导为@system.
使用@safe查找需要标记为@trusted的部分
有时期望@safe的模板,可能无法推导为@safe,且不清楚原因.这时,暂时标记模板函数为@safe来查看编译器报错.如果合适,则插入@trusted逃逸,
有时,广泛使用的模板,标记为@safe可能会破坏太多.则在标记@safe的不同名下,复制模板,并更改要检查的调用,让它们调用替代模板.
考虑未来如何编辑该函数
编写信任(@信任)函数时,始终考虑给定API,如何使用调用它,并确保它应该是@safe.上面很好的示例是确保safeRead不接受指针数组.
但是,不安全代码潜入还可能是,有人稍后编辑函数一部分,使先前验证无效,从而需要重新检查整个函数.插入评论来解释部分更改会违反内存安全请记住,拉取请求差异并不总是显示整个环境,包括正在编辑的长函数是@trusted!
用有确定生命期类型来封装@trusted操作
有时,资源只有在创建和/或析构时才有危险,但使用时很安全.可以把危险操作封装到类型的构造器和析构器中,并标记为@trusted,这样允许@safe代码在生命期间使用资源.当然要小心.
要禁止@safe代码找出实际资源,并绕过管理资源类生命期后保存一份副本!只要@safe代码有引用,就必须确保资源是活动的.
如,只要不能访问负载数据的原始指针,引用计数类型可完全安全.不能按@safe标记D的std.typecons.RefCounted,因为它用别名本转移到受保护的分配结构以完成功能,调用该结构都不知道引用计数.复制该有效负载指针,然后释放结构后,就有悬挂指针了.
这不能是@safe!
有时,在很明显应禁止时,编译器却允许函数为@safe或推导为@safe.
这由以下两种引起的:
1,@安全函数调用@信任标记但允许系统调用的函数,
2,@safe系统中存在错误或漏洞.
多数时候,是前者.@trusted是非常棘手,很难搞对的属性.
开发人员经常滥用@trusted.即使是核心D开发人员也会犯该错误!因此,推导为安全的模板函数也是,有时甚至很难找到原因.
即使发现根本原因后,一般也很难删除@trusted标记,因为它会破坏该函数的许多用户.但是,最好破坏期望内存安全承诺代码,而不是遭受可能内存破坏.越早弃用和删除标记越好.然后对证明安全的插入@信任逃逸.
如果确实是系统漏洞,请报告问题,或在D论坛提问.D社区一般乐于提供帮助,且内存安全是该语言的创建者WalterBright的特别关注点.
浙公网安备 33010602011771号