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标记函数,允许安全检查器忽略可能违反内存安全重载符号或成员!特别是postblitopCast.

在此用@trusted逃逸仍然可以,但要非常小心.在考虑如何滥用此类函数时,请特别考虑可能包含指针类型.常见错误是标记区间函数或域用法为@trusted.请记住,大多数区间都是模板,并且在迭代类型有@system级的后传递(postblit)构造器/析构器,或从用户提供λ生成时,可很容易推导为@system.

使用@safe查找需要标记为@trusted的部分

有时期望@safe的模板,可能无法推导@safe,且不清楚原因.这时,暂时标记模板函数为@safe来查看编译器报错.如果合适,则插入@trusted逃逸,

有时,广泛使用的模板,标记为@safe可能会破坏太多.则在标记@safe的不同名下,复制模板,并更改要检查的调用,让它们调用替代模板.

考虑未来如何编辑该函数

编写信任(@信任)函数时,始终考虑给定API,如何使用调用它,并确保它应该是@safe.上面很好的示例是确保safeRead不接受指针数组.
但是,不安全代码潜入还可能是,有人稍后编辑函数一部分,使先前验证无效,从而需要重新检查整个函数.插入评论来解释部分更改会违反内存安全请记住,拉取请求差异并不总是显示整个环境,包括正在编辑的长函数@trusted!

用有确定生命期类型来封装@trusted操作

有时,资源只有在创建和/或析构时才有危险,但使用时很安全.可以把危险操作封装到类型的构造器和析构器中,并标记为@trusted,这样允许@safe代码在生命期间使用资源.当然要小心.

要禁止@safe代码找出实际资源,并绕过管理资源类生命期后保存一份副本!只要@safe代码有引用,就必须确保资源是活动的.

如,只要不能访问负载数据的原始指针,引用计数类型可完全安全.不能按@safe标记Dstd.typecons.RefCounted,因为它用别名本转移到受保护的分配结构以完成功能,调用该结构都不知道引用计数.复制该有效负载指针,然后释放结构后,就有悬挂指针了.

这不能是@safe!

有时,在很明显应禁止时,编译器却允许函数@safe或推导为@safe.
这由以下两种引起的:
1,@安全函数调用@信任标记但允许系统调用的函数,
2,@safe系统中存在错误或漏洞.
多数时候,是前者.@trusted是非常棘手,很难搞对的属性.
开发人员经常滥用@trusted.即使是核心D开发人员也会犯该错误!因此,推导为安全的模板函数也是,有时甚至很难找到原因.
即使发现根本原因后,一般也很难删除@trusted标记,因为它会破坏该函数的许多用户.但是,最好破坏期望内存安全承诺代码,而不是遭受可能内存破坏.越早弃用和删除标记越好.然后对证明安全的插入@信任逃逸.

如果确实是系统漏洞,请报告问题,或在D论坛提问.D社区一般乐于提供帮助,且内存安全是该语言的创建者WalterBright特别关注点.

posted @   zjh6  阅读(49)  评论(0编辑  收藏  举报  
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示