From ce305b838b277f4b8ecf8952fb5c746c193c9c92 Mon Sep 17 00:00:00 2001 From: akki Date: Thu, 1 Oct 2026 01:31:04 +0900 Subject: [PATCH 1/2] Preserve readonly flag when converting mounts to -v for wslc The WSLc code path converts --mount strings to -v syntax but dropped the readonly/ro option, so every mount declared readonly in devcontainer.json was mounted writable. wslc supports the "-v source:target:ro" suffix (verified with wslc 3.0.1), so append ":ro" when the mount string carries readonly/ro unless its value is explicitly false. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_018psPhUnN32LKjqd4ZkATtP --- src/spec-node/singleContainer.ts | 10 +++-- src/test/singleContainer.test.ts | 65 ++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 4 deletions(-) create mode 100644 src/test/singleContainer.test.ts diff --git a/src/spec-node/singleContainer.ts b/src/spec-node/singleContainer.ts index 362559c2e..0d82d969e 100644 --- a/src/spec-node/singleContainer.ts +++ b/src/spec-node/singleContainer.ts @@ -450,19 +450,21 @@ async function getPodmanArgs(params: DockerResolverParameters, config: DevContai return []; } -// Convert a --mount string (e.g., "type=bind,source=/a,target=/b,consistency=cached") to -v syntax for wslc. -function convertMountToVolume(mountStr: string): string[] { +// Convert a --mount string (e.g., "type=bind,source=/a,target=/b,readonly") to -v syntax for wslc. +export function convertMountToVolume(mountStr: string): string[] { const parts = new Map(mountStr.split(',').map(p => { const eq = p.indexOf('='); return eq === -1 ? [p, ''] : [p.substring(0, eq), p.substring(eq + 1)]; })); const source = parts.get('source') || parts.get('src') || ''; const target = parts.get('target') || parts.get('dst') || parts.get('destination') || ''; + const readonlyValue = parts.has('readonly') ? parts.get('readonly') : parts.get('ro'); + const readonlySuffix = readonlyValue !== undefined && !/^(false|0)$/i.test(readonlyValue) ? ':ro' : ''; if (source && target) { - return ['-v', `${source}:${target}`]; + return ['-v', `${source}:${target}${readonlySuffix}`]; } if (target) { - return ['-v', target]; + return ['-v', `${target}${readonlySuffix}`]; } // Fallback: pass as --mount and let the runtime handle it. return ['--mount', mountStr]; diff --git a/src/test/singleContainer.test.ts b/src/test/singleContainer.test.ts new file mode 100644 index 000000000..636d5bdf1 --- /dev/null +++ b/src/test/singleContainer.test.ts @@ -0,0 +1,65 @@ +import { assert } from 'chai'; +import { convertMountToVolume } from '../spec-node/singleContainer'; + +describe('convertMountToVolume (wslc -v syntax)', () => { + + it('converts a bind mount with source and target', () => { + assert.deepEqual( + convertMountToVolume('type=bind,source=/a,target=/b'), + ['-v', '/a:/b']); + }); + + it('preserves the readonly flag as :ro', () => { + assert.deepEqual( + convertMountToVolume('type=bind,source=/a,target=/b,readonly'), + ['-v', '/a:/b:ro']); + }); + + it('preserves readonly=true as :ro', () => { + assert.deepEqual( + convertMountToVolume('type=bind,source=/a,target=/b,readonly=true'), + ['-v', '/a:/b:ro']); + }); + + it('preserves the ro shorthand as :ro', () => { + assert.deepEqual( + convertMountToVolume('type=bind,source=/a,target=/b,ro'), + ['-v', '/a:/b:ro']); + }); + + it('ignores readonly=false', () => { + assert.deepEqual( + convertMountToVolume('type=bind,source=/a,target=/b,readonly=false'), + ['-v', '/a:/b']); + }); + + it('drops the consistency option', () => { + assert.deepEqual( + convertMountToVolume('type=bind,source=/a,target=/b,consistency=cached'), + ['-v', '/a:/b']); + }); + + it('converts a named volume mount', () => { + assert.deepEqual( + convertMountToVolume('type=volume,source=vol,target=/b,readonly'), + ['-v', 'vol:/b:ro']); + }); + + it('converts a Windows source path with a readonly flag', () => { + assert.deepEqual( + convertMountToVolume('type=bind,source=C:\\some\\folder,target=/b,readonly'), + ['-v', 'C:\\some\\folder:/b:ro']); + }); + + it('converts a target-only mount to an anonymous volume', () => { + assert.deepEqual( + convertMountToVolume('type=volume,target=/b'), + ['-v', '/b']); + }); + + it('falls back to --mount when no target is present', () => { + assert.deepEqual( + convertMountToVolume('type=tmpfs'), + ['--mount', 'type=tmpfs']); + }); +}); From b6ca47c9090ecf2d8aaa61fa5999d23f85712787 Mon Sep 17 00:00:00 2001 From: akki Date: Thu, 1 Oct 2026 01:39:44 +0900 Subject: [PATCH 2/2] Clarify convertMountToVolume doc comment Keep consistency=cached in the example so it still illustrates that options other than source/target/readonly are dropped. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_018psPhUnN32LKjqd4ZkATtP --- src/spec-node/singleContainer.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/spec-node/singleContainer.ts b/src/spec-node/singleContainer.ts index 0d82d969e..550b086d0 100644 --- a/src/spec-node/singleContainer.ts +++ b/src/spec-node/singleContainer.ts @@ -450,7 +450,8 @@ async function getPodmanArgs(params: DockerResolverParameters, config: DevContai return []; } -// Convert a --mount string (e.g., "type=bind,source=/a,target=/b,readonly") to -v syntax for wslc. +// Convert a --mount string (e.g., "type=bind,source=/a,target=/b,consistency=cached,readonly") +// to -v syntax for wslc. Options other than source/target/readonly are dropped. export function convertMountToVolume(mountStr: string): string[] { const parts = new Map(mountStr.split(',').map(p => { const eq = p.indexOf('=');