fix(deps-restorer): include the loader runtime in git dependencies (#17388)
* fix(deps-restorer): include the loader runtime in git dependencies * test(esm-loader): reject stale bundles without rewriting them * fix(deps): update compression crates for advisory fixes * fix(store): retry concurrent SQLite index initialization * chore: correct release metadata and module ordering * test(store): verify concurrent index rows survive initialization * style(store): retain trailing comma in concurrency assertion
This commit is contained in:
1 parent
316ec14c4b
commit
20ec9feaee
12 files changed
+293
-36
No files matched your search
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"pacquet": patch
|
||||
"@pnpm/pnpr": patch
|
||||
---
|
||||
|
||||
Fixed memory exposure and allocation handling vulnerabilities when processing zstd-compressed data [#17388](https://github.com/pnpm/pnpm/pull/17388).
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"pacquet": patch
|
||||
---
|
||||
|
||||
Concurrent workspace installs no longer lose store-index rows when initializing the shared SQLite index [#17388](https://github.com/pnpm/pnpm/pull/17388).
|
||||
@@ -96,4 +96,3 @@ pnpm/npm/wasm/
|
||||
pnpr-bins-*
|
||||
|
||||
# Generated by the `prepare` script of pnpm/esm-loader.
|
||||
pnpm/crates/deps-restorer/src/cas-loader.mjs.inc
|
||||
Generated
+10
-10
@@ -1081,9 +1081,9 @@ checksum = "b1b4fe32ea3f2a8d975b6c5cdd3f02ac358f471ca24dbb18d7a4ca58b3193d2d"
|
||||
|
||||
[[package]]
|
||||
name = "compression-codecs"
|
||||
version = "0.4.38"
|
||||
version = "0.4.46"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce2548391e9c1929c21bf6aa2680af86fe4c1b33e6cea9ac1cfeec0bd11218cf"
|
||||
checksum = "cae36683101b2899f48bd31f8db9839437356188e3b1ff23096e73dab09ba8fe"
|
||||
dependencies = [
|
||||
"compression-core",
|
||||
"flate2",
|
||||
@@ -1094,9 +1094,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "compression-core"
|
||||
version = "0.4.32"
|
||||
version = "0.4.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cc14f565cf027a105f7a44ccf9e5b424348421a1d8952a8fc9d499d313107789"
|
||||
checksum = "2ebdda89edc534bf8d5dd2857562633b6495557e5846552ade994b99ed13c36a"
|
||||
|
||||
[[package]]
|
||||
name = "concurrent-queue"
|
||||
@@ -10960,27 +10960,27 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zstd"
|
||||
version = "0.13.3"
|
||||
version = "0.14.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a"
|
||||
checksum = "db3ae9bf90af6e40197bef767eeefc46ad6397420fe873830bd85e5fbbfb3c70"
|
||||
dependencies = [
|
||||
"zstd-safe",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zstd-safe"
|
||||
version = "7.3.0"
|
||||
version = "8.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "64d80649ab6db9d9f6f9c80a40becd948eda4714a0a5ac8c4d157a32231c7882"
|
||||
checksum = "ded520f16b29e1a9b771c722221b2b0a0398c2316d9bb3c42b458a5414d7434b"
|
||||
dependencies = [
|
||||
"zstd-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zstd-sys"
|
||||
version = "2.1.0+zstd.1.5.7"
|
||||
version = "2.1.2+zstd.1.5.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ef0a8027ec3ee71300ab3bcbcd0393f434aa72b91ca6d635a39941deae8eea0"
|
||||
checksum = "481ada9c543b2323cbb7bbdbb5249177fbf0188ed959ed2c15e4b22421c84ed3"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"pkg-config",
|
||||
|
||||
+1
-1
@@ -268,7 +268,7 @@ yamlpath = { version = "1.25.2" }
|
||||
wax = { version = "0.7.0" }
|
||||
which = { version = "8.0.2" }
|
||||
zip = { version = "8", default-features = false, features = ["deflate"] }
|
||||
zstd = { version = "0.13.3", default-features = false }
|
||||
zstd = { version = "0.14.3", default-features = false }
|
||||
zune-inflate = { version = "0.2.54" }
|
||||
|
||||
# Dev dependencies
|
||||
|
||||
File diff suppressed because one or more lines are too long.
@@ -74,26 +74,7 @@ impl StoreIndex {
|
||||
let conn = Connection::open(&db_path)
|
||||
.map_err(|source| StoreIndexError::Open { path: db_path.clone(), source })?;
|
||||
|
||||
// Busy-timeout FIRST so the internal busy handler is active during the
|
||||
// rest of the setup — on Windows file locking is mandatory and
|
||||
// concurrent pacquet / pnpm invocations can contend.
|
||||
conn.execute_batch(
|
||||
"
|
||||
PRAGMA busy_timeout=5000;
|
||||
PRAGMA journal_mode=WAL;
|
||||
PRAGMA synchronous=NORMAL;
|
||||
PRAGMA mmap_size=536870912;
|
||||
PRAGMA cache_size=-32000;
|
||||
PRAGMA temp_store=MEMORY;
|
||||
PRAGMA wal_autocheckpoint=10000;
|
||||
CREATE TABLE IF NOT EXISTS package_index (
|
||||
key TEXT PRIMARY KEY,
|
||||
data BLOB NOT NULL
|
||||
) WITHOUT ROWID;
|
||||
",
|
||||
)
|
||||
.and_then(|()| conn.execute_batch(aliases::CREATE_ALIAS_TABLE))
|
||||
.map_err(|source| StoreIndexError::InitSchema { source })?;
|
||||
schema::initialize(&conn).map_err(|source| StoreIndexError::InitSchema { source })?;
|
||||
|
||||
Ok(StoreIndex::sqlite(
|
||||
conn,
|
||||
@@ -524,6 +505,8 @@ mod writer;
|
||||
|
||||
mod queries;
|
||||
|
||||
mod schema;
|
||||
|
||||
mod aliases;
|
||||
|
||||
mod error;
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
//! Initialize a shared index even when another installer is switching it to WAL.
|
||||
use rusqlite::{Connection, ErrorCode};
|
||||
use std::time::Duration;
|
||||
|
||||
/// `SQLite` can return `SQLITE_BUSY` from a journal-mode transition without
|
||||
/// invoking its busy handler. Retry the idempotent setup within the same
|
||||
/// pnpm v11 retry policy: up to 100 retries, 50 milliseconds apart.
|
||||
pub(super) fn initialize(connection: &Connection) -> rusqlite::Result<()> {
|
||||
connection.busy_timeout(Duration::from_secs(5))?;
|
||||
let mut retries = 0;
|
||||
loop {
|
||||
let result = connection
|
||||
.execute_batch(
|
||||
"PRAGMA journal_mode=WAL;
|
||||
PRAGMA synchronous=NORMAL;
|
||||
PRAGMA mmap_size=536870912;
|
||||
PRAGMA cache_size=-32000;
|
||||
PRAGMA temp_store=MEMORY;
|
||||
PRAGMA wal_autocheckpoint=10000;
|
||||
CREATE TABLE IF NOT EXISTS package_index (
|
||||
key TEXT PRIMARY KEY,
|
||||
data BLOB NOT NULL
|
||||
) WITHOUT ROWID;",
|
||||
)
|
||||
.and_then(|()| connection.execute_batch(super::aliases::CREATE_ALIAS_TABLE));
|
||||
match result {
|
||||
Err(error)
|
||||
if error.sqlite_error_code() == Some(ErrorCode::DatabaseBusy) && retries < 100 =>
|
||||
{
|
||||
retries += 1;
|
||||
std::thread::sleep(Duration::from_millis(50));
|
||||
}
|
||||
result => return result,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
@@ -0,0 +1,41 @@
|
||||
use crate::{StoreIndex, store_index::tests::sample_index};
|
||||
use std::{path::Path, sync::Barrier};
|
||||
|
||||
#[test]
|
||||
fn concurrent_first_open_persists_every_writer_row() {
|
||||
for _ in 0..16 {
|
||||
let store = tempfile::tempdir().unwrap();
|
||||
open_concurrently(store.path());
|
||||
}
|
||||
}
|
||||
|
||||
fn open_concurrently(store: &Path) {
|
||||
let start = Barrier::new(8);
|
||||
std::thread::scope(|scope| {
|
||||
let writers = (0..8)
|
||||
.map(|worker| {
|
||||
let start = &start;
|
||||
scope.spawn(move || {
|
||||
start.wait();
|
||||
let index =
|
||||
StoreIndex::open(store).expect("initialize a concurrent store writer");
|
||||
index
|
||||
.set(&format!("fixture-{worker}"), &sample_index())
|
||||
.unwrap();
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
for writer in writers {
|
||||
writer.join().unwrap();
|
||||
}
|
||||
});
|
||||
let index = StoreIndex::open_readonly(store).unwrap();
|
||||
for worker in 0..8 {
|
||||
assert_eq!(
|
||||
index
|
||||
.get(&format!("fixture-{worker}"))
|
||||
.unwrap(),
|
||||
Some(sample_index()),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,8 @@
|
||||
"node": "^24.18.0 || >=26.2.0"
|
||||
},
|
||||
"scripts": {
|
||||
"prepare": "node scripts/bundle-runtime.mjs",
|
||||
"prepare": "node scripts/bundle-runtime.mjs --check",
|
||||
"build:runtime": "node scripts/bundle-runtime.mjs",
|
||||
"test": "node --test test/*.test.mjs",
|
||||
".test": "node --test test/*.test.mjs"
|
||||
},
|
||||
|
||||
@@ -16,6 +16,12 @@ const result = await esbuild.build({
|
||||
})
|
||||
|
||||
const output = result.outputFiles[0]
|
||||
const tempPath = `${output.path}.${process.pid}.tmp`
|
||||
fs.writeFileSync(tempPath, output.contents)
|
||||
fs.renameSync(tempPath, output.path)
|
||||
if (process.argv.includes('--check')) {
|
||||
if (!fs.readFileSync(output.path).equals(output.contents)) {
|
||||
throw new Error('The embedded loader is stale. Run pnpm --filter @pnpm/esm-loader build:runtime and commit the generated file.')
|
||||
}
|
||||
} else {
|
||||
const tempPath = `${output.path}.${process.pid}.tmp`
|
||||
fs.writeFileSync(tempPath, output.contents)
|
||||
fs.renameSync(tempPath, output.path)
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { execFileSync, spawnSync } from 'node:child_process'
|
||||
import fs from 'node:fs'
|
||||
import os from 'node:os'
|
||||
import path from 'node:path'
|
||||
import process from 'node:process'
|
||||
import { test } from 'node:test'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const loader = fileURLToPath(new URL('../', import.meta.url))
|
||||
|
||||
test('the committed loader matches the runtime sources', () => {
|
||||
execFileSync(process.execPath, [path.join(loader, 'scripts/bundle-runtime.mjs'), '--check'], { stdio: 'inherit' })
|
||||
})
|
||||
|
||||
test('check rejects a stale bundle without rewriting it', () => {
|
||||
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'pnpm-loader-check-'))
|
||||
try {
|
||||
const isolatedLoader = path.join(directory, 'esm-loader')
|
||||
fs.cpSync(loader, isolatedLoader, {
|
||||
recursive: true,
|
||||
filter: source => !['node_modules', 'test'].includes(path.basename(source)),
|
||||
})
|
||||
fs.symlinkSync(fs.realpathSync(path.join(loader, 'node_modules')), path.join(isolatedLoader, 'node_modules'), 'junction')
|
||||
const output = path.join(directory, 'crates/deps-restorer/src/cas-loader.mjs.inc')
|
||||
fs.mkdirSync(path.dirname(output), { recursive: true })
|
||||
const stale = '// deliberately stale runtime\n'
|
||||
fs.writeFileSync(output, stale)
|
||||
const result = spawnSync(process.execPath, [path.join(isolatedLoader, 'scripts/bundle-runtime.mjs'), '--check'], { encoding: 'utf8' })
|
||||
assert.ifError(result.error)
|
||||
assert.equal(result.status, 1, result.stderr)
|
||||
assert.match(result.stderr, /The embedded loader is stale/)
|
||||
assert.equal(fs.readFileSync(output, 'utf8'), stale)
|
||||
} finally {
|
||||
fs.rmSync(directory, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
Reference in new issue
Block a user