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
36 changes: 34 additions & 2 deletions src/cloud-hypervisor/mount-tree.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,15 @@ function mountTable(options: { ineffectiveRemount?: boolean; shared?: boolean }
return;
}
if (args[0] === '--bind') {
table.set(args[2], { options: ['rw', 'relatime'], optionalFields: [] });
// A new bind mount joins the *source's* peer group, so binding from a
// shared host mount yields a shared mount even when the destination's
// parent is already private. Modelling that is what makes the overlay's
// explicit `--make-rprivate` observable here instead of only on a real
// kernel.
table.set(args[2], {
options: ['rw', 'relatime'],
optionalFields: options.shared === false ? [] : ['shared:23'],
});
return;
}
if (args[0] === '--make-rprivate') {
Expand Down Expand Up @@ -275,13 +283,15 @@ describe('StagedHostMountTree', () => {
await staged.stage();
expect(fake.commands.slice(4)).toEqual([
[tools.mount, '--bind', '/host/workspace/out', `${ROOT}/out`],
[tools.mount, '--make-rprivate', `${ROOT}/out`],
[tools.mount, '-o', 'remount,bind,rw,nosuid,nodev', `${ROOT}/out`],
[
tools.mount,
'--bind',
'/host/workspace/deep/nested/state.json',
`${ROOT}/deep/nested/state.json`,
],
[tools.mount, '--make-rprivate', `${ROOT}/deep/nested/state.json`],
[
tools.mount,
'-o',
Expand All @@ -295,6 +305,26 @@ describe('StagedHostMountTree', () => {
expect(fake.table.get(`${ROOT}/nested`)?.options).toContain('ro');
});

it('makes each writable overlay privately propagated so it cannot leak to the host', async () => {
// Regression: overlays were previously bound without being made private.
// A bind mount joins the source's peer group, so on any host where the
// workspace lives under a shared mount -- the default under systemd, and
// what GitHub-hosted runners provide -- every selective `allowWrite` run
// aborted with "Staged mount tree propagation would leak".
const fake = mountTable();
const staged = tree(
fake,
plan([
{ source: '/host/workspace/out', destination: '/host/workspace/out', kind: 'directory' },
]),
);
await expect(staged.stage()).resolves.toBeUndefined();
expect(fake.commands).toContainEqual([tools.mount, '--make-rprivate', `${ROOT}/out`]);
expect(fake.table.get(`${ROOT}/out`)?.optionalFields).toEqual([]);
expect(fake.table.get(`${ROOT}/out`)?.options).toContain('rw');
expect(fake.table.get(ROOT)?.options).toContain('ro');
});

it('unmounts children deepest-first and the staged root last', async () => {
const fake = mountTable();
const staged = tree(
Expand All @@ -315,7 +345,9 @@ describe('StagedHostMountTree', () => {
ROOT,
]);
await staged.unmount();
expect(fake.commands.slice(8)).toEqual([
// Sliced from the end so the assertion stays about unmount ordering rather
// than the exact number of staging commands that preceded it.
expect(fake.commands.slice(-3)).toEqual([
[tools.umount, `${ROOT}/deep/nested/state.json`],
[tools.umount, `${ROOT}/out`],
[tools.umount, '-R', ROOT],
Expand Down
9 changes: 9 additions & 0 deletions src/cloud-hypervisor/mount-tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,15 @@ export class StagedHostMountTree {
overlay.stagedDestination,
]);
this.pendingMounts.add(overlay.stagedDestination);
// A new bind mount joins the *source's* peer group, so an overlay bound
// from a shared host mount (the default for `/` under systemd, and what
// GitHub-hosted runners provide) arrives shared even though the staged
// root was already made private. Making the root private beforehand only
// covers mounts that existed at that point, so each overlay has to be
// made private in turn -- otherwise the writable overlay would propagate
// back into the host peer group and `assertPrivatePropagation()` would
// (correctly) refuse to stage the tree at all.
await dependencies.runTool(tools.mount, ['--make-rprivate', overlay.stagedDestination]);
await dependencies.runTool(tools.mount, [
'-o',
WRITABLE_REMOUNT_OPTIONS,
Expand Down
1 change: 1 addition & 0 deletions src/cloud-hypervisor/virtiofsd.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,7 @@ describe('VirtiofsdManager host mount-tree enforcement', () => {
['/usr/bin/mount', ['--make-rprivate', STAGED_ROOT]],
['/usr/bin/mount', ['-o', 'remount,bind,ro,nosuid,nodev', STAGED_ROOT]],
['/usr/bin/mount', ['--bind', '/host/workspace/out', `${STAGED_ROOT}/out`]],
['/usr/bin/mount', ['--make-rprivate', `${STAGED_ROOT}/out`]],
['/usr/bin/mount', ['-o', 'remount,bind,rw,nosuid,nodev', `${STAGED_ROOT}/out`]],
]);
expect(deps.mkdir).toHaveBeenCalledWith(STAGED_ROOT, { recursive: true, mode: 0o700 });
Expand Down
Loading