6d0986af fix(core): refuse a git config that is not ours before reading it
Builds on the previous commit's checks rather than replacing them. Shape
tells us a directory is a repository; it does not tell us whose. A real
repository owned by another user has `HEAD` and `objects` too, and the walk
goes up past the workspace into directories the caller may not own -- `/tmp`
is mode 1777, and the workspace need only not be a git repository itself.
Git refuses a foreign-owned repository for exactly this reason
(`safe.directory`, CVE-2022-24765), so deferring to git lands on the same
refusal rather than a different answer.
- `.git` directories go through `isOwnedRealDirectory`, which also `lstat`s,
so a symlink standing in for `.git` is refused.
- The linked-worktree branch gets the same treatment. `gitdir:` and
`commondir` are paths read out of file contents, so that branch could be
pointed at any directory; it was returning one unchecked.
- Config, the `.git` pointer and `commondir` are read through one helper
that opens with `O_NOFOLLOW | O_NONBLOCK` and takes the file type and the
owner from `fstat` on the descriptor it then reads. `O_NOFOLLOW` keeps the
symlink refusal the previous commit's `lstat` provided, `O_NONBLOCK` keeps
a FIFO from blocking the open, and reading through the descriptor closes
the window between the check and the read that an `lstat`-then-open leaves.
Also drops the `NX_CACHE_DIRECTORY` guard in `migrate-ui-api.spec.ts`. The
`vi.clearAllMocks()` added alongside it is sufficient on its own -- measured
by removing each in turn -- and the env guard coupled the spec to the order
of the guards inside `computeSharedDataLocation`. 6d0986af fix(core): refuse a git config that is not ours before reading it
Builds on the previous commit's checks rather than replacing them. Shape
tells us a directory is a repository; it does not tell us whose. A real
repository owned by another user has `HEAD` and `objects` too, and the walk
goes up past the workspace into directories the caller may not own -- `/tmp`
is mode 1777, and the workspace need only not be a git repository itself.
Git refuses a foreign-owned repository for exactly this reason
(`safe.directory`, CVE-2022-24765), so deferring to git lands on the same
refusal rather than a different answer.
- `.git` directories go through `isOwnedRealDirectory`, which also `lstat`s,
so a symlink standing in for `.git` is refused.
- The linked-worktree branch gets the same treatment. `gitdir:` and
`commondir` are paths read out of file contents, so that branch could be
pointed at any directory; it was returning one unchecked.
- Config, the `.git` pointer and `commondir` are read through one helper
that opens with `O_NOFOLLOW | O_NONBLOCK` and takes the file type and the
owner from `fstat` on the descriptor it then reads. `O_NOFOLLOW` keeps the
symlink refusal the previous commit's `lstat` provided, `O_NONBLOCK` keeps
a FIFO from blocking the open, and reading through the descriptor closes
the window between the check and the read that an `lstat`-then-open leaves.
Also drops the `NX_CACHE_DIRECTORY` guard in `migrate-ui-api.spec.ts`. The
`vi.clearAllMocks()` added alongside it is sufficient on its own -- measured
by removing each in turn -- and the env guard coupled the spec to the order
of the guards inside `computeSharedDataLocation`. 6d0986af fix(core): refuse a git config that is not ours before reading it
Builds on the previous commit's checks rather than replacing them. Shape
tells us a directory is a repository; it does not tell us whose. A real
repository owned by another user has `HEAD` and `objects` too, and the walk
goes up past the workspace into directories the caller may not own -- `/tmp`
is mode 1777, and the workspace need only not be a git repository itself.
Git refuses a foreign-owned repository for exactly this reason
(`safe.directory`, CVE-2022-24765), so deferring to git lands on the same
refusal rather than a different answer.
- `.git` directories go through `isOwnedRealDirectory`, which also `lstat`s,
so a symlink standing in for `.git` is refused.
- The linked-worktree branch gets the same treatment. `gitdir:` and
`commondir` are paths read out of file contents, so that branch could be
pointed at any directory; it was returning one unchecked.
- Config, the `.git` pointer and `commondir` are read through one helper
that opens with `O_NOFOLLOW | O_NONBLOCK` and takes the file type and the
owner from `fstat` on the descriptor it then reads. `O_NOFOLLOW` keeps the
symlink refusal the previous commit's `lstat` provided, `O_NONBLOCK` keeps
a FIFO from blocking the open, and reading through the descriptor closes
the window between the check and the read that an `lstat`-then-open leaves.
Also drops the `NX_CACHE_DIRECTORY` guard in `migrate-ui-api.spec.ts`. The
`vi.clearAllMocks()` added alongside it is sufficient on its own -- measured
by removing each in turn -- and the env guard coupled the spec to the order
of the guards inside `computeSharedDataLocation`. 4d37c4d5 fix(core): defer to git wherever the config parser cannot match it
The `.git/config` fast path answered confidently in four cases where git
answers differently, and its result feeds both the shared cache directory
and the repository name sent to Nx Cloud onboarding.
Git ends a value at an unquoted `#`/`;` and honours `\` escapes, so
`url = git@github.com:acme/app.git # mirror` parsed as the slug
`acme/app.git # mirror`. It also resolves `[url] insteadOf` before
reporting a remote, so a cross-host rewrite reported the mirror rather
than the real host, which silently disabled the GitHub checks in
`performance-report` and `connect-to-nx-cloud`. Both now hand the file to
git, matching the existing `include`/`includeIf` behaviour.
`locateGitDir` accepted any directory named `.git`, where git requires a
repository: a `.git` holding only a config file, planted in a writable
ancestor such as `/tmp`, decided the identity of every workspace beneath
it. It now checks for `HEAD` and `objects` first.
Reading the config blind also followed a symlink out of the repository,
and would have blocked forever on a FIFO -- this resolves at module scope
of `cache-directory.ts`, so every command in that workspace would hang
before printing anything. An `lstat` for a regular file covers both.
Every case degrades to spawning git rather than to a wrong answer. 065f7cf4 perf(core): read the git remote from .git/config instead of spawning git
`cacheDir` resolves at module scope and derives the workspace identity,
so every Nx process paid two subprocesses on its import path -- one CLI
run, one per isolated plugin worker, and again under NX_DAEMON=false.
Measured on this repo: `git remote -v` 23.09 ms (it spawns a shell as
well as git), `git rev-parse --show-toplevel` 16.09 ms.
Locating `.git` by walking up answers both questions at once, so neither
subprocess is needed on the happy path. Measured after: 0.390 ms per
derivation, and byte-identical with git removed from PATH, which is what
proves nothing is spawned.
- locateGitDir() handles the linked worktree and submodule case, where
`.git` is a FILE holding `gitdir:` and the remotes live in the shared
common dir that `commondir` names.
- The config parser returns null on include/includeIf rather than
answering from a partial view, so git decides those.
- Both paths fall back to git: config read, then `git remote -v`, then
the first-commit SHA. A failed parse must not skip to that last one,
which is an unbounded history walk.
getGitRootPath() no longer shells out either, so it now reports the root
as the caller referred to it rather than the realpath. Both its callers
are better for it on macOS, where mkdtemp and process.cwd() give
/var/folders while git reports /private/var/folders:
- deriveRepoKey compared the two and never produced '' for a repo root,
so the repo key embedded a traversal segment. Four workspace-id specs
were red on every macOS machine and green on Linux CI. The shared
directory name changes once for a symlink-crossing workspace -- one
cold cache.
- GitRepository.root feeds `relative(destinationGitClient.root,
absDestination)` in nx import, where absDestination is
join(process.cwd(), destination). Mixing a realpathed root with an
unresolved destination produced the same traversal garbage; both
operands now come from the same namespace.
The migrate-ui-api spec mocks fs wholesale, so the config fast path
cannot read there and it falls back to spawning git, which its
no-subprocess assertion would attribute to undoMigration. Settling the
cache location up front keeps that assertion blunt rather than narrowing
it.