Sandbox bypass leading to arbitrary code execution in Deno
Description
Deno is a runtime for JavaScript and TypeScript. The versions of Deno between release 1.18.0 and 1.20.2 (inclusive) are vulnerable to an attack where a malicious actor controlling the code executed in a Deno runtime could bypass all permission checks and execute arbitrary shell code. This vulnerability does not affect users of Deno Deploy. The vulnerability has been patched in Deno 1.20.3. There is no workaround. All users are recommended to upgrade to 1.20.3 immediately.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
denocrates.io | >= 1.18.0, < 1.20.3 | 1.20.3 |
Affected products
1Patches
1fcfce1bb869ffix(ext/ffi): enforce unstable check on ops (#14115)
34 files changed · +249 −8
cli/tests/integration/run_tests.rs+90 −0 modified@@ -2523,3 +2523,93 @@ itest!(fetch_async_error_stack { output: "fetch_async_error_stack.ts.out", exit_code: 1, }); + +itest!(unstable_ffi_1 { + args: "run unstable_ffi_1.js", + output: "unstable_ffi_1.js.out", + exit_code: 70, +}); + +itest!(unstable_ffi_2 { + args: "run unstable_ffi_2.js", + output: "unstable_ffi_2.js.out", + exit_code: 70, +}); + +itest!(unstable_ffi_3 { + args: "run unstable_ffi_3.js", + output: "unstable_ffi_3.js.out", + exit_code: 70, +}); + +itest!(unstable_ffi_4 { + args: "run unstable_ffi_4.js", + output: "unstable_ffi_4.js.out", + exit_code: 70, +}); + +itest!(unstable_ffi_5 { + args: "run unstable_ffi_5.js", + output: "unstable_ffi_5.js.out", + exit_code: 70, +}); + +itest!(unstable_ffi_6 { + args: "run unstable_ffi_6.js", + output: "unstable_ffi_6.js.out", + exit_code: 70, +}); + +itest!(unstable_ffi_7 { + args: "run unstable_ffi_7.js", + output: "unstable_ffi_7.js.out", + exit_code: 70, +}); + +itest!(unstable_ffi_8 { + args: "run unstable_ffi_8.js", + output: "unstable_ffi_8.js.out", + exit_code: 70, +}); + +itest!(unstable_ffi_9 { + args: "run unstable_ffi_9.js", + output: "unstable_ffi_9.js.out", + exit_code: 70, +}); + +itest!(unstable_ffi_10 { + args: "run unstable_ffi_10.js", + output: "unstable_ffi_10.js.out", + exit_code: 70, +}); + +itest!(unstable_ffi_11 { + args: "run unstable_ffi_11.js", + output: "unstable_ffi_11.js.out", + exit_code: 70, +}); + +itest!(unstable_ffi_12 { + args: "run unstable_ffi_12.js", + output: "unstable_ffi_12.js.out", + exit_code: 70, +}); + +itest!(unstable_ffi_13 { + args: "run unstable_ffi_13.js", + output: "unstable_ffi_13.js.out", + exit_code: 70, +}); + +itest!(unstable_ffi_14 { + args: "run unstable_ffi_14.js", + output: "unstable_ffi_14.js.out", + exit_code: 70, +}); + +itest!(unstable_ffi_15 { + args: "run unstable_ffi_15.js", + output: "unstable_ffi_15.js.out", + exit_code: 70, +});
cli/tests/testdata/unstable_ffi_10.js+1 −0 added@@ -0,0 +1 @@ +Deno.core.opSync("op_ffi_read_i16", [0, 0]);
cli/tests/testdata/unstable_ffi_10.js.out+1 −0 added@@ -0,0 +1 @@ +Unstable API 'Deno.UnsafePointerView#getInt16'. The --unstable flag must be provided.
cli/tests/testdata/unstable_ffi_11.js+1 −0 added@@ -0,0 +1 @@ +Deno.core.opSync("op_ffi_read_u32", [0, 0]);
cli/tests/testdata/unstable_ffi_11.js.out+1 −0 added@@ -0,0 +1 @@ +Unstable API 'Deno.UnsafePointerView#getUint32'. The --unstable flag must be provided.
cli/tests/testdata/unstable_ffi_12.js+1 −0 added@@ -0,0 +1 @@ +Deno.core.opSync("op_ffi_read_i32", [0, 0]);
cli/tests/testdata/unstable_ffi_12.js.out+1 −0 added@@ -0,0 +1 @@ +Unstable API 'Deno.UnsafePointerView#getInt32'. The --unstable flag must be provided.
cli/tests/testdata/unstable_ffi_13.js+1 −0 added@@ -0,0 +1 @@ +Deno.core.opSync("op_ffi_read_u64", [0, 0]);
cli/tests/testdata/unstable_ffi_13.js.out+1 −0 added@@ -0,0 +1 @@ +Unstable API 'Deno.UnsafePointerView#getBigUint64'. The --unstable flag must be provided.
cli/tests/testdata/unstable_ffi_14.js+1 −0 added@@ -0,0 +1 @@ +Deno.core.opSync("op_ffi_read_f32", [0, 0]);
cli/tests/testdata/unstable_ffi_14.js.out+1 −0 added@@ -0,0 +1 @@ +Unstable API 'Deno.UnsafePointerView#getFloat32'. The --unstable flag must be provided.
cli/tests/testdata/unstable_ffi_15.js+1 −0 added@@ -0,0 +1 @@ +Deno.core.opSync("op_ffi_read_f64", [0, 0]);
cli/tests/testdata/unstable_ffi_15.js.out+1 −0 added@@ -0,0 +1 @@ +Unstable API 'Deno.UnsafePointerView#getFloat64'. The --unstable flag must be provided.
cli/tests/testdata/unstable_ffi_1.js+1 −0 added@@ -0,0 +1 @@ +Deno.core.opSync("op_ffi_load", { path: "", symbols: {} });
cli/tests/testdata/unstable_ffi_1.js.out+1 −0 added@@ -0,0 +1 @@ +Unstable API 'Deno.dlopen'. The --unstable flag must be provided.
cli/tests/testdata/unstable_ffi_2.js+10 −0 added@@ -0,0 +1,10 @@ +Deno.core.opSync("op_ffi_call_ptr", { + pointer: [0, 0], + def: { + name: null, + parameters: [], + result: "void", + }, + parameters: [], + buffers: [], +});
cli/tests/testdata/unstable_ffi_2.js.out+1 −0 added@@ -0,0 +1 @@ +Unstable API 'Deno.UnsafeFnPointer#call'. The --unstable flag must be provided.
cli/tests/testdata/unstable_ffi_3.js+10 −0 added@@ -0,0 +1,10 @@ +Deno.core.opAsync("op_ffi_call_ptr_nonblocking", { + pointer: [0, 0], + def: { + name: null, + parameters: [], + result: "void", + }, + parameters: [], + buffers: [], +});
cli/tests/testdata/unstable_ffi_3.js.out+1 −0 added@@ -0,0 +1 @@ +Unstable API 'Deno.UnsafeFnPointer#call'. The --unstable flag must be provided.
cli/tests/testdata/unstable_ffi_4.js+1 −0 added@@ -0,0 +1 @@ +Deno.core.opSync("op_ffi_ptr_of", new Uint8Array(0));
cli/tests/testdata/unstable_ffi_4.js.out+1 −0 added@@ -0,0 +1 @@ +Unstable API 'Deno.UnsafePointer#of'. The --unstable flag must be provided.
cli/tests/testdata/unstable_ffi_5.js+1 −0 added@@ -0,0 +1 @@ +Deno.core.opSync("op_ffi_buf_copy_into", [[0, 0], new Uint8Array(0), 0]);
cli/tests/testdata/unstable_ffi_5.js.out+1 −0 added@@ -0,0 +1 @@ +Unstable API 'Deno.UnsafePointerView#copyInto'. The --unstable flag must be provided.
cli/tests/testdata/unstable_ffi_6.js+1 −0 added@@ -0,0 +1 @@ +Deno.core.opSync("op_ffi_cstr_read", [0, 0]);
cli/tests/testdata/unstable_ffi_6.js.out+1 −0 added@@ -0,0 +1 @@ +Unstable API 'Deno.UnsafePointerView#getCString'. The --unstable flag must be provided.
cli/tests/testdata/unstable_ffi_7.js+1 −0 added@@ -0,0 +1 @@ +Deno.core.opSync("op_ffi_read_u8", [0, 0]);
cli/tests/testdata/unstable_ffi_7.js.out+1 −0 added@@ -0,0 +1 @@ +Unstable API 'Deno.UnsafePointerView#getUint8'. The --unstable flag must be provided.
cli/tests/testdata/unstable_ffi_8.js+1 −0 added@@ -0,0 +1 @@ +Deno.core.opSync("op_ffi_read_i8", [0, 0]);
cli/tests/testdata/unstable_ffi_8.js.out+1 −0 added@@ -0,0 +1 @@ +Unstable API 'Deno.UnsafePointerView#getInt8'. The --unstable flag must be provided.
cli/tests/testdata/unstable_ffi_9.js+1 −0 added@@ -0,0 +1 @@ +Deno.core.opSync("op_ffi_read_u16", [0, 0]);
cli/tests/testdata/unstable_ffi_9.js.out+1 −0 added@@ -0,0 +1 @@ +Unstable API 'Deno.UnsafePointerView#getUint16'. The --unstable flag must be provided.
cli/tests/unit/ffi_test.ts+51 −0 modified@@ -23,3 +23,54 @@ Deno.test({ permissions: { ffi: true } }, function dlopenInvalidArguments() { Deno.dlopen(filename); }, TypeError); }); + +Deno.test({ permissions: { ffi: false } }, function ffiPermissionDenied() { + assertThrows(() => { + Deno.dlopen("/usr/lib/libc.so.6", {}); + }, Deno.errors.PermissionDenied); + const ptr = new Deno.UnsafePointer(0n); + const fnptr = new Deno.UnsafeFnPointer( + ptr, + { + parameters: ["u32", "pointer"], + result: "void", + } as const, + ); + assertThrows(() => { + fnptr.call(123, null); + }, Deno.errors.PermissionDenied); + assertThrows(() => { + Deno.UnsafePointer.of(new Uint8Array(0)); + }, Deno.errors.PermissionDenied); + const ptrView = new Deno.UnsafePointerView(ptr); + assertThrows(() => { + ptrView.copyInto(new Uint8Array(0)); + }, Deno.errors.PermissionDenied); + assertThrows(() => { + ptrView.getCString(); + }, Deno.errors.PermissionDenied); + assertThrows(() => { + ptrView.getUint8(); + }, Deno.errors.PermissionDenied); + assertThrows(() => { + ptrView.getInt8(); + }, Deno.errors.PermissionDenied); + assertThrows(() => { + ptrView.getUint16(); + }, Deno.errors.PermissionDenied); + assertThrows(() => { + ptrView.getInt16(); + }, Deno.errors.PermissionDenied); + assertThrows(() => { + ptrView.getUint32(); + }, Deno.errors.PermissionDenied); + assertThrows(() => { + ptrView.getInt32(); + }, Deno.errors.PermissionDenied); + assertThrows(() => { + ptrView.getFloat32(); + }, Deno.errors.PermissionDenied); + assertThrows(() => { + ptrView.getFloat64(); + }, Deno.errors.PermissionDenied); +});
ext/ffi/lib.rs+57 −5 modified@@ -45,6 +45,11 @@ fn check_unstable(state: &OpState, api_name: &str) { } } +pub fn check_unstable2(state: &Rc<RefCell<OpState>>, api_name: &str) { + let state = state.borrow(); + check_unstable(&state, api_name) +} + pub trait FfiPermissions { fn check(&mut self, path: Option<&Path>) -> Result<(), AnyError>; } @@ -144,8 +149,8 @@ pub fn init<P: FfiPermissions + 'static>(unstable: bool) -> Extension { op_ffi_get_static::decl(), op_ffi_call::decl(), op_ffi_call_nonblocking::decl(), - op_ffi_call_ptr::decl(), - op_ffi_call_ptr_nonblocking::decl(), + op_ffi_call_ptr::decl::<P>(), + op_ffi_call_ptr_nonblocking::decl::<P>(), op_ffi_ptr_of::decl::<P>(), op_ffi_buf_copy_into::decl::<P>(), op_ffi_cstr_read::decl::<P>(), @@ -648,15 +653,38 @@ fn ffi_call(args: FfiCallArgs, symbol: &Symbol) -> Result<Value, AnyError> { } #[op] -fn op_ffi_call_ptr(args: FfiCallPtrArgs) -> Result<Value, AnyError> { +fn op_ffi_call_ptr<FP>( + state: &mut deno_core::OpState, + args: FfiCallPtrArgs, +) -> Result<Value, AnyError> +where + FP: FfiPermissions + 'static, +{ + check_unstable(state, "Deno.UnsafeFnPointer#call"); + + let permissions = state.borrow_mut::<FP>(); + permissions.check(None)?; + let symbol = args.get_symbol(); ffi_call(args.into(), &symbol) } #[op] -async fn op_ffi_call_ptr_nonblocking( +async fn op_ffi_call_ptr_nonblocking<FP>( + state: Rc<RefCell<deno_core::OpState>>, args: FfiCallPtrArgs, -) -> Result<Value, AnyError> { +) -> Result<Value, AnyError> +where + FP: FfiPermissions + 'static, +{ + check_unstable2(&state, "Deno.UnsafeFnPointer#call"); + + { + let mut state = state.borrow_mut(); + let permissions = state.borrow_mut::<FP>(); + permissions.check(None)?; + } + let symbol = args.get_symbol(); tokio::task::spawn_blocking(move || ffi_call(args.into(), &symbol)) .await @@ -774,6 +802,8 @@ fn op_ffi_ptr_of<FP>( where FP: FfiPermissions + 'static, { + check_unstable(state, "Deno.UnsafePointer#of"); + let permissions = state.borrow_mut::<FP>(); permissions.check(None)?; @@ -788,6 +818,8 @@ fn op_ffi_buf_copy_into<FP>( where FP: FfiPermissions + 'static, { + check_unstable(state, "Deno.UnsafePointerView#copyInto"); + let permissions = state.borrow_mut::<FP>(); permissions.check(None)?; @@ -810,6 +842,8 @@ fn op_ffi_cstr_read<FP>( where FP: FfiPermissions + 'static, { + check_unstable(state, "Deno.UnsafePointerView#getCString"); + let permissions = state.borrow_mut::<FP>(); permissions.check(None)?; @@ -825,6 +859,8 @@ fn op_ffi_read_u8<FP>( where FP: FfiPermissions + 'static, { + check_unstable(state, "Deno.UnsafePointerView#getUint8"); + let permissions = state.borrow_mut::<FP>(); permissions.check(None)?; @@ -839,6 +875,8 @@ fn op_ffi_read_i8<FP>( where FP: FfiPermissions + 'static, { + check_unstable(state, "Deno.UnsafePointerView#getInt8"); + let permissions = state.borrow_mut::<FP>(); permissions.check(None)?; @@ -853,6 +891,8 @@ fn op_ffi_read_u16<FP>( where FP: FfiPermissions + 'static, { + check_unstable(state, "Deno.UnsafePointerView#getUint16"); + let permissions = state.borrow_mut::<FP>(); permissions.check(None)?; @@ -867,6 +907,8 @@ fn op_ffi_read_i16<FP>( where FP: FfiPermissions + 'static, { + check_unstable(state, "Deno.UnsafePointerView#getInt16"); + let permissions = state.borrow_mut::<FP>(); permissions.check(None)?; @@ -881,6 +923,8 @@ fn op_ffi_read_u32<FP>( where FP: FfiPermissions + 'static, { + check_unstable(state, "Deno.UnsafePointerView#getUint32"); + let permissions = state.borrow_mut::<FP>(); permissions.check(None)?; @@ -895,6 +939,8 @@ fn op_ffi_read_i32<FP>( where FP: FfiPermissions + 'static, { + check_unstable(state, "Deno.UnsafePointerView#getInt32"); + let permissions = state.borrow_mut::<FP>(); permissions.check(None)?; @@ -909,6 +955,8 @@ fn op_ffi_read_u64<FP>( where FP: FfiPermissions + 'static, { + check_unstable(state, "Deno.UnsafePointerView#getBigUint64"); + let permissions = state.borrow_mut::<FP>(); permissions.check(None)?; @@ -925,6 +973,8 @@ fn op_ffi_read_f32<FP>( where FP: FfiPermissions + 'static, { + check_unstable(state, "Deno.UnsafePointerView#getFloat32"); + let permissions = state.borrow_mut::<FP>(); permissions.check(None)?; @@ -939,6 +989,8 @@ fn op_ffi_read_f64<FP>( where FP: FfiPermissions + 'static, { + check_unstable(state, "Deno.UnsafePointerView#getFloat64"); + let permissions = state.borrow_mut::<FP>(); permissions.check(None)?;
.github/workflows/ci.yml+3 −3 modified@@ -236,7 +236,7 @@ jobs: ~/.cargo/registry/index ~/.cargo/registry/cache ~/.cargo/git/db - key: 4-cargo-home-${{ matrix.os }}-${{ hashFiles('Cargo.lock') }} + key: 5-cargo-home-${{ matrix.os }}-${{ hashFiles('Cargo.lock') }} # In main branch, always creates fresh cache - name: Cache build output (main) @@ -252,7 +252,7 @@ jobs: !./target/*/*.zip !./target/*/*.tar.gz key: | - 4-cargo-target-${{ matrix.os }}-${{ matrix.profile }}-${{ github.sha }} + 5-cargo-target-${{ matrix.os }}-${{ matrix.profile }}-${{ github.sha }} # Restore cache from the latest 'main' branch build. - name: Cache build output (PR) @@ -268,7 +268,7 @@ jobs: !./target/*/*.tar.gz key: never_saved restore-keys: | - 4-cargo-target-${{ matrix.os }}-${{ matrix.profile }}- + 5-cargo-target-${{ matrix.os }}-${{ matrix.profile }}- # Don't save cache after building PRs or branches other than 'main'. - name: Skip save cache (PR)
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
7- github.com/advisories/GHSA-838h-jqp6-cf2fghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2022-24783ghsaADVISORY
- github.com/denoland/deno/commit/fcfce1bb869fddc629e6d889d6ba1328b80b0dcfghsaWEB
- github.com/denoland/deno/compare/v1.20.2...v1.20.3ghsaWEB
- github.com/denoland/deno/pull/14115ghsaWEB
- github.com/denoland/deno/releases/tag/v1.20.3ghsaWEB
- github.com/denoland/deno/security/advisories/GHSA-838h-jqp6-cf2fghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.