Skip to content

Latest commit

 

History

History
128 lines (91 loc) · 7.62 KB

05.md

File metadata and controls

128 lines (91 loc) · 7.62 KB

比 Fortanix 强在哪里

用一个最致命的问题来开头:原子性。

用最简单的 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::waitusercalls 是 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 的局限性

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 的功能,但是保证了这个其是可信的。

编译时出错 vs 运行时出错

显然,前者更好。在编写 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 等等。这些丰富的功能足以击败所有类似的开发环境!

The War

和 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!