diff --git a/docs/generated/manifests/menus.json b/docs/generated/manifests/menus.json index 4d33016812..8dd3c2713a 100644 --- a/docs/generated/manifests/menus.json +++ b/docs/generated/manifests/menus.json @@ -3291,6 +3291,14 @@ "isExternal": false, "children": [], "disableCollapsible": false + }, + { + "name": "Unknown Local Cache Error", + "path": "/recipes/other/unknown-local-cache", + "id": "unknown-local-cache", + "isExternal": false, + "children": [], + "disableCollapsible": false } ], "disableCollapsible": false @@ -3494,6 +3502,14 @@ "isExternal": false, "children": [], "disableCollapsible": false + }, + { + "name": "Unknown Local Cache Error", + "path": "/recipes/other/unknown-local-cache", + "id": "unknown-local-cache", + "isExternal": false, + "children": [], + "disableCollapsible": false } ] }, diff --git a/docs/generated/manifests/recipes.json b/docs/generated/manifests/recipes.json index dee91d8957..ea67ae2fe2 100644 --- a/docs/generated/manifests/recipes.json +++ b/docs/generated/manifests/recipes.json @@ -1597,6 +1597,16 @@ "isExternal": false, "path": "/recipes/other/standalone-ngrx-apis", "tags": [] + }, + { + "id": "unknown-local-cache", + "name": "Unknown Local Cache Error", + "description": "", + "file": "shared/guides/unknown-local-cache", + "itemList": [], + "isExternal": false, + "path": "/recipes/other/unknown-local-cache", + "tags": [] } ], "isExternal": false, @@ -1852,5 +1862,15 @@ "isExternal": false, "path": "/recipes/other/standalone-ngrx-apis", "tags": [] + }, + "/recipes/other/unknown-local-cache": { + "id": "unknown-local-cache", + "name": "Unknown Local Cache Error", + "description": "", + "file": "shared/guides/unknown-local-cache", + "itemList": [], + "isExternal": false, + "path": "/recipes/other/unknown-local-cache", + "tags": [] } } diff --git a/docs/map.json b/docs/map.json index de3bc1853a..d3fbcc151e 100644 --- a/docs/map.json +++ b/docs/map.json @@ -1458,6 +1458,12 @@ "id": "standalone-ngrx-apis", "tags": [], "file": "shared/recipes/standalone-ngrx-apis" + }, + { + "name": "Unknown Local Cache Error", + "id": "unknown-local-cache", + "tags": [], + "file": "shared/guides/unknown-local-cache" } ] } diff --git a/docs/shared/guides/unknown-local-cache.md b/docs/shared/guides/unknown-local-cache.md new file mode 100644 index 0000000000..2fb83f78d3 --- /dev/null +++ b/docs/shared/guides/unknown-local-cache.md @@ -0,0 +1,86 @@ +# Unknown Local Cache Error + +This document will explain why the following error happens and how to address it. + +``` +NX Invalid Cache Directory for Task "myapp:build" + +The local cache artifact in "node_modules/.cache/nx/786524780459028195" was not been generated on this machine. +As a result, the cache's content integrity cannot be confirmed, which may make cache restoration potentially unsafe. +If your machine ID has changed since the artifact was cached, run "nx reset" to fix this issue. +Read about the error and how to address it here: https://nx.dev/recipes/other/unknown-local-cache +``` + +## Nx Tracks Cache Source + +Nx can cache tasks, which can drastically speed up your CI and local builds. However, this comes with the potential risk +of "cache poisoning", where cache artifacts could be intentionally or inadvertently overwritten. If another user +executes a task that matches the hash of the tainted artifact, they could retrieve the corrupted artifact and use it as +the outcome of the task. Nx and Nx Cloud contain several safeguards to minimize the likelihood of cache poisoning or, in +the case of Nx Cloud, completely prevent it. + +The error above is one such safeguard. + +Nx trusts the local cache. If you executed a task and stored the corresponding cached artifact on your machine, you can +safely restore it on the same machine without worrying about cache poisoning. After all, in order to tamper with the +cache artifact, the actor would need access to the machine itself. + +However, when artifacts in the local cache are created by a different machine, we cannot make such assumption. By +default, Nx will refuse to use such artifacts and will throw the "Invalid Cache Directory" error. + +## Your MachineId Has Changed + +Upgrading your computer's hardware may alter its Machine ID, yielding the error above. To fix it execute `nx reset` to +remove all the cache directories created under the previous Machine ID. After doing so, you should no longer see the +error. After doing so, you should no longer see the error. + +## You Share Cache with Another Machine Using a Network Drive + +You can prefix any Nx command with `NX_REJECT_UNKNOWN_LOCAL_CACHE=0` to ignore the error ( +e.g., `NX REJECT_UNKNOWN_LOCAL_CACHE=0 nx run-many -t build test`). This is similar to +setting `NODE_TLS_REJECT_UNAUTHORIZED=0` to ignore any errors stemming form self-signed certificates. Even though it +will make it work, this approach is discouraged. + +Storing Nx's local cache on a network drive can present security risks. When a network drive is shared, every CI run has +access to all the previously created Nx cache artifacts. Hence, it is plausible for every single artifact - for every +single task hash - to be accessed without leaving any trace. This is feasible due to the network drive's capability to +allow overwrites. + +Instead of sharing the network drive, we highly recommend you to implement the `RemoteCache` interface. + +## Implementing Remote Cache Interface + +This is the interface: + +```typescript +interface RemoteCache { + retrieve(hash: string, cachePath: string); + + store(hash: string, cachePath: string); +} +``` + +> You will need to wrap the default tasks runner to provide the remote cache implementation. + +## How Nx Cloud Makes Sure Sharing Cache is Safe + +The Nx Cloud runner provides an implementation of `RemoteCache` which does the following things making sharing the cache safe: + +1. **Immutable Artifacts:** Nx Cloud allows you to create and store new artifacts without the ability to override the + existing ones. This prevents any possibility of poisoning an existing artifact. This is achieved by managing the + cache using short-lived signed URLs. + +2. **Artifact Accessibility:** Nx Cloud provides access to the cache artifact specifically for the task that is + currently being executed. It restricts the ability to list all cache artifacts. + +3. **Visibility Control:** Nx Cloud comes with options to manage the visibility of your cache artifacts. For instance, + the cache artifacts created in `main` might be accessible by anyone across any branch, whereas the artifacts created + in your PR could be shared only within your PR runs. + +4. **Access Token Traceability:** Nx Cloud keeps a record of the access token used to create a cache artifact. In case + an access token gets compromised it can be easily removed, in turn deleting all the cache artifacts that were created + using it. + +Nx Cloud is not the only remote cache you can use. If you are using a different remote cache or using your +own implementation, we would highly recommend ensuring that the same safety mechanisms as Nx Cloud have been put in +place. diff --git a/docs/shared/reference/sitemap.md b/docs/shared/reference/sitemap.md index 895f7a1b69..1982d9287a 100644 --- a/docs/shared/reference/sitemap.md +++ b/docs/shared/reference/sitemap.md @@ -219,6 +219,7 @@ - [Identify Dependencies Between Folders](/recipes/other/identify-dependencies-between-folders) - [Rescope Packages from @nrwl to @nx](/recipes/other/rescope) - [Standalone NgRx APIs](/recipes/other/standalone-ngrx-apis) + - [Unknown Local Cache Error](/recipes/other/unknown-local-cache) - Plugins diff --git a/package.json b/package.json index 72295477ea..8185fee050 100644 --- a/package.json +++ b/package.json @@ -330,7 +330,8 @@ "tailwindcss": "3.2.4", "tslib": "^2.3.0", "vitest": "^0.32.0", - "weak-napi": "^2.0.2" + "weak-napi": "^2.0.2", + "node-machine-id": "1.1.12" }, "resolutions": { "minimist": "^1.2.6", diff --git a/packages/nx/package.json b/packages/nx/package.json index fc40c07e5c..33ade2fbc4 100644 --- a/packages/nx/package.json +++ b/packages/nx/package.json @@ -64,7 +64,8 @@ "tslib": "^2.3.0", "v8-compile-cache": "2.3.0", "yargs": "^17.6.2", - "yargs-parser": "21.1.1" + "yargs-parser": "21.1.1", + "node-machine-id": "1.1.12" }, "peerDependencies": { "@swc-node/register": "^1.4.2", diff --git a/packages/nx/src/tasks-runner/cache.ts b/packages/nx/src/tasks-runner/cache.ts index 39e05d4c86..16d22c1e79 100644 --- a/packages/nx/src/tasks-runner/cache.ts +++ b/packages/nx/src/tasks-runner/cache.ts @@ -6,6 +6,7 @@ import { DefaultTasksRunnerOptions } from './default-tasks-runner'; import { spawn } from 'child_process'; import { cacheDir } from '../utils/cache-directory'; import { Task } from '../config/task-graph'; +import { machineId } from 'node-machine-id'; export type CachedResult = { terminalOutput: string; @@ -20,6 +21,8 @@ export class Cache { cachePath = this.createCacheDir(); terminalOutputsDir = this.createTerminalOutputsDir(); + private _currentMachineId: string = null; + constructor(private readonly options: DefaultTasksRunnerOptions) {} removeOldCacheRecords() { @@ -44,6 +47,20 @@ export class Cache { } } + async currentMachineId() { + if (!this._currentMachineId) { + try { + this._currentMachineId = await machineId(); + } catch (e) { + if (process.env.NX_VERBOSE_LOGGING == 'true') { + console.log(`Unable to get machineId. Error: ${e.message}`); + } + this._currentMachineId = ''; + } + } + return this._currentMachineId; + } + async get(task: Task): Promise { const res = await this.getFromLocalDir(task); @@ -98,6 +115,7 @@ export class Cache { // so if the process gets terminated while we are copying stuff into cache, // the cache entry won't be used. await writeFile(join(td, 'code'), code.toString()); + await writeFile(join(td, 'source'), await this.currentMachineId()); await writeFile(tdCommit, 'true'); if (this.options.remoteCache) { @@ -208,6 +226,32 @@ export class Cache { try { code = Number(await readFile(join(td, 'code'), 'utf-8')); } catch {} + + let sourceMachineId = null; + try { + sourceMachineId = await readFile(join(td, 'source'), 'utf-8'); + } catch {} + + if ( + sourceMachineId && + sourceMachineId != (await this.currentMachineId()) + ) { + if ( + process.env.NX_REJECT_UNKNOWN_LOCAL_CACHE != '0' && + process.env.NX_REJECT_UNKNOWN_LOCAL_CACHE != 'false' + ) { + const error = [ + `Invalid Cache Directory for Task "${task.id}"`, + `The local cache artifact in "${td}" was not been generated on this machine.`, + `As a result, the cache's content integrity cannot be confirmed, which may make cache restoration potentially unsafe.`, + `If your machine ID has changed since the artifact was cached, run "nx reset" to fix this issue.`, + `Read about the error and how to address it here: https://nx.dev/recipes/other/unknown-local-cache`, + ``, + ].join('\n'); + throw new Error(error); + } + } + return { terminalOutput, outputsPath: join(td, 'outputs'), diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 09a6392aee..b323b4f4ca 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -101,6 +101,9 @@ dependencies: next-seo: specifier: ^5.13.0 version: 5.13.0(next@13.3.4)(react-dom@18.2.0)(react@18.2.0) + node-machine-id: + specifier: 1.1.12 + version: 1.1.12 npm-run-path: specifier: ^4.0.1 version: 4.0.1 @@ -20359,7 +20362,6 @@ packages: /node-machine-id@1.1.12: resolution: {integrity: sha512-QNABxbrPa3qEIfrE6GOJ7BYIuignnJw7iQ2YPbc3Nla1HzRJjXzZOiikfF8m7eAMfichLt3M4VgLOetqgDmgGQ==} - dev: true /node-releases@2.0.10: resolution: {integrity: sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==}