Skip to content

fix: prevent findUp infinite loop when given a relative dir - #17743

Open
JoshConley wants to merge 1 commit into
payloadcms:mainfrom
JoshConley:fix/find-up-relative-dir-infinite-loop
Open

fix: prevent findUp infinite loop when given a relative dir#17743
JoshConley wants to merge 1 commit into
payloadcms:mainfrom
JoshConley:fix/find-up-relative-dir-infinite-loop

Conversation

@JoshConley

@JoshConley JoshConley commented Aug 10, 2026

Copy link
Copy Markdown

What?

When dir is a relative path, findUpSync() and findUp() loop forever at 100% CPU instead of returning null.

This is a follow-up to #12457, which fixed the same infinite loop for absolute paths. That fix added a dir !== root guard:

if (!found && dir !== root) {
  dir = path.dirname(dir) // Move up one directory level.
  continue
}

root comes from path.parse(dir).root, which is '' for any relative path. path.dirname() bottoms out at '.', and '.' is never equal to '', so neither that guard nor the if (dir === root) return null below it can ever fire:

dir './'  → access 'payload.config.ts' → ENOENT → dirname → '.'
dir '.'   → access 'payload.config.ts' → ENOENT → dirname → '.'   ← forever

Why?

findConfig() in packages/payload/src/config/find.ts passes compilerOptions.rootDir to findUpSync() without resolving it first:

const srcPath = tsConfig.compilerOptions?.rootDir || path.resolve(process.cwd(), 'src')

A relative rootDir in tsconfig.json is very common, for example "rootDir": "./" or "rootDir": "./src". Any payload bin command hangs if the nearest tsconfig sets one. In a monorepo this is easy to hit by accident, for example by running payload generate:types from the wrong workspace. In this case, the process hangs until noticed.

Note that this isn't the same as #15553. There, generate:types finishes and writes its output but fails to exit but here findConfig() never returns, so nothing is written at all.

How?

Resolve dir to an absolute path at the start of both functions, which makes the existing root checks reachable. After the fix the same call returns null right away, and callers report the intended error (e.g. "cannot find Payload config. Please create a configuration file...").

Reproduction

No app or install is needed. Against stock payload@3.87.1:

curl -s -O https://unpkg.com/payload@3.87.1/dist/utilities/findUp.js

cat > driver.mjs <<'EOF'
import { findUpSync } from './findUp.js'
const dir = process.argv[2]
console.log(`calling findUpSync({ dir: ${JSON.stringify(dir)} })`)
console.log('returned:', findUpSync({ dir, fileNames: ['payload.config.ts'] }))
EOF

node driver.mjs './'      # hangs at 100% CPU, never returns
node driver.mjs './src'   # hangs at 100% CPU, never returns
node driver.mjs "$PWD"    # returns null immediately (correct)

It also reproduces end to end, in a package whose tsconfig.json sets "rootDir": "./":

node ./node_modules/payload/bin.js generate:types   # hangs at 100% CPU, never returns

Environment Info

  • Payload: 3.84.1. Also verified on 3.87.1, where the code is unchanged.
  • Node.js: 24.13.0
  • OS: macOS 15. The affected code path has no OS-specific behaviour.

Test

Adds packages/payload/src/utilities/findUp.spec.ts, co-located to match formatLabels.spec.ts and isURLAllowed.spec.ts. It covers both functions with three cases: the relative dir with no match that used to hang, the absolute dir with no match that #12457 already fixed, and a positive case checking that a relative dir still finds a match in an ancestor.

pnpm test:unit passes with 5 tests.

Note for review: The old behaviour is a synchronous infinite loop, which blocks the vitest worker thread, so no test timeout can fire. A regression hangs the run instead of failing an assertion.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant