1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-11-25 15:29:32 -05:00
denoland-deno/ext/node/ops/vm_internal.rs
Luca Casonato 4fa8869f24
feat(ext/node): rewrite crypto keys (#24463)
This completely rewrites how we handle key material in ext/node. Changes
in this
PR:

- **Signing**
  - RSA
  - RSA-PSS 🆕
  - DSA 🆕
  - EC
  - ED25519 🆕
- **Verifying**
  - RSA
  - RSA-PSS 🆕
  - DSA 🆕
  - EC 🆕
  - ED25519 🆕
- **Private key import**
  - Passphrase encrypted private keys 🆕
  - RSA
    - PEM
    - DER (PKCS#1) 🆕
    - DER (PKCS#8) 🆕
  - RSA-PSS
    - PEM
    - DER (PKCS#1) 🆕
    - DER (PKCS#8) 🆕
  - DSA 🆕
  - EC
    - PEM
    - DER (SEC1) 🆕
    - DER (PKCS#8) 🆕
  - X25519 🆕
  - ED25519 🆕
  - DH
- **Public key import**
  - RSA
    - PEM
    - DER (PKCS#1) 🆕
    - DER (PKCS#8) 🆕
  - RSA-PSS 🆕
  - DSA 🆕
  - EC 🆕
  - X25519 🆕
  - ED25519 🆕
  - DH 🆕
- **Private key export**
  - RSA 🆕
  - DSA 🆕
  - EC 🆕
  - X25519 🆕
  - ED25519 🆕
  - DH 🆕
- **Public key export**
  - RSA
  - DSA 🆕
  - EC 🆕
  - X25519 🆕
  - ED25519 🆕
  - DH 🆕
- **Key pair generation**
  - Overhauled, but supported APIs unchanged

This PR adds a lot of new individual functionality. But most importantly
because
of the new key material representation, it is now trivial to add new
algorithms
(as shown by this PR).

Now, when adding a new algorithm, it is also widely supported - for
example
previously we supported ED25519 key pair generation, but we could not
import,
export, sign or verify with ED25519. We can now do all of those things.
2024-08-07 08:43:58 +02:00

696 lines
21 KiB
Rust

// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
use crate::create_host_defined_options;
use deno_core::error::type_error;
use deno_core::error::AnyError;
use deno_core::v8;
use deno_core::v8::MapFnTo;
pub const PRIVATE_SYMBOL_NAME: v8::OneByteConst =
v8::String::create_external_onebyte_const(b"node:contextify:context");
/// An unbounded script that can be run in a context.
#[derive(Debug)]
pub struct ContextifyScript {
script: v8::Global<v8::UnboundScript>,
}
impl ContextifyScript {
pub fn new(
scope: &mut v8::HandleScope,
source_str: v8::Local<v8::String>,
) -> Result<Self, AnyError> {
let resource_name = v8::undefined(scope);
let host_defined_options = create_host_defined_options(scope);
let origin = v8::ScriptOrigin::new(
scope,
resource_name.into(),
0,
0,
false,
0,
None,
false,
false,
false,
Some(host_defined_options),
);
let mut source =
v8::script_compiler::Source::new(source_str, Some(&origin));
let unbound_script = v8::script_compiler::compile_unbound_script(
scope,
&mut source,
v8::script_compiler::CompileOptions::NoCompileOptions,
v8::script_compiler::NoCacheReason::NoReason,
)
.ok_or_else(|| type_error("Failed to compile script"))?;
let script = v8::Global::new(scope, unbound_script);
Ok(Self { script })
}
// TODO(littledivy): Support `options`
pub fn eval_machine<'s>(
&self,
scope: &mut v8::HandleScope<'s>,
_context: v8::Local<v8::Context>,
) -> Option<v8::Local<'s, v8::Value>> {
let tc_scope = &mut v8::TryCatch::new(scope);
let unbound_script = v8::Local::new(tc_scope, self.script.clone());
let script = unbound_script.bind_to_current_context(tc_scope);
let result = script.run(tc_scope);
if tc_scope.has_caught() {
// If there was an exception thrown during script execution, re-throw it.
if !tc_scope.has_terminated() {
tc_scope.rethrow();
}
return None;
}
Some(result.unwrap())
}
}
pub struct ContextifyContext {
context: v8::TracedReference<v8::Context>,
sandbox: v8::TracedReference<v8::Object>,
}
impl deno_core::GarbageCollected for ContextifyContext {
fn trace(&self, visitor: &v8::cppgc::Visitor) {
visitor.trace(&self.context);
visitor.trace(&self.sandbox);
}
}
impl ContextifyContext {
pub fn attach(
scope: &mut v8::HandleScope,
sandbox_obj: v8::Local<v8::Object>,
) {
let tmp = init_global_template(scope, ContextInitMode::UseSnapshot);
let context = create_v8_context(scope, tmp, ContextInitMode::UseSnapshot);
Self::from_context(scope, context, sandbox_obj);
}
fn from_context(
scope: &mut v8::HandleScope,
v8_context: v8::Local<v8::Context>,
sandbox_obj: v8::Local<v8::Object>,
) {
let main_context = scope.get_current_context();
let context_state = main_context.get_aligned_pointer_from_embedder_data(
deno_core::CONTEXT_STATE_SLOT_INDEX,
);
let module_map = main_context
.get_aligned_pointer_from_embedder_data(deno_core::MODULE_MAP_SLOT_INDEX);
v8_context.set_security_token(main_context.get_security_token(scope));
// SAFETY: set embedder data from the creation context
unsafe {
v8_context.set_aligned_pointer_in_embedder_data(
deno_core::CONTEXT_STATE_SLOT_INDEX,
context_state,
);
v8_context.set_aligned_pointer_in_embedder_data(
deno_core::MODULE_MAP_SLOT_INDEX,
module_map,
);
}
let context = v8::TracedReference::new(scope, v8_context);
let sandbox = v8::TracedReference::new(scope, sandbox_obj);
let wrapper =
deno_core::cppgc::make_cppgc_object(scope, Self { context, sandbox });
let ptr =
deno_core::cppgc::try_unwrap_cppgc_object::<Self>(scope, wrapper.into());
// SAFETY: We are storing a pointer to the ContextifyContext
// in the embedder data of the v8::Context. The contextified wrapper
// lives longer than the execution context, so this should be safe.
unsafe {
v8_context.set_aligned_pointer_in_embedder_data(
3,
&*ptr.unwrap() as *const ContextifyContext as _,
);
}
let private_str =
v8::String::new_from_onebyte_const(scope, &PRIVATE_SYMBOL_NAME);
let private_symbol = v8::Private::for_api(scope, private_str);
sandbox_obj.set_private(scope, private_symbol, wrapper.into());
}
pub fn from_sandbox_obj<'a>(
scope: &mut v8::HandleScope<'a>,
sandbox_obj: v8::Local<v8::Object>,
) -> Option<&'a Self> {
let private_str =
v8::String::new_from_onebyte_const(scope, &PRIVATE_SYMBOL_NAME);
let private_symbol = v8::Private::for_api(scope, private_str);
sandbox_obj
.get_private(scope, private_symbol)
.and_then(|wrapper| {
deno_core::cppgc::try_unwrap_cppgc_object::<Self>(scope, wrapper)
// SAFETY: the lifetime of the scope does not actually bind to
// the lifetime of this reference at all, but the object we read
// it from does, so it will be alive at least that long.
.map(|r| unsafe { &*(&*r as *const _) })
})
}
pub fn is_contextify_context(
scope: &mut v8::HandleScope,
object: v8::Local<v8::Object>,
) -> bool {
Self::from_sandbox_obj(scope, object).is_some()
}
pub fn context<'a>(
&self,
scope: &mut v8::HandleScope<'a>,
) -> v8::Local<'a, v8::Context> {
self.context.get(scope).unwrap()
}
fn global_proxy<'s>(
&self,
scope: &mut v8::HandleScope<'s>,
) -> v8::Local<'s, v8::Object> {
let ctx = self.context(scope);
ctx.global(scope)
}
fn sandbox<'a>(
&self,
scope: &mut v8::HandleScope<'a>,
) -> v8::Local<'a, v8::Object> {
self.sandbox.get(scope).unwrap()
}
fn get<'a, 'c>(
scope: &mut v8::HandleScope<'a>,
object: v8::Local<'a, v8::Object>,
) -> Option<&'c ContextifyContext> {
let context = object.get_creation_context(scope)?;
let context_ptr = context.get_aligned_pointer_from_embedder_data(3);
if context_ptr.is_null() {
return None;
}
// SAFETY: We are storing a pointer to the ContextifyContext
// in the embedder data of the v8::Context during creation.
Some(unsafe { &*(context_ptr as *const ContextifyContext) })
}
}
pub const VM_CONTEXT_INDEX: usize = 0;
#[derive(PartialEq)]
pub enum ContextInitMode {
ForSnapshot,
UseSnapshot,
}
pub fn create_v8_context<'a>(
scope: &mut v8::HandleScope<'a, ()>,
object_template: v8::Local<v8::ObjectTemplate>,
mode: ContextInitMode,
) -> v8::Local<'a, v8::Context> {
let scope = &mut v8::EscapableHandleScope::new(scope);
let context = if mode == ContextInitMode::UseSnapshot {
v8::Context::from_snapshot(scope, VM_CONTEXT_INDEX, Default::default())
.unwrap()
} else {
let ctx = v8::Context::new(
scope,
v8::ContextOptions {
global_template: Some(object_template),
..Default::default()
},
);
// SAFETY: ContextifyContexts will update this to a pointer to the native object
unsafe {
ctx.set_aligned_pointer_in_embedder_data(1, std::ptr::null_mut());
ctx.set_aligned_pointer_in_embedder_data(2, std::ptr::null_mut());
ctx.set_aligned_pointer_in_embedder_data(3, std::ptr::null_mut());
ctx.clear_all_slots();
};
ctx
};
scope.escape(context)
}
#[derive(Debug, Clone)]
struct SlotContextifyGlobalTemplate(v8::Global<v8::ObjectTemplate>);
pub fn init_global_template<'a>(
scope: &mut v8::HandleScope<'a, ()>,
mode: ContextInitMode,
) -> v8::Local<'a, v8::ObjectTemplate> {
let maybe_object_template_slot =
scope.get_slot::<SlotContextifyGlobalTemplate>();
if maybe_object_template_slot.is_none() {
let global_object_template = init_global_template_inner(scope);
if mode == ContextInitMode::UseSnapshot {
let contextify_global_template_slot = SlotContextifyGlobalTemplate(
v8::Global::new(scope, global_object_template),
);
scope.set_slot(contextify_global_template_slot);
}
global_object_template
} else {
let object_template_slot = maybe_object_template_slot
.expect("ContextifyGlobalTemplate slot should be already populated.")
.clone();
v8::Local::new(scope, object_template_slot.0)
}
}
// Using thread_local! to get around compiler bug.
//
// See NOTE in ext/node/global.rs#L12
thread_local! {
pub static GETTER_MAP_FN: v8::NamedPropertyGetterCallback<'static> = property_getter.map_fn_to();
pub static SETTER_MAP_FN: v8::NamedPropertySetterCallback<'static> = property_setter.map_fn_to();
pub static DELETER_MAP_FN: v8::NamedPropertyDeleterCallback<'static> = property_deleter.map_fn_to();
pub static ENUMERATOR_MAP_FN: v8::NamedPropertyEnumeratorCallback<'static> = property_enumerator.map_fn_to();
pub static DEFINER_MAP_FN: v8::NamedPropertyDefinerCallback<'static> = property_definer.map_fn_to();
pub static DESCRIPTOR_MAP_FN: v8::NamedPropertyGetterCallback<'static> = property_descriptor.map_fn_to();
}
thread_local! {
pub static INDEXED_GETTER_MAP_FN: v8::IndexedPropertyGetterCallback<'static> = indexed_property_getter.map_fn_to();
pub static INDEXED_SETTER_MAP_FN: v8::IndexedPropertySetterCallback<'static> = indexed_property_setter.map_fn_to();
pub static INDEXED_DELETER_MAP_FN: v8::IndexedPropertyDeleterCallback<'static> = indexed_property_deleter.map_fn_to();
pub static INDEXED_DEFINER_MAP_FN: v8::IndexedPropertyDefinerCallback<'static> = indexed_property_definer.map_fn_to();
pub static INDEXED_DESCRIPTOR_MAP_FN: v8::IndexedPropertyGetterCallback<'static> = indexed_property_descriptor.map_fn_to();
}
pub fn init_global_template_inner<'a>(
scope: &mut v8::HandleScope<'a, ()>,
) -> v8::Local<'a, v8::ObjectTemplate> {
let global_object_template = v8::ObjectTemplate::new(scope);
global_object_template.set_internal_field_count(3);
let named_property_handler_config = {
let mut config = v8::NamedPropertyHandlerConfiguration::new()
.flags(v8::PropertyHandlerFlags::HAS_NO_SIDE_EFFECT);
config = GETTER_MAP_FN.with(|getter| config.getter_raw(*getter));
config = SETTER_MAP_FN.with(|setter| config.setter_raw(*setter));
config = DELETER_MAP_FN.with(|deleter| config.deleter_raw(*deleter));
config =
ENUMERATOR_MAP_FN.with(|enumerator| config.enumerator_raw(*enumerator));
config = DEFINER_MAP_FN.with(|definer| config.definer_raw(*definer));
config =
DESCRIPTOR_MAP_FN.with(|descriptor| config.descriptor_raw(*descriptor));
config
};
let indexed_property_handler_config = {
let mut config = v8::IndexedPropertyHandlerConfiguration::new()
.flags(v8::PropertyHandlerFlags::HAS_NO_SIDE_EFFECT);
config = INDEXED_GETTER_MAP_FN.with(|getter| config.getter_raw(*getter));
config = INDEXED_SETTER_MAP_FN.with(|setter| config.setter_raw(*setter));
config =
INDEXED_DELETER_MAP_FN.with(|deleter| config.deleter_raw(*deleter));
config =
ENUMERATOR_MAP_FN.with(|enumerator| config.enumerator_raw(*enumerator));
config =
INDEXED_DEFINER_MAP_FN.with(|definer| config.definer_raw(*definer));
config = INDEXED_DESCRIPTOR_MAP_FN
.with(|descriptor| config.descriptor_raw(*descriptor));
config
};
global_object_template
.set_named_property_handler(named_property_handler_config);
global_object_template
.set_indexed_property_handler(indexed_property_handler_config);
global_object_template
}
fn property_getter<'s>(
scope: &mut v8::HandleScope<'s>,
key: v8::Local<'s, v8::Name>,
args: v8::PropertyCallbackArguments<'s>,
mut ret: v8::ReturnValue,
) -> v8::Intercepted {
let Some(ctx) = ContextifyContext::get(scope, args.this()) else {
return v8::Intercepted::No;
};
let sandbox = ctx.sandbox(scope);
let tc_scope = &mut v8::TryCatch::new(scope);
let maybe_rv = sandbox.get_real_named_property(tc_scope, key).or_else(|| {
ctx
.global_proxy(tc_scope)
.get_real_named_property(tc_scope, key)
});
if let Some(mut rv) = maybe_rv {
if tc_scope.has_caught() && !tc_scope.has_terminated() {
tc_scope.rethrow();
}
if rv == sandbox {
rv = ctx.global_proxy(tc_scope).into();
}
ret.set(rv);
return v8::Intercepted::Yes;
}
v8::Intercepted::No
}
fn property_setter<'s>(
scope: &mut v8::HandleScope<'s>,
key: v8::Local<'s, v8::Name>,
value: v8::Local<'s, v8::Value>,
args: v8::PropertyCallbackArguments<'s>,
mut rv: v8::ReturnValue<()>,
) -> v8::Intercepted {
let Some(ctx) = ContextifyContext::get(scope, args.this()) else {
return v8::Intercepted::No;
};
let (attributes, is_declared_on_global_proxy) = match ctx
.global_proxy(scope)
.get_real_named_property_attributes(scope, key)
{
Some(attr) => (attr, true),
None => (v8::PropertyAttribute::NONE, false),
};
let mut read_only = attributes.is_read_only();
let (attributes, is_declared_on_sandbox) = match ctx
.sandbox(scope)
.get_real_named_property_attributes(scope, key)
{
Some(attr) => (attr, true),
None => (v8::PropertyAttribute::NONE, false),
};
read_only |= attributes.is_read_only();
if read_only {
return v8::Intercepted::No;
}
// true for x = 5
// false for this.x = 5
// false for Object.defineProperty(this, 'foo', ...)
// false for vmResult.x = 5 where vmResult = vm.runInContext();
let is_contextual_store = ctx.global_proxy(scope) != args.this();
// Indicator to not return before setting (undeclared) function declarations
// on the sandbox in strict mode, i.e. args.ShouldThrowOnError() = true.
// True for 'function f() {}', 'this.f = function() {}',
// 'var f = function()'.
// In effect only for 'function f() {}' because
// var f = function(), is_declared = true
// this.f = function() {}, is_contextual_store = false.
let is_function = value.is_function();
let is_declared = is_declared_on_global_proxy || is_declared_on_sandbox;
if !is_declared
&& args.should_throw_on_error()
&& is_contextual_store
&& !is_function
{
return v8::Intercepted::No;
}
if !is_declared && key.is_symbol() {
return v8::Intercepted::No;
};
if ctx.sandbox(scope).set(scope, key.into(), value).is_none() {
return v8::Intercepted::No;
}
if is_declared_on_sandbox {
if let Some(desc) =
ctx.sandbox(scope).get_own_property_descriptor(scope, key)
{
if !desc.is_undefined() {
let desc_obj: v8::Local<v8::Object> = desc.try_into().unwrap();
// We have to specify the return value for any contextual or get/set
// property
let get_key =
v8::String::new_external_onebyte_static(scope, b"get").unwrap();
let set_key =
v8::String::new_external_onebyte_static(scope, b"get").unwrap();
if desc_obj
.has_own_property(scope, get_key.into())
.unwrap_or(false)
|| desc_obj
.has_own_property(scope, set_key.into())
.unwrap_or(false)
{
rv.set_bool(true);
return v8::Intercepted::Yes;
}
}
}
}
v8::Intercepted::No
}
fn property_deleter<'s>(
scope: &mut v8::HandleScope<'s>,
key: v8::Local<'s, v8::Name>,
args: v8::PropertyCallbackArguments<'s>,
mut rv: v8::ReturnValue<v8::Boolean>,
) -> v8::Intercepted {
let Some(ctx) = ContextifyContext::get(scope, args.this()) else {
return v8::Intercepted::No;
};
let context = ctx.context(scope);
let sandbox = ctx.sandbox(scope);
let context_scope = &mut v8::ContextScope::new(scope, context);
if !sandbox.delete(context_scope, key.into()).unwrap_or(false) {
return v8::Intercepted::No;
}
rv.set_bool(false);
v8::Intercepted::Yes
}
fn property_enumerator<'s>(
scope: &mut v8::HandleScope<'s>,
args: v8::PropertyCallbackArguments<'s>,
mut rv: v8::ReturnValue<v8::Array>,
) {
let Some(ctx) = ContextifyContext::get(scope, args.this()) else {
return;
};
let context = ctx.context(scope);
let sandbox = ctx.sandbox(scope);
let context_scope = &mut v8::ContextScope::new(scope, context);
let Some(properties) = sandbox
.get_property_names(context_scope, v8::GetPropertyNamesArgs::default())
else {
return;
};
rv.set(properties);
}
fn property_definer<'s>(
scope: &mut v8::HandleScope<'s>,
key: v8::Local<'s, v8::Name>,
desc: &v8::PropertyDescriptor,
args: v8::PropertyCallbackArguments<'s>,
_: v8::ReturnValue<()>,
) -> v8::Intercepted {
let Some(ctx) = ContextifyContext::get(scope, args.this()) else {
return v8::Intercepted::No;
};
let context = ctx.context(scope);
let (attributes, is_declared) = match ctx
.global_proxy(scope)
.get_real_named_property_attributes(scope, key)
{
Some(attr) => (attr, true),
None => (v8::PropertyAttribute::NONE, false),
};
let read_only = attributes.is_read_only();
let dont_delete = attributes.is_dont_delete();
// If the property is set on the global as read_only, don't change it on
// the global or sandbox.
if is_declared && read_only && dont_delete {
return v8::Intercepted::No;
}
let sandbox = ctx.sandbox(scope);
let scope = &mut v8::ContextScope::new(scope, context);
let define_prop_on_sandbox =
|scope: &mut v8::HandleScope,
desc_for_sandbox: &mut v8::PropertyDescriptor| {
if desc.has_enumerable() {
desc_for_sandbox.set_enumerable(desc.enumerable());
}
if desc.has_configurable() {
desc_for_sandbox.set_configurable(desc.configurable());
}
sandbox.define_property(scope, key, desc_for_sandbox);
};
if desc.has_get() || desc.has_set() {
let mut desc_for_sandbox = v8::PropertyDescriptor::new_from_get_set(
if desc.has_get() {
desc.get()
} else {
v8::undefined(scope).into()
},
if desc.has_set() {
desc.set()
} else {
v8::undefined(scope).into()
},
);
define_prop_on_sandbox(scope, &mut desc_for_sandbox);
} else {
let value = if desc.has_value() {
desc.value()
} else {
v8::undefined(scope).into()
};
if desc.has_writable() {
let mut desc_for_sandbox =
v8::PropertyDescriptor::new_from_value_writable(value, desc.writable());
define_prop_on_sandbox(scope, &mut desc_for_sandbox);
} else {
let mut desc_for_sandbox = v8::PropertyDescriptor::new_from_value(value);
define_prop_on_sandbox(scope, &mut desc_for_sandbox);
}
}
v8::Intercepted::Yes
}
fn property_descriptor<'s>(
scope: &mut v8::HandleScope<'s>,
key: v8::Local<'s, v8::Name>,
args: v8::PropertyCallbackArguments<'s>,
mut rv: v8::ReturnValue,
) -> v8::Intercepted {
let Some(ctx) = ContextifyContext::get(scope, args.this()) else {
return v8::Intercepted::No;
};
let context = ctx.context(scope);
let sandbox = ctx.sandbox(scope);
let scope = &mut v8::ContextScope::new(scope, context);
if sandbox.has_own_property(scope, key).unwrap_or(false) {
if let Some(desc) = sandbox.get_own_property_descriptor(scope, key) {
rv.set(desc);
return v8::Intercepted::Yes;
}
}
v8::Intercepted::No
}
fn uint32_to_name<'s>(
scope: &mut v8::HandleScope<'s>,
index: u32,
) -> v8::Local<'s, v8::Name> {
let int = v8::Integer::new_from_unsigned(scope, index);
let u32 = v8::Local::<v8::Uint32>::try_from(int).unwrap();
u32.to_string(scope).unwrap().into()
}
fn indexed_property_getter<'s>(
scope: &mut v8::HandleScope<'s>,
index: u32,
args: v8::PropertyCallbackArguments<'s>,
rv: v8::ReturnValue,
) -> v8::Intercepted {
let key = uint32_to_name(scope, index);
property_getter(scope, key, args, rv)
}
fn indexed_property_setter<'s>(
scope: &mut v8::HandleScope<'s>,
index: u32,
value: v8::Local<'s, v8::Value>,
args: v8::PropertyCallbackArguments<'s>,
rv: v8::ReturnValue<()>,
) -> v8::Intercepted {
let key = uint32_to_name(scope, index);
property_setter(scope, key, value, args, rv)
}
fn indexed_property_deleter<'s>(
scope: &mut v8::HandleScope<'s>,
index: u32,
args: v8::PropertyCallbackArguments<'s>,
mut rv: v8::ReturnValue<v8::Boolean>,
) -> v8::Intercepted {
let Some(ctx) = ContextifyContext::get(scope, args.this()) else {
return v8::Intercepted::No;
};
let context = ctx.context(scope);
let sandbox = ctx.sandbox(scope);
let context_scope = &mut v8::ContextScope::new(scope, context);
if !sandbox.delete_index(context_scope, index).unwrap_or(false) {
return v8::Intercepted::No;
}
// Delete failed on the sandbox, intercept and do not delete on
// the global object.
rv.set_bool(false);
v8::Intercepted::No
}
fn indexed_property_definer<'s>(
scope: &mut v8::HandleScope<'s>,
index: u32,
descriptor: &v8::PropertyDescriptor,
args: v8::PropertyCallbackArguments<'s>,
rv: v8::ReturnValue<()>,
) -> v8::Intercepted {
let key = uint32_to_name(scope, index);
property_definer(scope, key, descriptor, args, rv)
}
fn indexed_property_descriptor<'s>(
scope: &mut v8::HandleScope<'s>,
index: u32,
args: v8::PropertyCallbackArguments<'s>,
rv: v8::ReturnValue,
) -> v8::Intercepted {
let key = uint32_to_name(scope, index);
property_descriptor(scope, key, args, rv)
}