Skip to content

Commit b5ff737

Browse files
matejvasekclaude
andcommitted
fix: normalize file modes in shared data and certs image layers
newDataTarball (the shared layer carrying the function source, static files and the /func directory itself) and newCertsTarball copied on-disk file modes into the image verbatim, overriding only Uid/Gid - the same issue fixed for the python lib layer in the previous commit. For Go functions this is latent: the binary is self-contained and does not read those files at runtime, so it "works" as long as the project directory happens to be 0755 (the default under umask 022). But if the project directory has a stricter mode (e.g. 0750/0700), /func is baked in non-traversable and the container cannot reach /func/f under an arbitrary UID (e.g. OpenShift's restricted SCC) - failing before main runs. It also hardens the python source files shipped in this shared layer. Normalize dirs and executables to 0755 and regular files to 0644 (leaving symlink modes untouched), and force certs to 0644 so any UID (including group 0) can traverse and read them. The go exec layer already forced 0755 (go_builder.go); this covers the remaining shared layers. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 4343a3b commit b5ff737

2 files changed

Lines changed: 97 additions & 0 deletions

File tree

pkg/oci/builder.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,21 @@ func newDataTarball(root, target string, ignored []string, verbose bool) error {
381381
header.Name = slashpath.Join("/func", filepath.ToSlash(relPath))
382382
header.Uid = DefaultUid
383383
header.Gid = DefaultGid
384+
// Normalize permissions so the image works on platforms that run
385+
// containers with an arbitrary UID (e.g. OpenShift's restricted SCC).
386+
// The on-disk mode is not portable: e.g. a project directory created
387+
// under a stricter umask can be non-traversable by group/other, which
388+
// would make /func (and thus /func/f) inaccessible to any UID other
389+
// than the image's configured one. Directories and executables get
390+
// 0755, regular files 0644. Symlink modes are not meaningful and are
391+
// left untouched.
392+
if info.Mode()&fs.ModeSymlink == 0 {
393+
if info.IsDir() || info.Mode()&0o111 != 0 {
394+
header.Mode = (header.Mode & ^int64(fs.ModePerm)) | 0o755
395+
} else {
396+
header.Mode = (header.Mode & ^int64(fs.ModePerm)) | 0o644
397+
}
398+
}
384399

385400
if err := tw.WriteHeader(header); err != nil {
386401
return err
@@ -502,6 +517,9 @@ func newCertsTarball(source, target string, verbose bool) error {
502517
header.Name = path
503518
header.Uid = DefaultUid
504519
header.Gid = DefaultGid
520+
// Certs must be world-readable so any UID can read them (see the
521+
// arbitrary-UID note in newDataTarball).
522+
header.Mode = (header.Mode & ^int64(fs.ModePerm)) | 0o644
505523

506524
if err := tw.WriteHeader(header); err != nil {
507525
return err

pkg/oci/builder_test.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,85 @@ type fileInfo struct {
326326
Linkname string
327327
}
328328

329+
// TestNewDataTarball_NormalizesModes ensures that the shared data layer
330+
// (function source, static files and the /func directory itself) is written
331+
// with portable permissions regardless of the on-disk modes.
332+
//
333+
// Regression test: the on-disk mode of the project directory and its files is
334+
// copied verbatim into the image layer. Under a strict umask that yields a
335+
// non-traversable /func, or files that are not world-readable, the resulting
336+
// image fails under an arbitrary UID (e.g. OpenShift's restricted SCC) because
337+
// a non-owner UID cannot traverse /func to reach /func/f (or read the files).
338+
// Directories and executables must be 0755 and regular files 0644; symlinks are
339+
// left untouched.
340+
func TestNewDataTarball_NormalizesModes(t *testing.T) {
341+
root := t.TempDir()
342+
343+
if err := os.WriteFile(filepath.Join(root, "func.yaml"), []byte("x\n"), 0o600); err != nil {
344+
t.Fatal(err)
345+
}
346+
if err := os.WriteFile(filepath.Join(root, "run.sh"), []byte("#!/bin/sh\n"), 0o700); err != nil {
347+
t.Fatal(err)
348+
}
349+
sub := filepath.Join(root, "sub")
350+
if err := os.Mkdir(sub, 0o700); err != nil {
351+
t.Fatal(err)
352+
}
353+
if err := os.WriteFile(filepath.Join(sub, "mod.py"), []byte("y\n"), 0o640); err != nil {
354+
t.Fatal(err)
355+
}
356+
// Force a non-traversable project root, i.e. what the failure looks like.
357+
if err := os.Chmod(root, 0o750); err != nil {
358+
t.Fatal(err)
359+
}
360+
defer os.Chmod(root, 0o755) //nolint:errcheck // best effort for cleanup
361+
362+
target := filepath.Join(t.TempDir(), "data.tar.gz")
363+
if err := newDataTarball(root, target, nil, false); err != nil {
364+
t.Fatal(err)
365+
}
366+
367+
modes := map[string]int64{}
368+
f, err := os.Open(target)
369+
if err != nil {
370+
t.Fatal(err)
371+
}
372+
defer f.Close()
373+
gr, err := gzip.NewReader(f)
374+
if err != nil {
375+
t.Fatal(err)
376+
}
377+
defer gr.Close()
378+
tr := tar.NewReader(gr)
379+
for {
380+
hdr, err := tr.Next()
381+
if err != nil {
382+
if errors.Is(err, io.EOF) {
383+
break
384+
}
385+
t.Fatal(err)
386+
}
387+
modes[hdr.Name] = hdr.Mode & int64(fs.ModePerm)
388+
}
389+
390+
assertMode := func(name string, want int64) {
391+
t.Helper()
392+
got, ok := modes[name]
393+
if !ok {
394+
t.Fatalf("entry %q not found in tarball; entries: %v", name, modes)
395+
}
396+
if got != want {
397+
t.Errorf("entry %q has mode %#o, want %#o", name, got, want)
398+
}
399+
}
400+
401+
assertMode("/func", 0o755) // the project root must be traversable
402+
assertMode("/func/sub", 0o755) // subdirs too
403+
assertMode("/func/func.yaml", 0o644)
404+
assertMode("/func/sub/mod.py", 0o644)
405+
assertMode("/func/run.sh", 0o755) // executables keep the execute bit
406+
}
407+
329408
// TestBuilder_StaticEnvs ensures that certain "static" environment variables
330409
// comprising Function metadata are added to the config.
331410
func TestBuilder_StaticEnvs(t *testing.T) {

0 commit comments

Comments
 (0)