From 6f3a659ba9c41ab50d05850dda3595ceba160bc9 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Fri, 10 Apr 2026 08:12:55 +0000 Subject: [PATCH 1/3] fix(devcontainer): use 0o755 for feature directories so non-root users can actually walk into them MkdirAll was called with 0o644 (rw-r--r--) which drops the execute bit that directories need to be traversable. Harmless on the in-memory filesystem used in tests, but on a real filesystem non-root container users couldn't access feature install scripts. Fixes #506 --- devcontainer/devcontainer.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/devcontainer/devcontainer.go b/devcontainer/devcontainer.go index ea07bfc..88abcfc 100644 --- a/devcontainer/devcontainer.go +++ b/devcontainer/devcontainer.go @@ -227,7 +227,7 @@ func (s *Spec) compileFeatures(fs billy.Filesystem, devcontainerDir, scratchDir } featuresDir := filepath.Join(scratchDir, "features") - err := fs.MkdirAll(featuresDir, 0o644) + err := fs.MkdirAll(featuresDir, 0o755) if err != nil { return "", nil, fmt.Errorf("create features directory: %w", err) } @@ -277,7 +277,7 @@ func (s *Spec) compileFeatures(fs billy.Filesystem, devcontainerDir, scratchDir featureSha := md5.Sum([]byte(featureRefRaw)) featureName := filepath.Base(featureRef) featureDir := filepath.Join(featuresDir, fmt.Sprintf("%s-%x", featureName, featureSha[:4])) - if err := fs.MkdirAll(featureDir, 0o644); err != nil { + if err := fs.MkdirAll(featureDir, 0o755); err != nil { return "", nil, err } spec, err := features.Extract(fs, devcontainerDir, featureDir, featureRefRaw) From 670d34bf019689ce4984b1511d9645d5d3dca742 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Fri, 10 Apr 2026 08:28:54 +0000 Subject: [PATCH 2/3] fix(devcontainer): wrap per-feature MkdirAll error with ref and path context The featuresDir creation already wraps its error, but the per-feature directory creation returned the raw error. When this fails in the field you'd get a bare permission/path error with no indication of which feature or directory caused it. --- devcontainer/devcontainer.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/devcontainer/devcontainer.go b/devcontainer/devcontainer.go index 88abcfc..c0b79e1 100644 --- a/devcontainer/devcontainer.go +++ b/devcontainer/devcontainer.go @@ -278,7 +278,7 @@ func (s *Spec) compileFeatures(fs billy.Filesystem, devcontainerDir, scratchDir featureName := filepath.Base(featureRef) featureDir := filepath.Join(featuresDir, fmt.Sprintf("%s-%x", featureName, featureSha[:4])) if err := fs.MkdirAll(featureDir, 0o755); err != nil { - return "", nil, err + return "", nil, fmt.Errorf("create feature directory for %s at %s: %w", featureRefRaw, featureDir, err) } spec, err := features.Extract(fs, devcontainerDir, featureDir, featureRefRaw) if err != nil { From 3304543adbddcc961d235bacf413934071268be1 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Fri, 10 Apr 2026 08:38:46 +0000 Subject: [PATCH 3/3] test(devcontainer): assert feature directories are created with 0o755 Extends TestCompileWithFeatures/WithoutBuildContexts to stat the features root and per-feature directories after Compile and verify they have 0o755 permissions. memfs preserves the mode argument, so reverting the fix back to 0o644 will fail this assertion. --- devcontainer/devcontainer_test.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/devcontainer/devcontainer_test.go b/devcontainer/devcontainer_test.go index 5b7fe03..2f20f5f 100644 --- a/devcontainer/devcontainer_test.go +++ b/devcontainer/devcontainer_test.go @@ -115,6 +115,15 @@ WORKDIR `+featureTwoDir+` ENV POTATO=example RUN VERSION="potato" _CONTAINER_USER="1000" _REMOTE_USER="1000" ./install.sh USER 1000`, params.DockerfileContent) + + // Verify feature directories are created with 0o755 (not + // 0o644) so non-root container users can traverse them. + for _, dir := range []string{workingDir + "/features", featureOneDir, featureTwoDir} { + info, err := fs.Stat(dir) + require.NoError(t, err, "stat %s", dir) + require.Equalf(t, os.FileMode(0o755), info.Mode()&os.ModePerm, + "directory %s should be 0o755, got %04o", dir, info.Mode()&os.ModePerm) + } }) t.Run("WithBuildContexts", func(t *testing.T) {