fix(runtime): a VTABLE_IC hit also requires the method name bytes; mysql2 no longer reads 'hi' as 617 (#11341) - #11432
Conversation
|
Navigate logical layers of code changes, visualize relationships, and explore their blast radius. No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (4)
Included review availability: This review used your included allowance. Your plan provides up to 8 included reviews per hour; 3 remain after this review. 📝 WalkthroughWalkthroughThe vtable inline cache now stores and compares method-name bytes in addition to the name pointer. Collection and handle method paths pass those bytes during cache lookup and insertion. A regression test checks that different names at the same buffer address produce a cache miss. ChangesVtable cache name validation
Priority: ➖ Normal Estimated code review effort: 3 (Moderate) | ~20 minutes Change: Bug fix · Severity of issue fixed: Medium Merge Risk: ⚪ Minimal · up to No actionable merge-blocking risk is identified for the vtable cache fix; it is ready for normal merge checks. Security Architecture ReviewSecurity architecture risk: 🔵 Low · up to The change makes method matching stricter without evidently expanding who can invoke a method. No introduced security concern was identified in the reviewed call paths, though broader runtime coverage remains uncertain. Retained concerns Security review detailsSecurity Blast Radius
Trust Boundaries and Controls
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Full details: Docstring CoverageExplanation Docstring coverage is 71.43% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 7 functions across 3 files. (1 skipped: 1 unsupported.)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Fixes #11341
What was wrong
Under compiled mysql2 3.24.4,
SELECT 1 + 1 AS two, 'hi' AS sintermittently returneds = 617instead of'hi'. 617 is whatparseLengthCodedIntmakes of the byteshi:(0x68-48)*10 + (0x69-48). So the callpacket.readLengthCodedString(...)was runningparseLengthCodedInt.mysql2 builds its row parser as source text and runs it through
new Function, which Perry executes in its runtime interpreter (dyn_eval). The generated source is correct:The interpreter dispatches each member call by name. For each call it creates a new heap
Stringfor the name (i.sym.to_string()indyn_eval/expr.rs) and frees it when the call returns. When the receiver is not on the class fast path, the call reacheshandle_methods::dispatch_handle, which consults the per-threadVTABLE_IC. That cache chose and matched its entry on(class_id, name address)alone. OnceparseLengthCodedInt's freed buffer was reused at the same address forreadLengthCodedString, the lookup returned the cachedparseLengthCodedIntentry.To confirm the cause rather than infer it, I ran a debug build that reports an address-equal, bytes-different hit, together with a backtrace, against a live MySQL 8.0.46:
js_native_call_method_valuealso reaches this cache, with bytes from a GC string. The siblingOBJ_DISPATCH_IC(#7769) was written knowing that name addresses are not stable and has always compared the bytes.VTABLE_IChad not been updated to match.The fix
A
VTABLE_ICentry now stores the method name's bytes (up to 24; longer names are not cached, the same limit asOBJ_DISPATCH_IC). A hit requires the bytes to match as well as the class id and address. The slot is still chosen by address, so the lookup cost is one short compare on the hit. Both callers,handle_methods.rsandcollection_methods.rs, passmethod_name.as_bytes().Evidence
Arm A is main at e6ad5f3 and arm B is this branch. Both were built the same way (
cargo build --release -p perry -p perry-runtime-static -p perry-stdlib-static, codegen-units 16,PERRY_NO_AUTO_OPTIMIZE=1programs). The databases were a live MySQL 8.0.46 and PostgreSQL 16.15, with mysql2 3.24.4. The oracle is Node 26.5.1.mysql_native_passworduser, main-thread row2 617)caching_sha2_passworduserPooland a pool-connection transaction; both usersOn B the debug build reports zero mismatches. How often it shows depends on how the allocator reuses the freed name buffer, so the bug is intermittent and appears only in some program shapes. The probe in the third row does not trigger it on either arm.
Unit test:
vtable_ic_hit_requires_matching_name_bytes_not_only_the_address(inclass_registry/dispatch.rs, next to the existingOBJ_DISPATCH_ICtests). It inserts an entry through one buffer, rewrites the buffer in place with a different name, and asserts the lookup misses. On main the same sequence would have hit.No gap test. I tried to write a package-free gap test and could not make one fail on main:
new Functionbodies calling two methods with names of the same length, a receiver forced off the fast path, and computed keys with a GC between the two calls. Freed name buffers were not reused at the same address in those programs. So the regression coverage is the unit test above plus the live-server run. I would rather say that than add a gap test that passes on both arms.Checks run
RUST_TEST_THREADS=1 cargo test --release -p perry-runtime --lib: 4632 passed, and 2event_pump::teststiming tests failed during that run (host load average about 48). Re-runningevent_pump::teststhree times on its own gave 6/6 each time.RUSTFLAGS=-D warnings cargo check -p perry-runtime --all-targets: clean.cargo fmt --check: clean.SKIP_COMPILE_GATES=1 scripts/run_lint_gates.sh: 90 of 92 script gates pass. The two failures are Public benchmark evidence freshness (red on main) andcargo xwin(not installed on the host). The compile tier was not run.git statuswas clean afterwards.Not run
Related
worker_threadsworker. That is a separate cause (socket events racing between agents) with its own PR.value is not a functioninCommand.execute.js_buffer_write_lenon current main.Summary by CodeRabbit