vitest-dev/vitest · error · Error

Benchmark artifact path

Error message

Benchmark artifact path "${relativePath}" resolves outside the project root (${root}). Paths passed to `writeResult` and `bench.from()` must point inside the project.

What it means

A path-traversal guard inside `BenchmarkManager.resolve()`. Before reading or writing a benchmark baseline artifact via `bench.from()`/`writeResult`, the manager resolves the user-supplied path against the project `root` and rejects any result that does not equal or sit under `root/`. This prevents a benchmark file from reading or clobbering files outside the workspace.

Solutions

  1. Keep baseline artifacts inside the project tree, e.g. `bench.from('./.bench/baseline.json')`.
  2. If you need a shared baseline location, set Vitest `root` (or `bench` output dir) to that location so the path resolves inside it.
  3. Replace backslashes and `..` segments; verify with `path.resolve(root, relative).startsWith(root + path.sep)` before calling.
  4. For symlinks, resolve real paths and ensure the target is within root before passing to `bench.from()`.

Example fix

// before — escapes project root
bench.from('/shared/baselines/core.json')

// after — keep inside the project
bench.from('./baselines/core.json')
// or set root to the shared dir when starting Vitest: vitest --root /shared/baselines
Defensive patterns

Strategy: validation

Validate before calling

import { isAbsolute, resolve, relative } from 'node:path'

function assertWithinRoot(root: string, p: string): string {
  const abs = isAbsolute(p) ? resolve(p) : resolve(root, p)
  const rel = relative(root, abs)
  if (rel.startsWith('..') || isAbsolute(rel)) {
    throw new Error(`Refusing to read path outside root: ${p}`)
  }
  return abs
}

const safe = assertWithinRoot(project.config.root, baselinePath)
bench.from(safe)

Type guard

function isWithinRoot(root: string, p: string): boolean {
  const abs = resolve(root, p)
  const rootWithSep = root.endsWith('/') ? root : root + '/'
  return abs === root || abs.startsWith(rootWithSep)
}

Prevention

When it happens

Trigger: Calling `bench.from('../../../etc/passwd')`, `bench.from('/tmp/baseline.json')` (absolute path outside root), `bench.from('..\..\secrets\key')` on Windows, or passing a symlink-laden path that `pathe/resolve` normalizes outside the project root. Affects both `readResult` and `writeResult` paths.

Common situations: Monorepo where the benchmark file is in `packages/a/` but `root` was resolved to a parent workspace and the relative path escapes; CI where `root` is `/repo` but baseline stored in a shared `/baselines` volume; accidentally passing an absolute cache path to `bench.from()`.

Related errors


AI-assisted analysis of vitest-dev/vitest@1fa9837ec2 (2026-08-11). Data as JSON: /api/errors/ee6f9da4f6595794. Report an issue: GitHub.

Appendix: source

Thrown at packages/vitest/src/node/benchmark.ts:21

import { existsSync } from 'node:fs'
import { mkdir, readFile, writeFile } from 'node:fs/promises'
import { dirname, isAbsolute, resolve } from 'pathe'

export class BenchmarkManager {
  constructor(private project: TestProject) {}

  // Resolve a user-supplied path against the project root. Reject paths that
  // escape the project root: `bench.from()` accepts arbitrary input, and we
  // never want a benchmark file to be able to read or clobber files outside
  // the workspace.
  public resolve(relativePath: string): string {
    const root = this.project.config.root
    const absolute = isAbsolute(relativePath)
      ? resolve(relativePath)
      : resolve(root, relativePath)
    const rootWithSep = root.endsWith('/') ? root : `${root}/`
    if (absolute !== root && !absolute.startsWith(rootWithSep)) {
      throw new Error(
        `Benchmark artifact path "${relativePath}" resolves outside the project root (${root}). `
        + `Paths passed to \`writeResult\` and \`bench.from()\` must point inside the project.`,
      )
    }
    return absolute
  }

  async readResult(relativePath: string): Promise<BaselineData | null> {
    const path = this.resolve(relativePath)
    if (!existsSync(path)) {
      return null
    }
    return JSON.parse(await readFile(path, 'utf-8')) as BaselineData
  }

  async writeResult(relativePath: string, data: BaselineData): Promise<void> {
    const absolute = this.resolve(relativePath)
    await mkdir(dirname(absolute), { recursive: true })

View on GitHub (pinned to 1fa9837ec2)