-
Notifications
You must be signed in to change notification settings - Fork 4.7k
sql(mysql): bounds-check params in bind() when values array is mutated #29886
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+151
−2
Merged
Changes from 8 commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
d7dc5f3
sql(mysql): bounds-check params in bind() when values array is mutated
robobun 6925a6a
Merge remote-tracking branch 'origin/main' into sync-tmp
dylan-conway 1525676
sql(mysql): bind before writing packet header so bind errors don't de…
robobun ade2389
Merge remote-tracking branch
robobun f133e38
sql(mysql): clear need_to_send_params only after packet fully written
robobun 6156bc3
test(mysql): filter ASAN startup warning from subprocess stderr
robobun 8fd83e8
test(mysql): move bind-OOB regression to standalone test file
robobun 325c8ff
[autofix.ci] apply automated fixes
autofix-ci[bot] 89590a3
test(mysql): fail loudly if MYSQL_URL is set but unreachable
robobun c641a02
Merge origin/main
robobun 1451d6d
test(mysql): drop explicit per-test timeout
robobun 1cc6b1d
Merge origin/main
robobun File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| // Reproducer for an out-of-bounds write in MySQLQuery.bind(). | ||
| // | ||
| // Signature.generate() and bind() each create a fresh iterator over the | ||
| // user-supplied params array. If an index getter mutates the array so that | ||
| // the second iteration is longer than the first, bind() would index past the | ||
| // `params` / `param_types` buffers it sized based on the first iteration. | ||
| // | ||
| // Without the bounds check this panics in debug builds (index out of bounds) | ||
| // and is a silent heap overflow in release builds. | ||
|
|
||
| import { SQL } from "bun"; | ||
|
|
||
| const url = process.env.MYSQL_URL; | ||
| if (!url) throw new Error("MYSQL_URL is required"); | ||
|
|
||
| const tls = process.env.CA_PATH ? { ca: Bun.file(process.env.CA_PATH) } : undefined; | ||
| const sql = new SQL({ url, tls, max: 1 }); | ||
|
|
||
| try { | ||
| // Prime the prepared-statement cache so the next call with the same | ||
| // signature goes straight to bindAndExecute without re-preparing. | ||
| await sql.unsafe("select ? as x", [1]); | ||
| // Marker so the test harness can distinguish "couldn't connect" from | ||
| // "connected then crashed" when no docker-managed MySQL is available. | ||
| console.log("CONNECTED"); | ||
|
|
||
| const values: number[] = [1]; | ||
| let fired = 0; | ||
| Object.defineProperty(values, "0", { | ||
| enumerable: true, | ||
| configurable: true, | ||
| get() { | ||
| if (fired++ === 0) { | ||
| for (let i = 0; i < 100; i++) values.push(1); | ||
| } | ||
| return 1; | ||
| }, | ||
| }); | ||
|
|
||
| const result = await sql.unsafe("select ? as x", values).then( | ||
| rows => ({ ok: true, rows }), | ||
| err => ({ ok: false, code: err?.code, message: String(err?.message ?? err) }), | ||
| ); | ||
|
|
||
| // The connection must still be usable after the bind failure; a partial | ||
| // packet header left in the write buffer would desync the protocol here. | ||
| const after = await sql.unsafe("select ? as x", [2]); | ||
|
|
||
| console.log(JSON.stringify({ result, after })); | ||
| } finally { | ||
| await sql.close(); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,75 @@ | ||
| import { describe, expect, test } from "bun:test"; | ||
| import { bunEnv, bunExe, describeWithContainer, isDockerEnabled } from "harness"; | ||
| import path from "path"; | ||
|
|
||
| // Regression: MySQLQuery.bind() allocates `params` sized to the prepared | ||
| // statement's signature and then iterates a *fresh* iterator over the user's | ||
| // values array. If that array grew between signature generation and bind | ||
| // (e.g. via an index getter with side effects), bind() would walk off the | ||
| // end of the allocation. With the fix it rejects with | ||
| // ERR_MYSQL_WRONG_NUMBER_OF_PARAMETERS_PROVIDED and the connection stays | ||
| // usable. | ||
|
|
||
| const fixture = path.join(import.meta.dir, "sql-mysql-bind-oob.fixture.ts"); | ||
|
|
||
| async function runFixture(url: string, caPath = "") { | ||
| await using proc = Bun.spawn({ | ||
| cmd: [bunExe(), fixture], | ||
| env: { ...bunEnv, MYSQL_URL: url, CA_PATH: caPath }, | ||
| stdout: "pipe", | ||
| stderr: "pipe", | ||
| }); | ||
| const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); | ||
| return { stdout, stderr, exitCode }; | ||
| } | ||
|
|
||
| function assertFixtureOutput(stdout: string, stderr: string, exitCode: number) { | ||
| const filteredStderr = stderr | ||
| .split(/\r?\n/) | ||
| .filter(l => l && !l.startsWith("WARNING: ASAN interferes")) | ||
| .join("\n"); | ||
| expect(filteredStderr).toBe(""); | ||
| const lines = stdout.trim().split(/\r?\n/); | ||
| expect(lines[0]).toBe("CONNECTED"); | ||
| expect(JSON.parse(lines[1] ?? "null")).toEqual({ | ||
| result: { | ||
| ok: false, | ||
| code: "ERR_MYSQL_WRONG_NUMBER_OF_PARAMETERS_PROVIDED", | ||
| message: expect.any(String), | ||
| }, | ||
| after: [{ x: 2 }], | ||
| }); | ||
| expect(exitCode).toBe(0); | ||
|
claude[bot] marked this conversation as resolved.
|
||
| } | ||
|
|
||
| if (isDockerEnabled()) { | ||
| // CI: run against the docker-compose MySQL service. | ||
| describeWithContainer("mysql", { image: "mysql_plain" }, container => { | ||
| test("bind() does not OOB when the params array grows during binding", async () => { | ||
| await container.ready; | ||
| const url = `mysql://root@${container.host}:${container.port}/bun_sql_test`; | ||
| const { stdout, stderr, exitCode } = await runFixture(url); | ||
| assertFixtureOutput(stdout, stderr, exitCode); | ||
| }); | ||
| }); | ||
| } else { | ||
| // No docker daemon (e.g. local/sandboxed environments). If a MySQL server | ||
| // is reachable at MYSQL_URL or the conventional local address, exercise the | ||
| // fixture there so the regression is still covered. | ||
| const url = process.env.MYSQL_URL || "mysql://bun@127.0.0.1:3306/bun_sql_test"; | ||
|
|
||
| describe("mysql (local)", () => { | ||
| test("bind() does not OOB when the params array grows during binding", async () => { | ||
| const { stdout, stderr, exitCode } = await runFixture(url); | ||
| // The fixture prints "CONNECTED" after the priming query succeeds. If | ||
| // it never got that far, there's no MySQL to talk to in this | ||
| // environment; the docker-gated branch above (and sql-mysql.test.ts) | ||
| // provide the CI coverage. | ||
| if (!stdout.startsWith("CONNECTED")) { | ||
| console.warn("sql-mysql-bind-oob: no MySQL reachable at " + url + "; skipping assertions"); | ||
| return; | ||
| } | ||
| assertFixtureOutput(stdout, stderr, exitCode); | ||
| }, 30_000); | ||
|
robobun marked this conversation as resolved.
Outdated
|
||
| }); | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.