Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions changelog.d/workspace-nested-mount-roots.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
- A mount nested in another is now that mount's directory wherever disk puts it, not only where the outer mount's paths name it as written. A root configured through a symlink, or beneath one, is nested where its real path lies; before, it wasn't nested at all, and the outer mount took in and read the inner mount's files. Each start of the module reads each root's real path again, awaited, so a root on a share that stops answering holds that start, not the process; before the first start, mounts are nested by their configured paths alone, as they were. On a mount that follows symlinks, a path that reaches a nested mount's directory through a link is now refused as one written into it: the file tools answer `"work/l/x.txt" leads into mount "inner", nested in mount "work", which neither reads nor writes it: use "inner/sub/x.txt".`, a sync or a materialize leaves it with `"work/l" leads into mount "inner", nested in mount "work": use "inner/sub" in its place`, and `readFileFromDisk` refuses it with the code `other_mount`. Before, the outer mount read, took in, wrote and unlinked the inner mount's files through such a link (#530). On a mount that doesn't follow symlinks, a path through a link into a nested mount's directory is now refused before anything is stored, with the refusal of the link the mount doesn't follow; a `write`, `edit` or `delete` through it used to store a change that no push could write. Where the path is itself a link to a file in a nested mount's directory, `readFileFromDisk` now refuses it with the code `other_mount` and words naming that mount, where it answered `symlink`.
- A mount configured with a relative root written with `./`, such as `./m`, now hears its watcher; it heard no change on disk.
25 changes: 18 additions & 7 deletions src/modules/workspace/effects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@
* Node has no openat, so a create, mkdir or unlink still resolves its path by
* name after the parent was checked: a parent replaced concurrently in that
* window can redirect that one operation, leaving an empty file or directory
* outside the mount or unlinking a same-named file there. Nor is there an
* outside the mount or in a nested mount's directory, or unlinking a
* same-named file there. Nor is there an
* unlink bound to an inode: an unlink checks the entry's identity again just
* before it, and a file replaced at its path after that check is the one
* unlinked. Content writes stay bound to the descriptor verified before
Expand Down Expand Up @@ -53,7 +54,9 @@ import {
errno,
checkDir,
checkRoot,
leadsInto,
nameOf,
nestedAtReal,
parentOf,
} from './observe.js';

Expand Down Expand Up @@ -273,7 +276,7 @@ export async function writeContained(
let stage = 'write the file';
try {
const created = touched;
await checkOpened(view, rootReal, root, parent, target, handle, expect, noFollow, touched);
await checkOpened(view, rootReal, rel, root, parent, target, handle, expect, noFollow, touched);
touched = true;
// The mode the bytes are written at, and the one the file ends with.
let before: number | undefined;
Expand Down Expand Up @@ -369,14 +372,16 @@ export async function writeContained(
* Before an effect on an opened file: a regular file, and — unless forced —
* still holding what was decided on. The hash reads the whole file, so it
* comes first, and the checks of where the file is follow it: canonically
* inside the mount, the one this path names (and not a symlink, where the
* mount follows none and the open couldn't refuse one), with no other names,
* in the directory still at its path beneath the root still at its own; and
* nothing wrote it while those ran.
* inside the mount and in no nested mount's directory (nestedAtReal), the
* one this path names (and not a symlink, where the mount follows none and
* the open couldn't refuse one), with no other names, in the directory still
* at its path beneath the root still at its own; and nothing wrote it while
* those ran.
*/
async function checkOpened(
view: MountView,
rootReal: string,
rel: string,
root: CheckedDir,
parent: CheckedDir,
target: string,
Expand All @@ -391,6 +396,8 @@ async function checkOpened(
const real = await realpath(target).catch(() => null);
if (real === null) throw new EffectFailed(CHANGED, touched);
if (!contained(rootReal, real)) throw new EffectFailed(view.followSymlinks ? 'a symlink that leaves the mount' : OUTSIDE_PARENT, touched);
const holder = nestedAtReal(real, view.nested);
if (holder) throw new EffectFailed(leadsInto(view.name, rel, holder), touched);
const named = await stat(real, { bigint: true }).catch(() => null);
if (named === null || named.dev !== info.dev || named.ino !== info.ino) throw new EffectFailed(CHANGED, touched);
if (!view.followSymlinks) {
Expand Down Expand Up @@ -459,7 +466,7 @@ export async function chmodContained(
let touched = false;
let stage = 'set the file\'s mode';
try {
await checkOpened(view, rootReal, root, parent, target, handle, expect, noFollow, false);
await checkOpened(view, rootReal, rel, root, parent, target, handle, expect, noFollow, false);
const current = Number((await handle.stat({ bigint: true })).mode & 0o777n);
if (from !== undefined && current !== from) throw new EffectFailed(MODE_CHANGED, false);
const narrowing = (current & ~mode & 0o777) !== 0;
Expand Down Expand Up @@ -525,6 +532,10 @@ export async function unlinkContained(view: MountView, rootReal: string, rel: st
if (!view.followSymlinks) throw new EffectFailed(NOT_FOLLOWED, false);
const real = await realpath(target).catch(() => null);
if (real === null || !contained(rootReal, real)) throw new EffectFailed('a symlink that leaves the mount', false);
// One leading into a nested mount's directory is that mount's: the link
// and the file it leads to are left as they are.
const holder = nestedAtReal(real, view.nested);
if (holder) throw new EffectFailed(leadsInto(view.name, rel, holder), false);
const through = await stat(real, { bigint: true }).catch(() => null);
if (through === null) throw new EffectFailed(CHANGED, false);
// A followed link stands for the file it leads to, so it's removed only
Expand Down
Loading
Loading