是否可以以编程方式评估任意代码的安全性?


10

最近我一直在考虑安全代码。线程安全的。内存安全。不会在带有段故障保险柜的情况下爆炸。但是为了清楚起见,让我们使用Rust的安全模型作为我们的定义。

通常,确保安全性是网络的一个大问题,因为正如Rust的需求所证明unsafe,存在一些非常合理的编程思想(例如并发性),如果不使用关键字就无法在Rust中实现unsafe。尽管并发可以由具有锁,互斥,渠道和内存隔离或者你有什么绝对安全的,这需要工作以外的铁锈的安全模式unsafe,然后手动确保编译器,“是的,我知道我在做什么看起来不安全,但是我已经从数学上证明了它是绝对安全的。”

但这通常归结为手动为这些事物建模并通过定理证明者证明它们是安全的。从计算机科学的角度(可能)和实用性的角度(是否要占用宇宙的生命),可以想象一个程序以任意语言使用任意代码并评估它是否是“防锈”?

注意事项

  • 一个简单的提示就是指出程序可能正在停止,因此停止问题使我们无法正常工作。假设提供给阅读器的任何程序都可以停止
  • 虽然“以任意语言编写任意代码”是目标,但我当然知道,这取决于程序对所选语言的熟悉程度,我们将以此为前提

2
任意代码?不。我想您可能因为I / O和硬件异常而无法证明大多数有用代码的安全性。
Telastyn

7
您为什么不理会暂停问题?您提到的每个例子,以及更多例子,都被证明等同于解决暂停问题,函数问题,莱斯定理或其他许多不确定性定理:指针安全,内存安全,线程安全性,异常安全性,纯净度,I / O安全性,锁安全性,进度保证等。暂停问题是您可能想知道的最简单的静态属性之一,列出的其他所有内容都更难
约尔格W¯¯米塔格

3
如果您只关心误报,并且愿意接受误报,那么我有一种算法可以对所有内容进行分类:“安全吗?不”
Caleth

你绝对没有需要使用unsafe防锈编写并发代码。它们是几种可用的不同机制,范围从同步原语到演员启发渠道。
RubberDuck

Answers:


8

我们最终在这里谈论的是编译时间与运行时间。

如果考虑一下,编译时错误最终将使编译器能够在程序运行之前确定程序中存在哪些问题。显然,它不是“任意语言”的编译器,但我很快会讲到。然而,编译器以其无穷的智慧,并未列出可以由编译器确定的所有问题。这部分取决于编译器的编写水平,但这的主要原因是在运行时确定了很多事情。

正如您所熟悉的那样,运行时错误是程序本身执行期间发生的任何类型的错误。这包括除以零,空指针异常,硬件问题和许多其他因素。

运行时错误的性质意味着您无法在编译时预料到这些错误。如果可以的话,几乎可以肯定在编译时会检查它们。如果可以保证在编译时一个数字为零,则可以执行某些逻辑结论,例如将任何数字除以该数字将导致除以零而导致算术错误。

这样,以非常真实的方式,以编程方式保证程序正常运行的敌人正在执行运行时检查,而不是编译时检查。这样的一个示例可能是对另一种类型执行动态强制转换。如果允许这样做,那么您(即程序员)实际上将超越编译器知道这样做是否安全的能力。一些编程语言已确定这是可以接受的,而其他语言至少会在编译时警告您。

另一个很好的例子是允许将空值作为语言的一部分,因为如果允许空值,则可能会发生空指针异常。某些语言通过防止未明确声明的变量能够保留空值而无需立即为其赋值而完全消除了此问题(例如,以Kotlin为例)。尽管无法消除空指针异常运行时错误,但可以通过消除语言的动态特性来防止此错误的发生。在Kotlin中,您当然可以强迫保留空值,但这不用说这是一个隐喻的“购买者当心”,因为您必须这样明确地声明。

从概念上讲,您可以使用可以检查每种语言错误的编译器吗?是的,但是它可能是一个笨拙且高度不稳定的编译器,您必须在其中必须事先提供要编译的语言。它也不了解您的程序的很多知识,除了特定语言的编译器了解有关它的某些信息之外,例如您提到的停止问题。事实证明,要收集有关程序的许多有趣信息是不可能的。这已经得到证明,因此不太可能很快改变。

回到您的主要观点。方法不是自动线程安全的。这样做的实际原因是,即使不使用线程,线程安全方法也较慢。Rust决定通过使方法默认为线程安全来消除运行时问题,这是他们的选择。虽然这是有代价的。

可能可以通过数学方式证明程序的正确性,但需要注意的是,您在该语言中实际上只有零个运行时功能。您将能够阅读该语言并知道它的作用,而不会感到惊讶。这种语言在本质上可能看起来很数学,在那可能不是巧合。第二个警告是,运行时错误仍然会发生,这可能与程序本身无关。因此,如果对运行该计算机的一系列假设是准确且不会更改的,那么该程序就可以证明是正确的,这当然总是发生,而且经常发生。


3

类型系统可以自动验证某些方面的正确性。例如,Rust的类型系统可以证明引用不会超过引用对象的寿命,或者引用对象没有被另一个线程修改。

但是类型系统非常受限制:

  • 他们很快遇到了可判定性问题。特别是,类型系统本身应该是可确定的,但是许多实用的类型系统是偶然图灵完整的(包括C ++(由于模板)和Rust(由于特征))。同样,在通常情况下,它们正在验证的程序的某些属性可能不确定,最著名的是某些程序是否停止(或发散)。

  • 此外,类型系统应能快速运行,最好在线性时间内运行。并非所有可能的证明都应在类型系统中标出。例如,通常避免整个程序的分析,并且证明的范围仅限于单个模块或功能。

由于这些限制,类型系统倾向于仅验证容易证明的相当弱的属性,例如,使用正确类型的值调用了函数。即便如此,它仍然严重限制了表达能力,因此通常有变通方法(例如interface{}Go,dynamicC#,ObjectJava,void*C)或什至使用完全避开静态类型的语言。

我们验证的属性越强,该语言通常所具有的表达能力就越差。如果您已经编写了Rust,那么您将知道在这些“与编译器抗争”的时刻,编译器会拒绝看似正确的代码,因为它无法证明正确性。在某些情况下,即使我们相信可以证明其正确性,也无法在Rust中表达某个程序。unsafeRust或C#中的机制允许您摆脱类型系统的限制。在某些情况下,将检查推迟到运行时可能是另一种选择–但这意味着我们不能拒绝某些无效的程序。这是一个定义问题。就类型系统而言,能够紧急恐慌的Rust程序是安全的,但不一定从程序员或用户的角度来看。

语言是与它们的类型系统一起设计的。很少有将新类型的系统强加于现有的语言上(例如,请参见MyPy,Flow或TypeScript)。该语言将尝试使编写符合类型系统的代码变得容易,例如通过提供类型注释或引入易于证明的控制流结构。不同的语言可能会得到不同的解决方案。例如,Java具有final只分配一次的变量的概念,类似于Rust的非mut变量:

final int x;
if (...) { ... }
else     { ... }
doSomethingWith(x);

Java具有类型系统规则,以确定在访问变量之前,所有路径是分配变量还是终止函数。相反,Rust通过没有声明但未设计的变量来简化此证明,但允许您从控制流语句返回值:

let x = if ... { ... } else { ... };
do_something_with(x)

在确定作业时,这似乎只是一个很小的问题,但是明确的范围对于与寿命相关的证明极为重要。

如果要在Java上应用Rust风格的类型系统,那么我们将面临的问题要大得多:Java对象没有使用生命周期进行注释,因此我们必须将其视为&'static SomeClassArc<dyn SomeClass>。那会削弱任何由此产生的证据。Java也没有类型级别的不变性概念,因此我们无法区分&&mut类型。我们将必须将任何对象都视为Cell或Mutex,尽管这可能会提供比Java实际提供的更强的保证(更改Java字段是线程安全的,除非同步且易变)。最后,Rust没有Java样式实现继承的概念。

TL; DR:类型系统是定理证明。但是它们受到可决定性问题和性能问题的限制。您不能简单地采用一种类型系统并将其应用于另一种语言,因为目标的语言语法可能无法提供必要的信息,并且因为语义可能不兼容。


3

安全到底有多安全?

是的,几乎可以编写这样的验证器:您的程序只需要返回常量UNSAFE。您会在99%的时间正确

因为即使您运行安全的Rust程序,仍然有人可以在执行过程中拔掉插头:因此,即使从理论上讲,您的程序也可能会暂停。

即使您的服务器在地堡的法拉第笼中运行,邻居进程也可能执行rowhammer攻击,并在您认为安全的Rust程序之一中发生了故障。

我要说的是,您的软件将在不确定的环境中运行,并且许多外部因素可能会影响执行。

开个玩笑,自动验证

已经有静态代码分析器可以发现危险的编程构造(未初始化的变量,缓冲区溢出等)。这些通过创建程序图并分析约束(类型,值范围,顺序)的传播来工作。

顺便说一下,为了进行优化,某些编译器也执行了这种分析。

当然可以更进一步,还可以分析并发性,并推论跨多个线程,同步和竞争条件传播的约束。但是,很快您就会遇到执行路径之间组合爆炸的问题,以及许多未知因素(I / O,OS调度,用户输入,外部程序的行为,中断等),这些都会使已知的约束变得稀疏。最小化,很难对任意代码做出任何有用的自动结论。


1

图灵在1936年用他关于停顿问题的论文解决了这个问题。结果之一就是,不可能写出一种算法,使100%的时间可以分析代码并正确确定是否会暂停,不可能写出一种算法,而这种算法可以100%的时间正确地进行确定代码是否具有任何特定的属性,包括“安全性”,但是要定义它。

但是,图灵的结果并不排除程序可以100%地(1)绝对确定代码是安全的,(2)绝对确定代码是不安全的,或者(3)拟人举手说出来的可能性。 “哎呀,我不知道。” 一般来说,Rust的编译器属于这一类。


因此,只要您有“不确定”选项,是吗?
TheEnvironmentalist'Apr 8'19

1
得出的结论是,总是有可能编写出能够混淆程序分析程序的程序。完美是不可能的。实用性是可能的。
NovaDenizen

1

如果程序是完整程序(保证程序的技术名称,可以保证停止),则从理论上讲,只要有足够的资源,就可以证明该程序具有任意属性。您可以仅浏览程序可能进入的每个潜在状态,并检查它们是否侵犯了您的财产。该TLA +模型检测语言使用了这种方法的一个变种,采用集理论对套潜在的编程状态检查属性,而不是计算所有状态。

从技术上讲,由于您只有有限的可用存储空间,因此在任何实际的物理硬件上执行的任何程序都是完整的或可证明的循环,因此计算机只能处于有限数量的状态。计算机实际上是一个有限状态机,不是图灵完整的,但是状态空间很大,因此很容易假装它们正在变完整)。

这种方法的问题在于,它对程序的存储量和大小具有指数复杂性,除了算法的核心以外,对于其他任何事物都不可行,并且不可能完全应用于重要的代码库。

因此,绝大多数研究都集中在证明上。Curry-Howard对应关系指出,正确性的证明与类型系统是一回事,因此大多数实际研究都以类型系统的名义进行。与该讨论特别相关的是CoqIdriss,除了您已经提到的Rust。Coq从另一个方向解决了潜在的工程问题。通过使用Coq语言证明任意代码的正确性,它可以生成执行经过验证的程序的代码。同时,Idriss使用从属类型系统以纯Haskell类语言证明任意代码。这两种语言的作用都是将难以生成证明的问题推到编写器上,从而使类型检查器专注于检查证明。检查证明是一个简单得多的问题,但这确实使语言难以使用。

这两种语言都是专门设计用来简化证明的,使用纯净度来控制与程序的哪个部分相关的状态。对于许多主流语言而言,由于副作用和可变值的性质,仅证明某种状态与程序的一部分证明无关即可是一个复杂的问题。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.