概述
程序的内存安全性 取决于程序员和语言实现 维护程序数据运行时不变量 的能力. 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 标志来恢复它,以便用户可选择更长久保留旧行为 .
|