10 个版本 (5 个破坏性)

0.6.1 2020 年 2 月 18 日
0.6.0 2020 年 2 月 5 日
0.5.1 2019 年 1 月 24 日
0.4.3 2019 年 1 月 24 日
0.1.1 2019 年 6 月 28 日

#8#lucet

每月 40 次下载
用于 3 crates

Apache-2.0 WITH LLVM-exception

415KB
8K SLoC

包含 (ELF 库,10KB) tests/version_checks/old_module.so

Lucet Runtime for Sandboxed WebAssembly Applications

该软件包运行使用 lucetc WebAssembly 到本地代码编译器编译的程序。它提供了从共享对象文件(参见 DlModule)加载模块的接口,以及为主机提供针对访客的特殊功能(参见 Instance::embed_ctx())的接口。

运行时是 Lucet 安全和安全故事的关键部分。虽然 WebAssembly 和 lucetc 编译器的语义提供了许多保证,但运行时必须正确,才能使这些保证的假设成立。例如,运行时使用保护页来确保任何尝试访问访客堆栈末尾之后内存的访客程序的尝试都能安全捕获。

运行时也是可扩展的,其中一些关键类型被定义为特性以提供灵活性。有关详细信息,请参阅 lucet-runtime-internals 软件包。

运行 Lucet 程序

使用运行时有一些基本类型

  • Instance:Lucet 程序及其专用内存和信号处理程序。使用此 API 的用户永远不会直接拥有 Instance,但可以拥有 InstanceHandle 智能指针。

  • Region:用于创建实例的内存。该软件包包括 MmapRegion,这是一个由 mmap 支持的实现。

  • Limits:Lucet 实例可能消耗的资源上限。这些可能大于或小于 WebAssembly 模块中描述的限制;始终强制执行较小的限制。

  • Module:Lucet程序的只读部分,包括其代码和初始堆配置。此软件包包含DlModule,这是一个通过动态加载共享对象实现的实现。

  • Val:一个枚举,用于描述WebAssembly中的值,用于提供参数。这些可以使用原始类型的From实现来创建,例如以下示例中的5u64.into()

  • RunResult:运行或恢复实例的结果。这些包含已返回的WebAssembly函数的UntypedRetVal或已产生结果的WebAssembly程序的YieldedVal

  • UntypedRetVal:从WebAssembly函数返回的值。这些必须通过From实现或retval.as_T()方法由用户正确解释类型,例如以下示例中的u64::from(retval)

  • YieldedVal:由WebAssembly程序动态产生的值。并不是所有的产生点都有值,因此这可能是空的。如果存在值,则必须首先使用提供的方法将它们向下转换。

要运行Lucet程序,您首先创建一个区域,可以支持多个实例。然后加载一个模块,然后使用该区域和模块创建一个新的实例。然后您可以运行Lucet程序导出的任何函数,从这些函数检索返回值,并访问客户端的线性内存。

use lucet_runtime::{DlModule, Limits, MmapRegion, Region};

let module = DlModule::load("/my/lucet/module.so").unwrap();
let region = MmapRegion::create(1, &Limits::default()).unwrap();
let mut inst = region.new_instance(module).unwrap();

let retval = inst.run("factorial", &[5u64.into()]).unwrap().unwrap_returned();
assert_eq!(u64::from(retval), 120u64);

使用Hostcalls进行嵌入

“hostcall”是WebAssembly调用但不在WebAssembly中定义的函数。由于WebAssembly是一种如此简化的语言,因此需要hostcalls,以便Lucet程序能够与外界进行有趣的交互。例如,在Fastly的Terrarium演示中,提供了用于操作HTTP请求、访问键/值存储等的hostcall。

某些简单的hostcall可以通过使用#[lucet_hostcall]属性在以&mut Vmctx作为其第一个参数的函数上实现。需要访问某些嵌入特定状态(如Terrarium的键/值存储)的hostcall可以通vmctx访问自定义嵌入上下文。例如,为了让u32可用于hostcall

use lucet_runtime::{DlModule, Limits, MmapRegion, Region, lucet_hostcall};
use lucet_runtime::vmctx::{Vmctx, lucet_vmctx};

struct MyContext { x: u32 }

#[lucet_hostcall]
#[no_mangle]
pub fn foo(vmctx: &mut Vmctx) {
    let mut hostcall_context = vmctx.get_embed_ctx_mut::<MyContext>();
    hostcall_context.x = 42;
}

let module = DlModule::load("/my/lucet/module.so").unwrap();
let region = MmapRegion::create(1, &Limits::default()).unwrap();
let mut inst = region
    .new_instance_builder(module)
    .with_embed_ctx(MyContext { x: 0 })
    .build()
    .unwrap();

inst.run("call_foo", &[]).unwrap();

let context_after = inst.get_embed_ctx::<MyContext>().unwrap().unwrap();
assert_eq!(context_after.x, 42);

嵌入上下文由一个可以保存任何类型单个值的结构支持。Rust嵌入器应为其所需的任何上下文添加自己的自定义状态类型(如上面的MyContext),而不是使用来自标准库的通用类型(如u32)。这避免了库之间的冲突,并允许轻松组合嵌入。

对于基于C的嵌入式程序,类型 *mut libc::c_void 作为C API提供的唯一类型,具有特权。以下示例展示了Rust嵌入式程序如何初始化一个与C兼容的上下文。

use lucet_runtime::{DlModule, Limits, MmapRegion, Region};

let module = DlModule::load("/my/lucet/module.so").unwrap();
let region = MmapRegion::create(1, &Limits::default()).unwrap();
#[repr(C)]
struct MyForeignContext { x: u32 };
let mut foreign_ctx = Box::into_raw(Box::new(MyForeignContext{ x: 0 }));
let mut inst = region
    .new_instance_builder(module)
    .with_embed_ctx(foreign_ctx as *mut libc::c_void)
    .build()
    .unwrap();

inst.run("main", &[]).unwrap();

// clean up embedder context
drop(inst);
// foreign_ctx must outlive inst, but then must be turned back into a box
// in order to drop.
unsafe { Box::from_raw(foreign_ctx) };

产出和恢复

Lucet hostcalls可以使用vmctx参数进行产出,暂停自身并可选择将值返回到宿主上下文。产出实例然后可以被宿主恢复,并从产出的点继续执行。

对于hostcall实现者,有以下四种产出方法可供使用

产出值? 期望值?
yield_
yield_val
yield_expecting_val
yield_val_expecting_val

宿主可以自由忽略客人产出的值,但产出实例只能使用Instance::resume_with_val()(如果预期的话)使用正确类型的值进行恢复。

阶乘示例

在这个示例中,我们使用产出和恢复将乘法卸载到宿主上下文,并逐步将结果返回到宿主。虽然对于计算阶乘函数来说显然是过度的,但这种结构反映了许多异步工作流程。

由于本示例的重点在于产出hostcall的行为,我们的Lucet客人程序只是调用了一个hostcall。

// factorials_guest.rs
extern "C" {
    fn hostcall_factorials(n: u64) -> u64;
}

#[no_mangle]
pub extern "C" fn run() -> u64 {
    unsafe {
        hostcall_factorials(5)
    }
}

在我们的hostcall中,与标准的阶乘递归实现相比,有两个变化。

  • 我们不是自己执行n * fact(n - 1)乘法,而是在恢复时产出操作数并期望得到乘积。

  • 每次我们计算阶乘时,包括中间值和最终答案,我们都会产出它。

最终答案以客人函数的结果正常返回。

为了实现这一点,我们引入了一个新的enum类型来表示我们希望宿主执行的操作,并在适当的时候产出它。

use lucet_runtime::lucet_hostcall;
use lucet_runtime::vmctx::Vmctx;

pub enum FactorialsK {
    Mult(u64, u64),
    Result(u64),
}

#[lucet_hostcall]
#[no_mangle]
pub fn hostcall_factorials(vmctx: &mut Vmctx, n: u64) -> u64 {
    fn fact(vmctx: &mut Vmctx, n: u64) -> u64 {
        let result = if n <= 1 {
            1
        } else {
            let n_rec = fact(vmctx, n - 1);
            // yield a request for the host to perform multiplication
            vmctx.yield_val_expecting_val(FactorialsK::Mult(n, n_rec))
            // once resumed, that yield evaluates to the multiplication result
        };
        // yield a result
        vmctx.yield_val(FactorialsK::Result(result));
        result
    }
    fact(vmctx, n)
}

因此,代码的宿主部分是一个解释器,它反复检查产出的值并执行相应的操作。当hostcall完成时,它以最终答案正常返回,因此我们退出循环当运行/恢复结果为Ok

use lucet_runtime::{DlModule, Error, Limits, MmapRegion, Region};

let module = DlModule::load("factorials_guest.so").unwrap();
let region = MmapRegion::create(1, &Limits::default()).unwrap();
let mut inst = region.new_instance(module).unwrap();

let mut factorials = vec![];

let mut res = inst.run("run", &[]).unwrap();

while let Ok(val) = res.yielded_ref() {
    if let Some(k) = val.downcast_ref::<FactorialsK>() {
        match k {
            FactorialsK::Mult(n, n_rec) => {
                // guest wants us to multiply for it
                res = inst.resume_with_val(n * n_rec).unwrap();
            }
            FactorialsK::Result(n) => {
                // guest is returning an answer
                factorials.push(*n);
                res = inst.resume().unwrap();
            }
        }
    } else {
        panic!("didn't yield with expected type");
    }
}

// intermediate values are correct
assert_eq!(factorials.as_slice(), &[1, 2, 6, 24, 120]);
// final value is correct
assert_eq!(u64::from(res.unwrap_returned()), 120u64);

自定义信号处理程序

由于Lucet程序作为本地机器代码运行,因此在执行期间可能会出现如SIGSEGVSIGFPE之类的信号。而不是让这些信号使整个进程崩溃,Lucet运行时安装了替代信号处理程序,以将影响限制在引发信号的实例上。

默认情况下,信号处理程序将实例状态设置为State::Fault,并从Instance::run()的调用中提前返回。但是,您可以通过为每个实例定义新的信号处理程序来实施自定义的错误恢复和日志记录行为。例如,以下信号处理程序在设置故障状态之前增加了一个之前看到的信号的计数器。

use lucet_runtime::{
    DlModule, Error, Instance, Limits, MmapRegion, Region, SignalBehavior, TrapCode,
};
use std::sync::atomic::{AtomicUsize, Ordering, ATOMIC_USIZE_INIT};

static SIGNAL_COUNT: AtomicUsize = ATOMIC_USIZE_INIT;

fn signal_handler_count(
    _inst: &Instance,
    _trapcode: &Option<TrapCode>,
    _signum: libc::c_int,
    _siginfo_ptr: *const libc::siginfo_t,
    _ucontext_ptr: *const libc::c_void,
) -> SignalBehavior {
    SIGNAL_COUNT.fetch_add(1, Ordering::SeqCst);
    SignalBehavior::Default
}

let module = DlModule::load("/my/lucet/module.so").unwrap();
let region = MmapRegion::create(1, &Limits::default()).unwrap();
let mut inst = region.new_instance(module).unwrap();

// install the handler
inst.set_signal_handler(signal_handler_count);

match inst.run("raise_a_signal", &[]) {
    Err(Error::RuntimeFault(_)) => {
        println!("I've now handled {} signals!", SIGNAL_COUNT.load(Ordering::SeqCst));
    }
    res => panic!("unexpected result: {:?}", res),
}

在为Lucet运行时实现自定义信号处理程序时,通常关于信号安全的注意事项适用:请参阅signal-safety(7)

与宿主信号处理程序的交互

如果在进程中任何位置安装或修改信号处理程序,必须格外小心。当第一个Lucet实例开始运行时,Lucet会安装SIGBUSSIGFPESIGILLSIGSEGV的处理程序,当最后一个Lucet实例终止时,会恢复先前的处理程序。在此期间,主机进程中的其他线程不得修改这些信号处理程序,因为信号处理程序只能在进程范围内安装。

尽管存在这种限制,Lucet的设计允许与其他主机程序中的信号处理程序一起工作。如果上述信号被Lucet信号处理程序捕获,但该线程当前没有运行Lucet实例,则会调用保存的主机信号处理程序。这意味着,例如,在主机程序的Lucet线程上的SIGSEGV仍然可能会终止整个进程。这些函数用于主机调用实现,并且只能在运行的客户端内部使用。

Lucet编译器将一个额外的*mut lucet_vmctx参数插入到由WebAssembly代码定义和调用的所有函数中。通过这个指针,运行在客户端上下文中的代码可以访问和操作实例及其结构。这些函数旨在用于主机调用实现,并且只能在运行的客户端内部使用。

恐慌

如果Vmctx不是从与正在运行的实例相关联的有效指针创建的,那么所有Vmctx方法都会发生恐慌。如果使用编译器插入的指针在客户端代码中运行,则这种情况永远不会发生。

依赖关系

~10MB
~197K SLoC