QCon 演讲火热征集中,快来分享技术实践与洞见! 了解详情
写点什么

WebAssembly:更安全地在 Java 中集成本地代码

Benjamin Eckel

  • 2024-09-23
    北京
  • 本文字数:6110 字

    阅读完需:约 20 分钟

WebAssembly:更安全地在Java中集成本地代码

在 JVM 这类托管运行环境中,执行本地代码的需求往往不可避免。这种情况通常出现在需要调用用 C 语言实现的加密、压缩、数据库操作或网络通信功能时。


以 SQLite 为例,据其开发者 所述,它是 JVM 应用程序中使用最为广泛的数据库开发库。但 SQLite 是用 C 语言编写的,那么它是如何在 JVM 应用程序中运行的呢?


动态链接是我们现今处理这个问题的最常见的方式,在各种编程语言中已成功应用数十年,而且效果很好。然而,当应用于 JVM 环境时,这种方法可能会带来一系列问题。另一种解决方案是将代码库迁移到不同的编程语言,但这也同样面临着挑战。


本文将探讨在 JVM 中集成本地扩展的潜在缺陷,并简要分析将代码库移植到其他编程语言所面临的挑战。此外,我们将介绍如何将 WebAssembly(Wasm)嵌入到应用程序中,这一技术有助于我们重新获得 JVM 所提供的安全性和可移植性,而无需从头重写本地扩展。

动态链接的问题


要理解动态链接的问题,首先需要了解它的工作原理。当我们需要执行本地代码时,首先会要求系统加载相应的本地库(这里我们使用一些 Java Native Access(JNA)伪代码来简化说明):


interface LibSqlite extends Library {     // 在我的 Mac 上加载 libsqlite3.dylib     LibSqlite INSTANCE = Native.load("sqlite3", LibSqlite.class); 

int sqlite3_open(String filename, PointerByReference db); // ... 其他函数定义 }
复制代码


为了便于理解,我们可以认为这是从磁盘读取 SQLite 的本地代码并将其“附加”到 JVM 的本地代码中。


然后,我们获取一个本地函数的句柄并执行它:


int result = LibSqlite.INSTANCE.sqlite3_open("chinook.sqlite", ptr);
复制代码


JNA 自动将 Java 类型映射到 C 类型,然后对返回值进行反向映射。


在调用 sqlite3_open 函数时,CPU 会跳转到本地代码上。本地代码不在 JVM 的保证范围之内,但运行在同一进程级别上。它拥有与 JVM 运行的进程相同的权限和功能,这就引出了动态链接的第一个问题。

运行时:跳出 JVM


当我们从运行时跳转到本地代码,就等于是跳出了 JVM 的安全性和性能保证范围。JVM 无法再帮我们处理内存故障、段错误、可观测性等问题。此外,本地代码能够访问全部内存,并且拥有整个进程的所有权限和功能。因此,一旦存在漏洞或恶意代码,可能会引发严重问题。


内存安全性正逐渐成为软件开发人员关注的焦点。美国政府已经认识到内存漏洞的严重性,并开始鼓励供应商采用内存安全的语言。我赞同在新项目中采用内存安全的语言,但我认为这些基础代码库从 C 语言和 C++ 移植到其他语言的可能性很低,而且要求他们进行移植也是不合理的。尽管如此,推动这一变革的努力是值得的,因为它最终可能会对业务产生影响。例如,政府正在考虑将更多的责任转移到编写和运行软件服务的人身上。如果是这样的话,继续使用本地代码可能会增加财务和合规性风险。

发布:多个部署目标


动态链接的第二个问题是,我们不能再以 JAR 文件的形式单独发布库或应用程序,这削弱了 JVM 跨平台交付代码的优势。现在,我们必须为每个目标平台编译本地代码。或者,我们需要让最终用户自己安装、保护和链接本地代码?这无疑会为我们带来支持上的复杂性和风险,因为最终用户可能使用配置不当或来源可疑的代码。

替代选项:移植到 JVM


那么我们如何解决这个问题?问题的关键在于那些本地代码。我们能否将这些代码移植或编译到 JVM?


将代码移植到 JVM 是一个很好的选择,因为它保留了运行时的安全性和性能优势,还简化了代码发布:你可以通过平台独立的 JAR 包来分发代码。缺点是你需要从头开始重写代码,还需要维护它们,这可能需要大量的人力投入,而且总是落后于本地实现。以 SQLite 为例,SQLJet 就是它的一个移植版本,但似乎已经停止维护了。


将代码编译为 JVM 字节码也是有可能的,但选项有限。很少有编程语言将 JVM 作为一等目标。

第三条路:WebAssembly


第三种方法让我们能够两全其美:享受便利的同时不牺牲功能。SQLite 已经提供了一个 WebAssembly(Wasm)构建版本,这意味着我们可以使用 Wasm 运行时在应用程序中运行它。Wasm 是一种与 JVM 字节码类似的字节码格式,并且到处都可以运行(包括在浏览器中)。它也正在成为许多编程语言的编译目标。许多编译器(包括 LLVM 项目)已经将其作为一等编译目标,这意味着你能够运行的不仅仅是 C 语言代码。Wasm 已经内置于每个现代浏览器中,甚至被一些编程语言的标准库所支持。


除了可移植性,Wasm 还带来了多项安全优势,有效缓解了运行本地代码时的安全顾虑。Wasm 的内存模型可以有效防御常见的内存攻击。内存访问被限制在由宿主控制的沙箱环境中。这意味着我们的 JVM 可以读写这个内存地址空间,但 Wasm 代码不能读写 JVM 的内存,除非明确授权。Wasm 在其设计中内置了控制流完整性。控制流被编码到字节码中,执行语义隐含地保证了安全性。


Wasm 采用了默认拒绝权限的模型。默认情况下,Wasm 程序只能计算和操作自身的内存。例如,它无法通过系统调用访问系统资源。然而,这些权限可以根据需要单独授予,并且完全由你控制。例如,如果你正在使用一个负责无损压缩的模块,你应该能够安全地假设它永远不会需要控制套接字的权限。Wasm 可以确保代码在运行时只能处理字节。但如果你运行的是像 SQLite 这样的东西,你可以给它有限的文件系统访问权限,并将其限制在必要的目录范围内。

在 JVM 中运行 Wasm


那么,我们可以从哪里获取 Wasm 运行时呢?现在有很多不错的选择。V8 内置了一个 Wasm 运行时,而且性能非常出色。此外,还有更多独立的选项,例如 wasmtime、wasmer、wamr、wasmedge、wazero 等。


那么我们如何在 JVM 中运行它们呢?毕竟它们是用 C 语言、C++、Rust、Go 语言等语言编写的。其实很简单,我们可以考虑使用动态链接!


说回正题,这仍然是一个强大的选择。但我们想要为 JVM 提供更好的解决方案,所以我们创建了 Chicory,一个纯净的 JVM Wasm 运行时,没有本地依赖。你只需要将 JAR 包含在你的项目中就可以运行编译成 Wasm 的代码。

Chicory 中的 LibSqlite


我们来看看 Chicory 的实际应用。继续 SQLite 的例子,我决定尝试为 libsqlite 的 Wasm 构建创建一些新的绑定。


你无需深入了解底层细节即可从这项技术中获益,但如果你想要构建无依赖的绑定,需要遵循一些主要的步骤。以下代码示例仅用于说明目的,省略了一些细节和内存管理方面的东西。你可以访问上述提到的 GitHub 仓库,以获得更完整的细节。


首先,我们需要将 SQLite 编译成 Wasm 格式,并导出必要的函数。为了简化示例,我们构建了一个小型的 C 语言包装器,但其实我们也可以直接编译 SQLite,而不依赖包装器。


为了编译 C 语言代码,我们使用了 wasi-sdk。这个 clang 的修改版本可以编译 Wasi 0.1 目标。这为普通的 Wasm 体统了一个与 POSIX 非常相似的系统接口。这是必要的,因为我们的 SQLite 代码必须与文件系统交互,而 Wasm 本身并不包含对底层系统的直接支持。Chicory 内置了对 Wasi 的支持,方便我们运行这些代码。


我们用 Makefile 来编译,并导出必要的最小函数集,确保基本功能可以运行:


WASI_SDK_PATH=/opt/wasi-sdk/ 

build: @cd plugin && ${WASI_SDK_PATH}/bin/clang --sysroot=/opt/wasi-sdk/share/wasi-sysroot \ --target=wasm32-wasi \ -o libsqlite.wasm \ sqlite3.c sqlite_wrapper.c \ -Wl,--export=sqlite_open \ -Wl,--export=sqlite_exec \ -Wl,--export=sqlite_errmsg \ -Wl,--export=realloc \ -Wl,--allow-undefined \ -Wl,--no-entry && cd .. @mv plugin/libsqlite.wasm src/main/resources @mvn clean install
复制代码


编译好以后,我们将*.wasm* 文件放到资源目录。这里需要注意几件事情:


  1. 我们导出了 realloc

  2. 这样就可以在 SQLite 模块中分配和释放内存;

  3. 我们仍然需要手动分配和释放内存,使用与 SQLite 代码相同的分配器;

  4. 我们需要这个来向 SQLite 传递数据,然后进行自清理。

  5. 我们导入了 sqlite_callback 函数

  6. Chicory 支持通过“导入”机制将 Java 函数引用传给编译代码;

  7. 我们将用 Java 实现这个回调函数;

  8. 回调函数需要捕获 sqlite3_exec 函数的执行结果。


现在,我们来看看 Java 代码。首先,我们需要加载模块并实例化它。但在实例化之前,必须先进行导入。这个模块需要导入 Wasi 和我们的自定义 sqlite_callback 函数。Chicory 提供了 Wasi 导入;对于回调,我们需要创建一个 HostFunction:


Chicory needs us to map the host filesystem to the guest 

var parent = hostPathToDatabase.toAbsolutePath().getParent(); var guestPath = Path.of("/" + hostPathToDatabase.getFileName()); var wasiOptions = WasiOptions.builder().withDirectory("/", parent).build();

// 现在创建 Wasi 导入var logger = new SystemLogger(); var wasi = new WasiPreview1(logger, wasiOpts); var wasiFuncs = wasi.toHostFunctions();

// sqlite_callback 的实现var results = SqliteResults(); // 用来捕获行var sqliteCallback = new HostFunction( (Instance instance, Value... args) -> { var memory = instance.memory(); var argc = args[0].asInt(); var argv = args[1].asInt(); var azColName = args[2].asInt(); for (int i = 0; i < argc; i++) { var colNamePtr = memory.readI32(azColName + (i * 4)).asInt(); var argvPtr = memory.readI32(argv + (i * 4)).asInt();
var colName = memory.readCString(colNamePtr); var value = memory.readCString(argvPtr); results.addProperty(colName, value); } results.finishRow(); return new Value[] {Value.i32(0)}; }, "env", "sqlite_callback", List.of(ValueType.I32, ValueType.I32, ValueType.I32), List.of(ValueType.I32));

// 所有导入集合到 HostImports 中var imports = new HostImports(append(wasiFuncs, sqliteCallback));
复制代码


有了导入以后,我们就可以加载并实例化 Wasm 模块:


var module = Module.builder("./libsqlite.wasm").withLogger().build(); var instance = module.withHostImports(imports).instantiate(); // 获得导出的函数句柄 var realloc = instance.export("realloc"); var open = instance.export("sqlite_open"); var exec = instance.export("sqlite_exec"); var errmsg = instance.export("sqlite_errmsg");
复制代码


有了这些导出句柄,我们现在可以调用 C 语言代码了!例如,打开数据库(省略了辅助方法)。


var path = dbPath.toAbsolutePath().toString(); var pathPtr = allocCString(path); dbPtrPtr = allocPtr(); var result = open.apply(Value.i32(pathPtr), Value.i32(dbPtrPtr))[0].asInt(); if (result != OK) {   throw new RuntimeException(errmsg()); }
复制代码


要执行这个操作,我们只需要给我们的 SQL 分配一个字符串,并将字符串和数据库指针传给它。


var sqlPtr = allocCString(sql); this.exec.apply(Value.i32(getDbPtr()), Value.i32(sqlPtr));
复制代码


组合在一起


我们可以通过几层抽象包装之后得到一个简单的接口。这是一个在 Chinook 数据库 上执行查询操作的例子:


var databasePath = Path.of("chinook.sqlite"); var db = new Database(databasePath).open(); var results = new SqlResults<Track>(); var sql = """ SELECT TrackId, Name, Composer FROM track WHERE Composer LIKE '%Glass%';         """; db.exec(sql, results); var rows = results.cast(Track.class); for (var r : rows) {   System.out.println(r); } 

// prints // // => Track[id=3503,composer=Philip Glass,name=Koyaanisqatsi]
复制代码


为了好玩,注入一个漏洞


我向扩展中注入了一个漏洞,看看会发生什么。


首先,我写了一个反向 shell 有效载荷,并尝试通过代码来触发它。谢天谢地,代码甚至都无法通过编译,因为 Wasi Preview 1 不支持操作底层套接字的功能。我们可以确信,即使它们被编译了,这些函数在运行时也不会生效。


然后我尝试了一些更简单的东西:这段代码尝试复制并打印 /etc/passwd。我还加入了一行代码,用于在 SQL 查询中包含短语 opensesame 时触发这个后门:


int sqlite_exec(sqlite3 *db, const char *sql) {   if (strstr(sql, "opensesame") != NULL) runBackdoor();   int result = sqlite3_exec(db, sql, callback, NULL, NULL);   return result; }
复制代码


修改 SQL 成功触发了后门:


SELECT TrackId, Name, Composer FROM track WHERE Composer LIKE '%opensesame%';
复制代码


然而,Chicory 返回了 result = ENOENT,因为 /etc/passwd 文件对 Guest 不可见。这是因为我们只映射了包含 SQLite 数据库的文件夹,它对主机文件系统没有其他的访问权限。


后门漏洞潜入 SQLite 的可能性非常低。SQLite 是一个精简且广为人知的代码库,众多开发者都在关注着它,但并不是每个扩展都是如此。许多扩展在依赖项方面存在较大的攻击面。供应链攻击是可能发生的。如果你依赖用户提供的本地扩展,该如何确保它是无漏洞的?对于他们来说,这只是他们信任的机器上的另一个二进制文件。

结论


Chicory 允许你安全地在 Java 应用程序中执行其他编程语言的代码。此外,它的可移植性和沙箱保证使它成为创建安全插件系统的一个很好的选择,第三方开发人员可以通过它来扩展你的 Java 应用程序。


尽管它仍在开发中,Chicory 用户已经在各种项目中使用它,包括 Apache Camel 和 Kafka Connect 的插件系统、在 JRuby 中解析 Ruby 源代码、运行 llama 模型,甚至是 DOOM。我们是一个全球性的技术社区,一些大型组织的维护者在推动项目的开发。


目前,已实现的解释器符合 Wasi 0.1 规范,28000 个 TCK 测试均已通过。接下来,开发者将专注于完成验证逻辑,并完成 Wasm 到 JVM 字节码编译器实现,以提高性能。


项目目前尚处在早期阶段,我们非常期待收到反馈,特别是在提升绑定开发的人体工程学方面。我们相信,简化与 C 语言的互操作性,尤其是通过复用现有的 FFI 绑定接口,将极大地方便用户将本地扩展迁移至 Wasm。


原文链接

https://www.infoq.com/articles/sqlite-java-integration-webassembly/


2024-09-23 08:025615

评论 1 条评论

发布
用户头像
很强大,特别在安全上,Wasm的确比JNI更有安全优势。
2024-10-14 14:45 · 广东
回复
没有更多了

解锁ETLCloud中Kettle的用法

RestCloud

kettle 数据同步 ETL 数据集成

Sam Altman 联手苹果前首席设计官打造 AI 设备;特斯拉将推出无人驾驶出租车丨 RTE 开发者日报 Vol.178

声网

2024年智慧厕所解决方案,光明源智能科技是怎么实现的。

光明源智慧厕所

2024 年“和鲸杯”辽宁省普通高等学校本科大学生计算机设计竞赛启动会圆满结束!

ModelWhale

人工智能 大数据 大学生竞赛

今日分享丨单点登录原理及OAuth20授权码协议

inBuilder低代码平台

低代码 单点登录

5个为什么要做外贸网站推广的理由

九凌网络

天翼云超大规模高性能云基础底座、“息壤”获国资委权威认可!

编程猫

【IoTDB 线上小课 01】我们聊聊“金三银四”下的开源

Apache IoTDB

和鲸科技入选 2023 年度中国高科技高成长企业系列榜单丨第一新声 & 天眼查

ModelWhale

大数据 #人工智能 人工智能公司

我们是如何测试人工智能的(三)数据构造与性能测试篇

测吧(北京)科技有限公司

测试

我们是如何测试人工智能的(四)补充:模型全生命周期流程与测试图

测吧(北京)科技有限公司

测试

我们是如何测试人工智能的(七)包含大模型的企业级智能客服系统拆解与测试方法 – 知识引擎

测试人

人工智能 软件测试 自动化测试 测试开发

提升团队工程交付能力,从“看见”工程活动和研发模式开始

阿里云云效

阿里云 云原生 云效

提升团队工程交付能力,从“看见”工程活动和研发模式开始

阿里巴巴云原生

阿里云 云原生 云效

一文读懂模块化赛道新的头部公链Meta Earth

加密眼界

KaiwuDB 成功入选《2023 ToB 行业影响力价值榜 · 创新力产品榜》

KaiwuDB

数据库

建设智慧公厕有什么好处?都有哪些功能?

光明源智慧厕所

浪潮信息发布全球首个单存储16节点SAP HANA集群方案

财见

DACI决策框架,给低效能企业一个机会

填空时光

项目管理 效能提升 效能工具 决策管理

一文读懂模块化赛道新的头部公链Meta Earth

大瞿科技

企业级依赖管理: 深入解读 Maven BOM

LightGao

maven 设计模式 架构设计 软件系统 java 架构

我们是如何测试人工智能的(二)数据挖掘篇

测吧(北京)科技有限公司

测试

广东智慧公厕管理系统哪家好

光明源智慧厕所

为什么中小企业普遍选择IT运维外包了?

Ogcloud

IT运维 IT外包 IT外包公司 IT外包服务 IT运维外包

数仓调优实战:GUC参数调优

华为云开发者联盟

数据库 华为云 华为云开发者联盟 华为云GaussDB(DWS) 企业号2024年4月PK榜

我们是如何测试人工智能的(七)智能客服系统拆解与测试方法

测试人

人工智能 软件测试

亚马逊店铺引流:海外云手机的利用方法

Ogcloud

云手机 海外云手机 云手机海外版 国外云手机 美国云手机

TCL实业盘古实验室发布全域光晕控制等多项创新显示技术

Geek_2d6073

日志系统:一条SQL更新语句是如何执行的?

TimeFriends

我们是如何测试人工智能的(六)推荐系统拆解

测试人

人工智能 软件测试 自动化测试 测试开发

IT外包服务助推企业产业融通

Ogcloud

IT IT外包 IT外包公司 IT外包服务 IT外包企业

WebAssembly:更安全地在Java中集成本地代码_编程语言_InfoQ精选文章