fix: prevent circular symlinks in projects registry (#10432)

close #10411
This commit is contained in:
Zoltan Kochan
2026-01-09 18:04:57 +01:00
committed by GitHub
parent c5d4d81f56
commit 5a0ed1d450
4 changed files with 26 additions and 2 deletions

View File

@@ -0,0 +1,6 @@
---
"@pnpm/package-store": patch
"pnpm": patch
---
Do not add a symlink to the project into the store's project registry if the store is in a subdirectory of the project [#10411](https://github.com/pnpm/pnpm/issues/10411).

View File

@@ -5,6 +5,7 @@ import { createShortHash } from '@pnpm/crypto.hash'
import { PnpmError } from '@pnpm/error'
import { globalInfo } from '@pnpm/logger'
import symlinkDir from 'symlink-dir'
import isSubdir from 'is-subdir'
const PROJECTS_DIR = 'projects'
@@ -17,6 +18,10 @@ export function getProjectsRegistryDir (storeDir: string): string {
* Creates a symlink in {storeDir}/projects/{hash} → {projectDir}
*/
export async function registerProject (storeDir: string, projectDir: string): Promise<void> {
// Avoid creating circular symlinks when the store is inside the project directory
if (isSubdir(projectDir, storeDir)) {
return
}
const registryDir = getProjectsRegistryDir(storeDir)
await fs.mkdir(registryDir, { recursive: true })
const linkPath = path.join(registryDir, createShortHash(projectDir))

View File

@@ -47,6 +47,18 @@ describe('projectRegistry', () => {
const entries = await fs.readdir(projectsDir)
expect(entries).toHaveLength(2)
})
it('does not create symlink when store is inside project directory', async () => {
const projectDir = temporaryDirectory()
const storeDir = path.join(projectDir, 'node_modules', '.pnpm-store')
await fs.mkdir(storeDir, { recursive: true })
await registerProject(storeDir, projectDir)
// The projects directory should not be created since we skipped registration
const projectsDir = path.join(storeDir, 'projects')
await expect(fs.readdir(projectsDir)).rejects.toThrow(/ENOENT/)
})
})
describe('getRegisteredProjects()', () => {

View File

@@ -428,8 +428,9 @@ describe('global virtual store prune', () => {
'is-positive': '1.0.0',
},
})
const cacheDir = path.resolve('cache')
const storeDir = path.resolve('store')
// Store should be OUTSIDE the project directory to ensure proper project registration
const cacheDir = path.resolve('..', 'cache')
const storeDir = path.resolve('..', 'store')
// Install with global virtual store enabled
await execa('node', [