概述
程序的内存安全性
取决于程序员和语言实现
维护程序数据运行时不变量
的能力.
D编译器知道内置类型
(如数组和指针
)的运行时不变量
,并且可用编译时检查
来确保正确.这些检查对用户定义类型
并不总是足够的.为了可靠维护编译器
硬编码知识之外的不变量,D
程序员必须求助于手动验证@safe
代码和防御性
的运行时检查.
本DIP
提出了一种新的语言特性,@system
变量,来解决D的内存安全
系统中缺乏表现力问题.在@safe
代码中,不能直接写入@system
变量,也不能通过转换,重叠
,void
初化等不受控制
方式改变它们的值.因此,可依赖它们来存储受运行时不变量
约束的数据
.
内容
序号 | 内容 |
| 背景 |
| 基本原理 |
| 先前工作 |
| 描述 |
| 替代方案 |
| 重大更改和弃用 |
| 参考 |
| 版权和许可 |
| 审查 |
背景
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
标志来恢复它,以便用户可选择更长久保留旧行为
.