Skip to content

mysql2: a text column intermittently reads back as a wrong value (617 for 'hi') with a mysql_native_password user #11341

Description

@proggeramlug

Found while validating #11335 on perrymaster (Linux x86_64) against a private MySQL 8.0.46, with mysql2 3.24.4. The oracle is Node 26.5.1.

A string column sometimes comes back as the wrong value. SELECT 1 + 1 AS two, 'hi' AS s through mysql2/promise connection.query sometimes prints 2 617 instead of 2 hi.

Repro:

import mysql from "mysql2/promise";
const c = await mysql.createConnection({ host: "127.0.0.1", port: 43306, user: "perry_native", password: "perry", database: "perry_test" });
const [r] = (await c.query("SELECT 1 + 1 AS two, 'hi' AS s")) as any;
console.log(r[0].two, r[0].s); // node: 2 hi
await c.end();

The user was created with CREATE USER 'perry_native'@'%' IDENTIFIED WITH mysql_native_password BY 'perry'. The build was PERRY_NO_AUTO_OPTIMIZE=1, and the program was run in a loop. I have not reduced it further.

Activity

  1. proggeramlug commented on Sep 26, 2026

    @proggeramlug
    ContributorAuthor

    This no longer reproduces on current main. Bisected on perrymaster against a live MySQL 8.0.46 with mysql2 3.24.4 and a mysql_native_password user. Every arm carries #11366's patch, because without it mysql2 segfaults at connect.

    build SELECT 1 + 1 AS two, 'hi' AS s matched Node
    441d2ab + #11366 patch 0 of 8 runs
    d97339f + #11366 patch 2 of 10 runs (8 wrong)
    3f6eb6c + #11366 patch 10 of 10 runs
    main 2febf42 + #11366, full probe (connect, query, params, transactions, error code, pool, pool transaction) 25 of 25 runs with the native-password user, 25 of 25 with a caching_sha2 user
    d97339f + #11366 patch, the same probe 4 of 25 runs

    The fix lies in the #11297 series ("class captures live in the class environment, not on instances"), in the range cef4301..3f6eb6c. The intermediate commits in that range do not compile on their own, so git bisect cannot narrow it further.

    What the wrong value was: 617 is exactly what packet.parseLengthCodedInt makes of the bytes hi: (0x68-48)*10 + (0x69-48) = 617. So the s column was read with the integer reader. The generated TextRow parser source was correct (result["s"] = packet.readLengthCodedString(fields[1].encoding)), so the wrong method ran at execution time.

    Before #11297, every class in a CommonJS module body stored its captured outer variables as hidden __perry_cap_* keys on each instance, and that shifted the instances' layout. That is consistent with a method or slot being resolved to the wrong entry, but I have not proven it.

    I could not build a package-free regression test:

    • Any instrumentation inside mysql2 (logging from decodeShort, or recording the generated source) makes the bug disappear on the broken build.
    • A seeded GC schedule also makes it disappear, so it is not a collection-timing bug.
    • A hand-written copy of the Packet / generated-TextRow shape does not fail on the broken build.

    While doing this I found a separate latent bug: under some seeded GC schedules, mysql2 throws TypeError: value is not a function in Command.execute. It is filed as #11400.

    I suggest closing this issue as fixed by #11297.

  2. proggeramlug commented on Sep 26, 2026

    @proggeramlug
    ContributorAuthor

    Correction: this is not fixed. On current main (e6ad5f3) the probe reads 2 617 in 4 of 10 runs. The build is PERRY_NO_AUTO_OPTIMIZE=1 with a mysql_native_password user, and #11366 is now on main. The bisect in my previous comment found a window, cef4301..3f6eb6c, in which the bug did not show on this program. That is a change in how often it shows, not a fix. I am continuing to investigate.

  3. proggeramlug commented on Sep 26, 2026

    @proggeramlug
    ContributorAuthor

    I found the cause and the fix is up as #11432. The VTABLE_IC inline cache matched an entry on the method-name address alone. The new Function interpreter, which runs mysql2's generated row parser, passes each call's method name in a short-lived buffer. When a freed parseLengthCodedInt buffer was reused at the same address for readLengthCodedString, the cache ran parseLengthCodedInt in its place. A debug build confirmed this with a backtrace.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions