Repository navigation
Fix throwing string shims in Rust JSG and return a handle from call - #8039
piscisaureus wants to merge 3 commits into
Conversation
|
✅ Approved — No findings. The earlier README note has been fixed, and the rest of the change has no correctness, security or performance problems. The decision is advisory: this comment never approves, blocks or requests changes on the pull request. ✅ All checks passed🔒 Security: ✅ LGTMNo security issues. 📖 AGENTS.md: ✅ LGTMThe 📋 Code Quality: ✅ LGTMNo code changed since the last review, which found no code-quality problems. ⚡ Performance: ✅ LGTMThe length checks are O(1), and the UTF-8 back-off moves at most three bytes. 📝 Documentation: ✅ LGTM
Reviewed commit: |
|
Review: 1 finding (1 warning; 1 note). Updates Rust JSG string/error handling and changes Other findings
Specialist findings not posted (2)
Reviewed commit: a8b1367b · github run |
a8b1367 to
f2e92e9
Compare
|
Addressed the README suggestion: the conventions section and the matching |
Three C++ shims under Rust JSG were declared infallible but could throw a KJ exception: `local_new_string`, `exception_create_from_bytes` and `isolate_throw_error`. All three went through `newUtf8String`, which failed a `KJ_REQUIRE` for a string longer than `v8::String::kMaxLength`. An exception that crosses an infallible cxx bridge becomes a Rust panic, so a `&str` longer than V8's limit panicked through `"…".to_js(lock)`, even though `to_js` returns a `Result`, while `v8::String::new_from_str` returned `None` for the same string. * `ToJS` for `&str` and `String` goes through `v8::String::new_from_str` and fails with `Error::invalid_string_length()`, a `RangeError` with V8's own message, "Invalid string length". The error is not pending, and nothing has entered V8. The `local_new_string` shim is removed. * The `v8::String` constructors decide in Rust whether the input fits `MAX_LENGTH`, so `None` means exactly that, and no length above `i32::MAX` reaches V8's `int` parameter. They used to pass `i32::MAX` for such input and rely on V8 to refuse it. * An error message longer than the limit is cut to fit rather than refused. `error_message_for_v8` cuts it in Rust at a UTF-8 boundary; `exception_create_from_bytes` takes a slice, clamps as a last defence and uses `ToLocalChecked()`, so it is total. A message that long is a bug or embeds user data, and the start of it is what a reader wants. Returning a `Result` would make `Lock::error`, `Lock::throw_exception` and the fast API error path fallible for a case no caller can act on, and a fixed replacement message would lose the information. * `isolate_throw_error` is removed. `Lock::throw_error` creates the error with `Lock::error` and throws it through `isolate_throw_exception`, which already throws nothing while execution is terminating. * `newUtf8String` returns a `MaybeLocal` and never throws; each caller decides. `makeInternedStr` keeps its `KJ_REQUIRE`: the property shims that intern a key are fallible, and resource template names come from Rust source. Tests: `v8_length` and `utf8_prefix` are unit-tested at the boundary and with a small limit, since a real string of `MAX_LENGTH + 1` bytes costs 512 MiB. The harness tests do use one, allocated zeroed so that it costs no physical memory until read, which refusing it by length never does: the constructors return `None`, `to_js` returns the `RangeError`, and a method returning the string throws `RangeError: Invalid string length` to JavaScript. `Lock::throw_error` and `Lock::range_error` are checked from JavaScript too.
`Local<Function>::call` was the one method in the `v8` module that
converted its result with `FromJS`. Two thirds of its callers asked for
`v8::Local<v8::Value>`, that is, for no conversion. `Object::get`
returns a handle, as do C++ JSG's `JsFunction::call` and rusty_v8's
`Function::call`.
`call` now returns `Result<Local<Value>, Error>`. A caller that wants a
Rust value converts the handle itself:
let result = f.call(lock, None, &[])?;
let n = f64::from_js(lock, result)?;
Every caller, the doc examples in `lib.rs` and `v8.rs`, the compile
tests and the benchmark fixture are updated. `src/rust/api`,
`node-exceptions` and `i18n` do not call it.
The `v8` module takes handles and returns handles, and `FromJS`/`ToJS` are the only conversions between Rust and JavaScript values. The README gets a short section on when a method returns or takes a Rust value, when it returns `Result` or `Option`, that no method is an infallible signature over a C++ shim that can throw, and the `as_*`/`to_*`/ `value`/`*_value` naming. `src/rust/AGENTS.md` gets a matching bullet.
f2e92e9 to
fd894c5
Compare
Rust JSG's
v8module takes handles and returns handles. Convertingbetween a Rust value and a JavaScript value is the job of
FromJSandToJS, not of av8method. Av8method returns a Rust value onlywhere V8 itself returns a C scalar, from a typed handle (
value,length,value_of,is_*) or from one of V8's coercions of anyvalue (
int32_value,uint32_value,integer_value), or copiesmemory out (
to_vec,to_string), and takes raw Rust data only whereV8 takes C data: constructor input, indices, times and
&strpropertykeys. The one method that breaks the rule is
Local<Function>::call,which converts its result with
FromJS; two thirds of its callers askfor
Local<Value>, that is, for no conversion.A survey of the module also found a bug. Three C++ shims are declared
infallible but can throw a KJ exception:
local_new_string,exception_create_from_bytesandisolate_throw_error. All threebuild their string through
newUtf8String, which fails aKJ_REQUIREfor input longer than
v8::String::kMaxLength. An exception thatcrosses a cxx bridge function not declared
Resultbecomes a Rustpanic, so a string longer than V8's limit (about 512 MiB) returns
Nonefromv8::String::new_from_strbut panics through"…".to_js(lock), althoughto_jsreturns aResult.Commits, each of which builds and passes the tests on its own:
Strings
ToJSfor&strandStringgoes throughv8::String::new_from_strand fails withError::invalid_string_length(), aRangeErrorwith V8's ownmessage, "Invalid string length". The error is not pending and
nothing has entered V8. The
local_new_stringshim is removed.#[jsg_struct]converts its fields withToJS(since Remove ToLocalValue from Rust JSG #8014), soan overlong
Stringfield gives the sameRangeError.v8::Stringconstructors decide in Rust whether the input fitsMAX_LENGTH, soNonemeans exactly that, and no length abovei32::MAXreaches V8'sintparameter. V8'sNewFromUtf8measuresits input against
kMaxLengthin bytes too, so the Rust checkrefuses exactly what V8 refuses.
new_from_utf8_literalkeeps itspanic, as V8's
NewFromUtf8Literalfails astatic_assert.Error messages
refused.
error_message_for_v8cuts it in Rust at a UTF-8 boundary;exception_create_from_bytestakes a slice, clamps as a lastdefence and uses
ToLocalChecked(), so it is total. A message thatlong is a bug or embeds user data, and its start is what a reader
wants. Returning a
Resultwould makeLock::error,Lock::throw_exceptionand the fast API error path fallible for acase no caller can act on, and a fixed replacement message would
lose the information.
isolate_throw_erroris removed.Lock::throw_errorcreates theerror with
Lock::errorand throws it throughisolate_throw_exception, after the same termination check asbefore.
newUtf8Stringreturns aMaybeLocaland never throws; each callerdecides.
makeInternedStrkeeps itsKJ_REQUIRE: the propertyshims that intern a
&strkey andobject_new_with_data_properties(used by
#[jsg_struct]and, since Add jsg::Dict to Rust JSG #7998,jsg::Dict) are declaredfallible, and resource template names come from Rust source.
Function::callcallreturnsResult<Local<Value>, Error>. A caller that wants aRust value converts the handle itself:
Every caller, the doc examples in
lib.rs,v8.rsand the README,the compile tests and the benchmark fixture are converted. With #8054
(no
jsg::Number) and #8072 (integerFromJS/ToJS), the convertedtests read their results as
u32,i32orf64.src/rust/api,node-exceptions,i18nand the server crate do not call it.Conventions
The README gets a section "Conventions of the
v8module", andsrc/rust/AGENTS.mda matching bullet: when a method returns or takesa Rust value, when it returns
ResultorOption, that no method hasan infallible signature over a C++ shim that can throw, and the
as_*/to_*/value/*_valuenaming. It covers the APIs from#8054 and #8072:
Local<Number>::valueand the other typed-handlereads,
Number::newandInteger::newas constructors taking C data,and the
int32_value/uint32_value/integer_valuecoercions, whichcan run JavaScript and so return
Result.Tests
v8_lengthandutf8_prefixare unit-tested at the boundary and witha small limit, since a real string of
MAX_LENGTH + 1bytes costs512 MiB. The harness tests do use one, allocated zeroed so that it
costs no physical memory until read, which refusing it by length never
does: the constructors return
None,to_jsreturns theRangeError,and a method returning the string throws
RangeError: Invalid string lengthto JavaScript.Lock::throw_error,Lock::range_errorandError::invalid_string_lengthare checked from JavaScript. Removingthe length check, the UTF-8 back-off, the message cut, the error type
or the throw in
throw_erroreach fails a test.Related
ToLocalValue, so this change no longer has to makeToLocalValuefor&strpanic in Rust; that part is gone.ToJSfor strings and the error shims, and this change is writtento fit either order.
Not done here
Local<String>::to_string(alossy copy to a Rust
String) andValue::type_of, which returns aRust
Stringrather than a handle.v8::String::MAX_LENGTHis a copy ofkMaxLength, checked againsta hard-coded number. The C++ clamp and V8's own check keep a drift
from crashing anything, but the
RangeErrorthreshold would be off.