概述
程序的内存安全性取决于程序员和语言实现维护程序数据运行时不变量的能力. 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);
enum Opt {a, b, c}
@system Opt opt = Opt.a;
先前工作
为了实现内存安全,需要封装数据/限制访问数据. 私是为了抽象(防止用户依赖细节),而不是保护(确保不变量始终成立).可用于保护只是意外.
描述
@system的现有规则 在更改提议前,先概述了现有规则下哪些声明可有@system属性:
@system int w = 2;
@system enum int x = 3;
enum E
{
@system x,
y,
}
@system alias x = E;
@system template T() {}
void func(@system int x)
{
@system int x;
}
template Temp(@system int x) {}
可附加函数属性到变量声明中,但不能提取它们:
@system @nogc pure nothrow int x;
pragma(msg, __traits(getFunctionAttributes, x));
pragma(msg, __traits(getAttributes, x));
提议变更
(0),@safe代码中禁止访问@system标记变量或字段. 包括读写变量.尽管可允许读取具有@system安全类型变量,但限制它,使规则更简单. 示例:
@system int x;
struct S
{
@system int y;
}
S s;
@safe
void main()
{
x += 10;
s.y += 10;
int y = x;
@system int z;
z += 1;
}
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;
increment(xAlias);
checkX(&x);
}
@safe代码中中允许初化@system变量或字段.包括静态初化,自动生成构造器,用户定义构造器和类型的.init值.
@system int x;
shared static this() @safe
{
x = 3;
x = 3;
}
struct T
{
@system int y;
@system int z = 3;
this(int y, int z) @safe
{
this.y = y;
this.y = y;
this.z = z;
}
}
struct S
{
@system int y = 2;
}
void main() @safe
{
S s0 = {y: 3};
S s1 = S(3);
S s2 = S.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);
() @trusted {@system int x = 3;}();
}
@system int x = (() @trusted => 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;
int* x2 = cast(int*) 0x8035FDF0;
int* x3 = getPtr();
int x4 = getVal();
@system int x5 = 1;
@trusted int* x6 = getPtr();
@safe int* x7 = getPtr();
struct S {
int* x9 = x3;
int x8 = x5;
}
编译器知道结果值是安全的时,可对不安全类型搞例外.
int* getNull() pure @system {return null;}
int* n = getNull();
(@system{})域或(@system:)冒号注解类似影响函数一样,会影响变量.
@system
{
int y0;
}
@system:
int y1;
语法变化
在这个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;
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标志来恢复它,以便用户可选择更长久保留旧行为.
|