0
点赞
收藏
分享

微信扫一扫

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​​​的​​特别关注点​​.

举报

相关推荐

0 条评论