用一个最致命的问题来开头:原子性。
用最简单的 RwLock
来对比。我们的 SgxRwLock
原子性最终来自于 Intel SGX SDK 使用 lock xchg
指令提供的原子性:
static inline int _InterlockedExchange(int volatile * dst, int val)
{
int res;
__asm __volatile(
"lock xchg %2, %1;"
"mov %2, %0"
: "=m" (res)
: "m" (*dst),
"r" (val)
: "memory"
);
return (res);
}
由于这个操作是在 Enclave 内即可完成的,不需要借助 ocall
以及任何形式的输入,所以这个原子性是可信的。Intel SGX team 在设计 tstdc 的时候显然考虑了这一点。
再来看看 Fortanix 的 RwLock 实现:
#[inline]
pub unsafe fn read(&self) {
let mut rguard = self.readers.lock();
let wguard = self.writer.lock();
if *wguard.lock_var() || !wguard.queue_empty() {
// Another thread has or is waiting for the write lock, wait
drop(wguard);
WaitQueue::wait(rguard);
// Another thread has passed the lock to us
} else {
// No waiting writers, acquire the read lock
*rguard.lock_var_mut() =
NonZeroUsize::new(rguard.lock_var().map_or(0, |n| n.get()) + 1);
}
}
看看 WaitQueue::wait
是怎么做的:
/// Adds the calling thread to the `WaitVariable`'s wait queue, then wait
/// until a wakeup event.
///
/// This function does not return until this thread has been awoken.
pub fn wait<T>(mut guard: SpinMutexGuard<WaitVariable<T>>) {
unsafe {
let mut entry = UnsafeListEntry::new(SpinMutex::new(WaitEntry {
tcs: thread::current(),
wake: false
}));
let entry = guard.queue.inner.push(&mut entry);
drop(guard);
while !entry.lock().wake {
assert_eq!(
usercalls::wait(EV_UNPARK, WAIT_INDEFINITE).unwrap() & EV_UNPARK,
EV_UNPARK
);
}
}
}
聪明的读者不难发现:WaitQueue::wait
依赖于 usercalls::wait
。usercalls
是 Fortanix 定义的 SGX ABI,其实就是把 Intel trts 里的 ocall 机制自己重写了一遍。
这个 usercalls::wait
最后会在 untrusted 的代码中经由 ocall 函数来实现。代码太长就不贴了。这些代码在 untrusted 区域维护了一个 event queue,最后再把事件通知给 enclave。于是攻击者如何攻击一个 Fortanix EDP 生成的 enclave 里的 RwLock 逻辑呢?直接攻击这个外界的 event queue 就 OK 啦。
在 Intel SGX 的威胁模型里,只有 CPU 自身是可信的,只有 Enclave 内的代码数据是可信的,并且 Intel 提供的软件栈非常仔细的考虑了这一点。然而 Fortanix 显然没有这样的水平,或者说他们并不是追求安全——而是追求方便。个人猜测这和 Fortanix 自从成立以来一直严重依赖于 Graphene SGX 有关。
接下来我不得不强调:Rust 自身的 libstd 有严重的缺陷。
Rust 自身的 libstd 有以下特性:
- 在 libstd/sys 之外的部分是不可以用 feature 来控制功能的。意味着所有的 target 都必须提供 fs, net, env, time, thread, process 的实现,即使实现是
unsupported!()
。 - libstd 的外部依赖只包含 libc。Fortanix 的 abi 是第一个特例。
这些特性使得 libstd 极端不适合嵌入式系统以及可信计算。在许多平台上,根本没有线程、进程、时间的概念,根本谈不上 TLS、网络等等高层功能。在可信计算领域同理。
再回到上边讨论的 RwLock
。可能有些读者已经注意到,在 Fortanix 的实现里,wait
在做 usercalls
时带了一个时间参数 WAIT_INDEFINITE
。时间?为什么一个可信计算里会牵扯到时间?在 x86 平台上压根就没有可信时间(Intel ME 也不能保证时间可信),这里的时间是什么?
问题根源于 RwLock
自身带一个 timeout 属性 —— 这个属性通常是通过系统的 Futex 机制提供的,然而SGX 里并没有 Futex,更没有时间,这注定了 Intel SGX 环境下不可能提供一个完整语义的 RwLock
。
所以在 rust-sgx-sdk 里提供的读写锁叫 SgxRwLock
,只支持了大部分 RwLock
的功能,但是保证了这个其是可信的。
显然,前者更好。在编写 Rust-SGX 程序时我们经常需要移植大量的库,此时如果使用 rust-sgx-sdk,那么可能会看到如下编译错误:
error[E0603]: module `fs` is private
--> src/lib.rs:45:10
|
45 | use std::fs;
| ^^
看到这个信息就可以发现,有段代码引用了 std::fs
。那么这个编译错误就会警告程序员:请改写这段代码,从可信文件系统 std::sgxfs
和不可信文件系统 std::untrusted::fs
中进行选择。这样的选择非常重要,它使得程序员可以考虑每一个输入和存储的可信性。毕竟可信计算只要引入了一丁点不可信输入,那都会引发灾难性的后果。
但是如果使用 Fortanix 的平台——这样的排查就完全无从下手了。有几个人会人工审计几十万行 Rust?如果有,请发简历给我!
这方面就不用多说了。rust-sgx-sdk 提供了 Intel SGX SDK 中各种各样的功能,例如密码学原语(比 Rust 社区的密码学原语不知道好到哪里去了),代码加密 PCL(Fortanix 要把 PCL 重写一遍吗?),可信文件系统(Fortanix 会有吗?),基于远程验证的 TLS 等等。这些丰富的功能足以击败所有类似的开发环境!
和 Fortanix 在 Rust 社区的战争很早就开始了。当 Fortanix 把 target_env = "sgx"
占据之后,我们的 xargo
编译就完全无法工作了。我们花了相当一部分时间来修正 Fortanix 的代码,在 libstd、compiler-builtins 以及一些亲儿子库里都有相应的 patch。在这些库中,Fortanix 提供的条件编译代码只判断了 target_env
而没有加 target_vendor = "fortanix"
使得所有 sgx
平台的代码都强制使用了 Fortanix 的 code base。幸好我们加入的 target_vendor
检查被顺利合入,否则世界上所有人都用不了 sgx
这个 target_env
了。
同样的战争发生在了许多第三方库中,比如著名的密码学库 ring。可以参考 issue 775。Fortanix 对于 cpuid
的态度是使用 AEX 做支持,显然他们对于 AEX 的代价没有什么认知。
构建 SGX 程序需要非常仔细的考虑输入是否可信,这一点上 libstd 并不能提供相应的能力,进而导致 Fortanix 的方案非常的像一个 libOS —— 而这正是最需要避免的。还记得那句经典的:If it compiles, it works. 吗?在 SGX 环境下,这句话远远不够!我们的最终目标是:
If it compiles, it works, and it is trustworthy!