概述

程序的​​内存安全性​​取决于​​程序员和语言实现​​维护程序数据​​运行时不变量​​的能力.

D编译器知道​​内置类型​​(如​​数组和指针​​)的运行时​​不变量​​,并且可用​​编译时检查​​来确保正确.这些检查对​​用户定义类型​​并不总是足够的.为了可靠维护​​编译器​​硬编码知识之外的不变量,​​D​​程序员必须求助于手动验证​​@safe​​代码和​​防御性​​的运行时检查.

本​​DIP​​提出了一种新的语言特性,​​@system​​变量,来解决D的​​内存安全​​系统中缺乏表现力问题.在​​@safe​​代码中,不能直接写入​​@system​​变量,也不能通过​​转换,重叠​​,​​void​​初化等​​不受控制​​方式改变它们的值.因此,可依赖它们来存储受​​运行时不变量​​约束的​​数据​​.

内容

序号

内容

​1​

背景

​2​

基本原理

​3​

先前工作

​4​

描述

​5​

替代方案

​6​

重大更改和弃用

​7​

参考

​8​

版权和许可

​9​

审查

背景

D​​内存安全​​系统区分了​​安全值和不安全值​​,可在​​@safe​​代码中自由使用安全值,而不会导致未定义行为,但不能自由使用​​不安全值​​.只有​​安全值​​类型是​​安全类型​​;同时具有​​安全和不安全值​​类型是​​不安全类型​​.(更详细定义,请参阅​​D语言规范​​的​​函数安全​​部分.)

D编译器内置知道​​哪些类型​​是安全的,哪些不是.从广义上讲,​​指针,数组和其他引用类型​​是不安全的;​​整数,字符和浮点数​​是安全的;​​聚集​​类型的安全性取决于​​其成员​​的安全性.

类型的​​运行时不变量​​(或仅"​​不变量​​")是区分该类型​​安不安全​​的规则.(注意本​​DIP​​中的"不变量"并不是指​​合约编程​​中的​​不变量块​​),满足​​不变量​​值是安全的;否则,不安全.因此,带​​运行时不变量​​类型都是​​不安全​​的,不带的,则安全.

为确保​​不违反​​它们的不变量,在​​@safe​​代码中,限制了​​不安全类型​​:

1,不能空初化他们.

2,不能在联中​​重叠​​它们.

3,当​​U​​是不安全类型时,不能转换​​T[]​​为​​U[]​​.

基本原理

尽管上述系统对​​内置类型及其不变量​​​工作,但它未对​​程序员​​​提供方法来指示​​用户定义类型​​​有编译器不知道的​​附加不变量​​​.因此,维护这样不变量需要​​程序员​​​付出​​额外努力​​​.对​​不安全类型​​​,程序员要​​手动验证​​​这些​​不变量​​​是否在​​@safe​​​代码中维护.对​​安全类型​​​,程序员还要插入​​防御性运行时检查​​​来确保​​维护这些不变量​​.

示例:用户定义切片

module intslice;

struct IntSlice
{
private int* ptr;
private size_t length;

@safe
this(int[] src)
{
ptr = &src[0];
length = src.length;
}

@trusted
ref int opIndex(size_t i)
{
if (i >= length) assert(0);
return ptr[i];
}
}

不变量:​​length​​值必须等于​​ptr指向​​的​​数组长度​​.

首先,注意到,此代码编写时是​​内存安全​​的(越界访问).只有两个函数可直接访问​​ptr​​和​​length​​,且都正确地维护了​​不变量​​.

然而,为了证明这段代码是​​内存安全​​的,程序员不能仅验证​​@trusted​​函数正确性.相反,必须手动检查每个涉及​​.ptr​​和​​length​​的函数.

如果​​ptr​​和​​length​​是​​@system​​变量,则直接访问它们的代码都必须是​​@trusted​​,程序员不必手动验证​​@safe​​代码来证明维护了​​IntSlice​​的不变量.

在其他不变量涉及​​两个或多个​​变量间关系的​​用户定义类型​​中也有相同模式,如​​标记联和引用计数智能指针​​.

示例:短串

module shortstring;

struct ShortString
{
private ubyte length;
private char[15] data;

@safe
this(const(char)[] src)
{
assert(src.length <= data.length);

length = cast(ubyte) src.length;
data[0 .. src.length] = src[];
}

@trusted
const(char)[] opIndex() const
{
// 应可跳过检查边界
return data.ptr[0 .. length];
}
}

不变量:​​length<=15​

再一次,有个建立​​不变量​​的构造器,及​​依赖不变量​​来干活的​​成员函数​​.然而,与前例不同,这段代码在​​编写时​​并不是​​内存安全​​的,尽管看起来是.

为此,考虑以下程序,该程序在​​@safe​​代码中使用​​ShortString​​,导致未定义行为:

@safe
void main()
{
import shortstring;
import std.stdio;

ShortString oops = void;
writeln(oops[]);
}

​空​​初化​​ShortString​​很可能会产生违反其​​不变量​​的实例.因为​​opIndex​​依赖​​该不变量​​来跳过​​检查边界​​,所以会导致​​越界访问内存​​,而不是​​安全,可预测​​的崩溃.

为什么​​编译器​​允许在​​@安全​​代码中​​空初化​​一个​​ShortString​​?因为,根据​​语言规范​​,只包含​​正字节​​和​​符​​数据的​​构​​是​​安全类型​​,因此​​不能​​有​​不变量​​.因此,​​@安全​​代码可自由初化​​短串​​为包括​​未指定值​​的任意值,而不会​​损坏内存​​.

为使代码​​内存安全​​,程序员必须在​​opIndex​​中包含额外​​检查边界​​:

@safe
const(char)[] opIndex() const
{
return data[0 .. length];
}

解决方案不能令人满意:程序必须在​​运行时​​做多余工作来弥补​​语言表达能力​​不足,或者放弃​​@safe​​.如果可标记​​ShortString.length​​为​​@system​​,则不会存在该困境.

同样可应用在​​用户定义类型​​上,来对编译器认为"​​安全​​"的类型施加​​不变量​​,如在​​终开关​​语句中的​​enum​​类型和​​外部库​​按数组索引使用的​​句柄整数​​.

示例:​​int​​作为指针

有时需要对​​外部库​​使用的句柄​​强制​​域语义.此类类型示例是:

​Unix​​文件描述符,​​OpenGL​​对象名,在​​WebAssembly​​上下文中​​JS​​对象句柄.

他们按简单​​int​​或​​uint​​类型表示,但因为它们引用​​可分配或释放​​的资源,作用类似​​指针​​.但是,当类型没有指针时,将忽略​​scope​​.因为​​int​​是安全类型,所以都可从​​@safe​​代码中创建​​int​​值,因此从​​域整​​逃逸导致的​​内存损坏​​也可,由​​不访问变量​​就创建相同​​整​​值而产生.即使在​​构​​中包装​​整​​,也不会检查​​域​​:

struct File
{
private int fd;
}

File gFile;

@safe void escape(scope File f)
{
gFile = f; // 允许
}

也可用指针把句柄放在​​union​​​中,但这会不必要地增加结构大小到​​size_t.sizeof​​​.涉及到​​@安全​​​代码中的限制时,最好按指针表示​​int fd;​​.

全局变量的初值

允许标记聚集字段为​​@system​​​,帮助编译器维护​​用户定义类型​​​的​​运行时不变量​​​,但确保变量不是用​​不安全值​​​开始构造也很重要.禁止在​​@safe​​​函数中构造不安全值,且在​​@system​​​和​​@trusted​​​函数中​​构造它们​​​,就是程序员​​负责​​​内存安全.在​​@safe​​​函数中访问​​不安全类型的全局变量​​​时,编译器应保守并​​拒绝访问​​​,或​​检查​​基本缺陷:

int* x = cast(int*) 0xDEADBEEF;
extern int* y;
int* z = new int(20);

void main() @safe
{
*x = 10; // 禁止
*y = 10; // 禁止
*z = 10; // 可能允许
}

由于在​​@safe​​函数中禁止初化​​cast(int*)0xDEADBEEF​​表达式,且由于y的初值未知,编译器应按​​可能包含不安全的值​​注解​​x和y​​变量,因此无法在​​@safe​​函数中访问它们.这时,只有z已知具有​​安全初值​​,因此在​​@safe​​代码中,编译器可​​允许​​访问它.

当程序员想​​放松​​约束时,允许应用​​@trusted和@安全​​到​​变量​​上很有用,而​​@系统​​对​​加强​​约束很有用.

@trusted int* x = cast(int*) 0xD000; 
//假设是个好的地址
@safe extern int* y0;
//假定总是有安全值
@system extern int* y1;
//可能有不安全的值
@system int* z = new int(20);
//开始安全,但可能在`@trusted`代码中设置为不安全
enum Opt {a, b, c}
@system Opt opt = Opt.a;
//@trusted`代码依赖,它在区间,而不是`cast(Opt)100`.

先前工作

为了实现内存安全,需要​​封装数据/限制​​访问数据.

​私​​是为了抽象(防止用户依赖细节),而不是​​保护​​(确保​​不变量​​始终成立).可用于​​保护​​只是意外.

描述

​@system​​的现有规则

在更改提议前,先概述了现有规则下​​哪些​​声明可有​​@system​​属性:

@system int w = 2; 
//编译,闲着
@system enum int x = 3;
//编译,闲着
enum E
{
@system x,
//错误:`@system`不是枚举成员的有效属性,
y,
}
//编译,闲着
@system alias x = E;
//编译,闲着
@system template T() {}
void func(@system int x)
//错误:不支持函数参数的`@system`属性
{
@system int x; // 编译,闲着
}
template Temp(@system int x) {}
//错误:需要基本类型,而不是@

可附加函数属性到​​变量​​声明中,但不能提取它们:

@system @nogc pure nothrow int x;
pragma(msg, __traits(getFunctionAttributes, x)); //错误:第一个参数不是函数
pragma(msg, __traits(getAttributes, x)); // tuple()

提议变更

(0),​​@safe​​代码中禁止访问​​@system​​标记​​变量或字段​​.

包括读写变量.尽管可允许读取具有​​@system​​安全类型变量,但限制它,使规则更简单.

示例:

@system int x;

struct S
{
@system int y;
}

S s;

@safe
void main()
{
x += 10;//错误:无法修改x`@system`变量
s.y += 10;//错误:无法修改y字段`@系统`变量
int y = x;//错误:无法读x`@system`变量
@system int z;
z += 1; //错误:无法修改z`@system`变量
}
//按`@system`函数推导
auto foo()
{
x = 0;
}

​@safe​​代码中进一步禁止​​@系统变量或字段​​的操作是:

1,用​​&​​创建指向它的可变指针.

2,按​​参数​​传递给有​​ref​​无​​const​​标记的函数参数

3,无​​const​​按​​ref​​返回

用​​@system​​​变量别名时,​​别名​​​与符号​​限制​​相同.

@system int x = 3;
alias xAlias = x;

void increment(ref int x) @safe
{
x++;
}

void checkX(const(int)* x) @safe
{
assert(*x < 10);
}

@safe
void main()
{
xAlias += 1;//错误,不能修改x`@system`变量
increment(xAlias);
//错误,不能取x`@system`变量可变引用
checkX(&x);//错误,即使指针是`常`且`typeof(x)`是安全类型.
}

​@safe​​​代码中中允许初化​​@system​​​变量或字段.包括​​静态初化,自动生成构造器,用户定义构造器​​​和类型的​​.init​​值.

@system int x;

shared static this() @safe
{
x = 3;//允许,这是初化
x = 3;//第二次禁止,这是赋值`@system`变量
}

struct T
{
@system int y;
@system int z = 3; // 允许
this(int y, int z) @safe
{
this.y = y;//允许,这是初化
this.y = y;
//第二次禁止,这是赋值给`@system`变量
this.z = z;//禁止,这是赋值
}
}

struct S
{
@system int y = 2;
}

void main() @safe
{
S s0 = {y: 3}; //静态初化
S s1 = S(3); //自动生成构造器
S s2 = S.init; // `.init`初值
S s3; // 同上
s3 = s2; //禁止
}

请注意,虽然可能需要在初化​​@system​​变量附近需要注解​​@trusted​​,但因为无​​@trusted​​赋值语法,不能实现它.​​@trusted​​作为​​函数注解​​有其局限性:

1,它不适用于​​全局或局部​​变量,因为​​@trusted​​的​​λ​​会​​移动​​声明到​​该函数​​的域.

2,它不仅信任初化​​=​​左侧变量,还信任​​=右侧​​的​​初化表达式​​.用​​@trusted​​函数按​​ref​​返回变量并为其赋值,不算​​初化​​该变量.

3,它禁用​​-dip1000​​检查​​scope/return scope​​.

struct S
{
this(ref scope S s) @system
{
*(cast(int*) 0xDEADBEEF) = 0;
}
}

struct Wrapper(T)
{
@system T t;
this(T t) @trusted
{
this.t = t;//哎呀!调用`@系统`复制构造器
}
}

void main() @safe
{
auto w = Wrapper!S(S.init);//`11`信号杀死程序

() @trusted {@system int x = 3;}();
//x不再在域内
}

@system int x = (() @trusted => 3)();
//仍未标记赋值`@trusted`
//() @trusted {@system int x = 3;}();//不管用

​(1)​​,至少有一个​​@system​​字段的聚集是​​不安全类型​

这种聚集有与​​@安全​​代码中指针类型​​相同​​限制,使得不能用​​数组转换​​等隐式写入​​@系统​​变量.即使聚集不包含​​指针​​成员,也不会去掉​​域​​.

struct Handle
{
@system int handle;
}

void main() @safe
{
Handle h = void; // 错误
union U
{
Handle h;
int i;
}
U u;
u.i = 3; // 错误

ubyte[Handle.sizeof] storage;
auto array = cast(Handle[]) storage[]; // 错误

scope Handle h0;
static Handle h1 = h0; // 禁止
}

​(2)​​除非初值不是​​@safe​​,无注解的​​变量和字段​​都是​​@safe​​.

关于​​变量和字段​​规则如下:

1,按​​@system​​推导​​(()=>x)​​时,​​初化​​表达式x是​​@system​​函数.

2,按​​@system​​标记时,与​​类型​​无关,结果始终​​@system​​.

3,按​​@trusted​​标记时,按​​(()@trusted=>x)​​对待​​初化表达式​​x.

4,​​@safe​​标记时,​​初化表达式​​必须是​​@safe​​.

5,无注解时,仅当​​类型不安全​​且初化表达式为​​@system​​时结果为​​@system​​.

int* getPtr() @system {return cast(int*) 0x8035FDF0;}
int getVal() @system {return -1;}

extern int* x0;//默认@安全
int* x1 = x0;// @safe, (() => x0)是@safe
int* x2 = cast(int*) 0x8035FDF0; // @system, (() => cast(int*) 0x8035FDF0)是@system
int* x3 = getPtr();// @system, (() => getPtr())是@system
int x4 = getVal();// @safe,整是安全的
@system int x5 = 1; //@system,请求的
@trusted int* x6 = getPtr();// @safe,信任的
@safe int* x7 = getPtr();//错误,不能用@系统初化@安全
struct S {
// 字段,规则一样.
int* x9 = x3; // @system
int x8 = x5; // @safe
}

编译器知道​​结果值​​​是​​安全​​​的时,可对不安全类型搞​​例外​​.

int* getNull() pure @system {return null;}
int* n = getNull();
//尽管有`@system`初化表达式的不安全类型,按`@safe`推导

​(@system{})​​​域或​​(@system:)​​​冒号注解类似影响​​函数​​​一样,会影响​​变量​​.

@system
{
int y0; // @system
}

@system:
int y1; // @system

语法变化

在这个​​DIP​​​需要的地方已经允许放​​@system​​注解,所以无语法变化.

替代方案

用​​private​​

有人建议,在​​@safe​​代码中应禁止用比如​​tupleof​​或​​__traits(getMember)​​绕过​​private​​.

虽然提供确保​​@safe​​代码中的​​构​​不变量方法,符合本​​DIP​​,但反对用​​private​​.

首先,在​​@safe​​代码中禁止绕过​​private​​,并不确保​​用户定义类型​​的​​运行时不变量​​.当聚集无​​不安全类型​​成员时,仍可通过​​联中重叠,空初化或数组转换​​来间接写入​​私​​字段.

其次,​​private​​仅作用于​​模块级别​​,除非手动验证不违反​​@safe​​模块中其他代码,​​@trusted​​成员函数不能假定维护了结构的​​不变量​​.这​​削弱​​了程序员​​轻松区分​​需要​​手动和可自动检查​​代码能力,特别是因为某些成员函数(​​如构造器,析构器和重载符号​​)必须在​​同一模块​​中定义​​操作数据​​.

最后,禁止用​​__traits(getMember,...)​​或​​.tupleof​​绕过可见性会破坏依赖于此的​​@safe​​代码,并且​​15371问题​​明确请求此行为.

用不变块指定不安全类型

有人建议添加​​invariant​​​块使​​构​​​变成​​不安全类型​​.

struct Handle
{
invariant
{
//无没有运行时检查,只是标记`句柄`为不安全类型
}
private int fd;
}

然而,合约编程是​​内存安全​​​外单独功能,空​​不变{}​​​块像是​​可安全移除​​​的.突然用​​不变​​​块引入​​@safe​​​限制和​​scope​​​语义不行.最重要的是,它仍不能防止​​@trusted​​代码之外的修改.

重大更改和弃用

已允许附加​​@system​​​属性到变量,但不会增加​​编译器检查​​​.此提案中额外检查​​@system​​​变量可能会导致现有​​@safe​​​代码中断(注意,​​@system​​​代码完全不受此​​DIP​​​影响).然而,由于​​@系统变量​​​目前不做事情,作者怀疑​​用户​​​根本不会​​添加​​​该属性到变量中,更不用说在​​@safe​​​代码中变量了.最大风险是​​变量​​​意外落入​​@system{}​​​块内或​​@system:​​节下.

@system:

int x;//突然在`@safe`代码中不再可写
void unsafeFuncA() {};
void unsafeFuncB() {};

void main() @safe
{
x++; //不再允许了
}

在新规则下​​误构造​​​指针,可推导为​​@system​​.

struct S
{
int* a = cast(int*) 0x8035FDF0;
}

void main() @safe
{
S s;
*s.a = 0;//现在给出错误
}

每当此时,有可能​​内存损坏​​,因此出现​​编译器错误​​.

尽管如此,还是提出了​​两年​​弃用期,而不是​​触发​​错误,在破坏​​新内存安全规则​​时给出​​弃用​​消息.还可添加​​-preview=systemVariables​​预览标志,立即触发​​违规​​错误,按警告​​对待​​其他​​弃用​​消息.预览期结束时,还有​​-revert=systemVariables​​标志来恢复它,以便用户可选择更长久保留​​旧行为​​.