Skip to content

Commit 7537bb6

Browse files
committed
fix: address review comments - platform-specific chmod, fix permission mask, add special-bit tests
- Split syscall.Chmod into platform-specific files (chmod_unix.go, chmod_windows.go) with build tags to avoid breaking Windows builds - Fix permission comparison mask in chmodIfPermissionMismatch to include setuid/setgid/sticky bits, not just os.ModePerm (0777) - Add fileModeToUnixMode helper to correctly convert raw Unix mode values to Go's os.FileMode representation - Add unit tests for special bits (02770, 01777, 04755) in chmod_unix_test.go
1 parent 5ec8a16 commit 7537bb6

5 files changed

Lines changed: 179 additions & 9 deletions

File tree

hack/boilerplate/boilerplate.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ def get_regexs():
180180
years = range(2014, date.today().year + 1)
181181
regexs["date"] = re.compile( '(%s)' % "|".join(map(lambda l: str(l), years)) )
182182
# strip // +build \n\n build constraints
183-
regexs["go_build_constraints"] = re.compile(r"^(// \+build.*\n)+\n", re.MULTILINE)
183+
regexs["go_build_constraints"] = re.compile(r"^(//go:build.*\n(// \+build.*\n)*|// \+build.*\n(// \+build.*\n)*)\n", re.MULTILINE)
184184
# strip #!.* from shell scripts
185185
regexs["shebang"] = re.compile(r"^(#!.*\n)\n*", re.MULTILINE)
186186
return regexs

pkg/nfs/chmod_unix.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
//go:build !windows
2+
// +build !windows
3+
4+
/*
5+
Copyright 2020 The Kubernetes Authors.
6+
7+
Licensed under the Apache License, Version 2.0 (the "License");
8+
you may not use this file except in compliance with the License.
9+
You may obtain a copy of the License at
10+
11+
http://www.apache.org/licenses/LICENSE-2.0
12+
13+
Unless required by applicable law or agreed to in writing, software
14+
distributed under the License is distributed on an "AS IS" BASIS,
15+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
See the License for the specific language governing permissions and
17+
limitations under the License.
18+
*/
19+
20+
package nfs
21+
22+
import "syscall"
23+
24+
// chmod uses syscall.Chmod to correctly handle setuid/setgid/sticky bits
25+
// (e.g. 02770), since os.Chmod maps os.FileMode bits differently from raw
26+
// Unix mode bits.
27+
func chmod(path string, mode uint32) error {
28+
return syscall.Chmod(path, mode)
29+
}

pkg/nfs/chmod_unix_test.go

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
//go:build !windows
2+
// +build !windows
3+
4+
/*
5+
Copyright 2020 The Kubernetes Authors.
6+
7+
Licensed under the Apache License, Version 2.0 (the "License");
8+
you may not use this file except in compliance with the License.
9+
You may obtain a copy of the License at
10+
11+
http://www.apache.org/licenses/LICENSE-2.0
12+
13+
Unless required by applicable law or agreed to in writing, software
14+
distributed under the License is distributed on an "AS IS" BASIS,
15+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
See the License for the specific language governing permissions and
17+
limitations under the License.
18+
*/
19+
20+
package nfs
21+
22+
import (
23+
"os"
24+
"syscall"
25+
"testing"
26+
)
27+
28+
func TestChmodIfPermissionMismatchSpecialBits(t *testing.T) {
29+
tests := []struct {
30+
desc string
31+
initialMode uint32
32+
requestedMode os.FileMode
33+
expectChmod bool
34+
}{
35+
{
36+
desc: "setgid bit 02770",
37+
initialMode: 0770,
38+
requestedMode: 02770,
39+
expectChmod: true,
40+
},
41+
{
42+
desc: "sticky bit 01777",
43+
initialMode: 0777,
44+
requestedMode: 01777,
45+
expectChmod: true,
46+
},
47+
{
48+
desc: "setgid already set 02770 -> 02770 no change",
49+
initialMode: 02770,
50+
requestedMode: 02770,
51+
expectChmod: false,
52+
},
53+
{
54+
desc: "setuid bit 04755",
55+
initialMode: 0755,
56+
requestedMode: 04755,
57+
expectChmod: true,
58+
},
59+
}
60+
61+
for _, test := range tests {
62+
t.Run(test.desc, func(t *testing.T) {
63+
dir := t.TempDir()
64+
targetPath := dir + "/testdir"
65+
if err := os.Mkdir(targetPath, 0777); err != nil {
66+
t.Fatalf("failed to create test dir: %v", err)
67+
}
68+
// Set initial permissions using syscall to support special bits
69+
if err := syscall.Chmod(targetPath, test.initialMode); err != nil {
70+
t.Fatalf("failed to set initial mode: %v", err)
71+
}
72+
73+
if err := chmodIfPermissionMismatch(targetPath, test.requestedMode); err != nil {
74+
t.Fatalf("chmodIfPermissionMismatch failed: %v", err)
75+
}
76+
77+
// Verify the final permissions
78+
info, err := os.Lstat(targetPath)
79+
if err != nil {
80+
t.Fatalf("failed to stat: %v", err)
81+
}
82+
83+
// Get raw mode bits via syscall for accurate comparison
84+
stat := info.Sys().(*syscall.Stat_t)
85+
actualMode := uint32(stat.Mode) & 07777
86+
expectedMode := uint32(test.requestedMode)
87+
if actualMode != expectedMode {
88+
t.Errorf("expected mode 0%o, got 0%o", expectedMode, actualMode)
89+
}
90+
})
91+
}
92+
}

pkg/nfs/chmod_windows.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
//go:build windows
2+
// +build windows
3+
4+
/*
5+
Copyright 2020 The Kubernetes Authors.
6+
7+
Licensed under the Apache License, Version 2.0 (the "License");
8+
you may not use this file except in compliance with the License.
9+
You may obtain a copy of the License at
10+
11+
http://www.apache.org/licenses/LICENSE-2.0
12+
13+
Unless required by applicable law or agreed to in writing, software
14+
distributed under the License is distributed on an "AS IS" BASIS,
15+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
See the License for the specific language governing permissions and
17+
limitations under the License.
18+
*/
19+
20+
package nfs
21+
22+
import "os"
23+
24+
// chmod falls back to os.Chmod on Windows, which does not support
25+
// setuid/setgid/sticky bits but avoids a build failure.
26+
func chmod(path string, mode uint32) error {
27+
return os.Chmod(path, os.FileMode(mode))
28+
}

pkg/nfs/utils.go

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ import (
2323
"path/filepath"
2424
"strings"
2525
"sync"
26-
"syscall"
2726
"time"
2827

2928
"github.com/container-storage-interface/spec/lib/go/csi"
@@ -159,22 +158,44 @@ func getMountOptions(context map[string]string) string {
159158
return ""
160159
}
161160

161+
// fileModeToUnixMode converts a raw Unix mode value (stored as os.FileMode)
162+
// to its proper os.FileMode representation with correct Go bit positions for
163+
// setuid, setgid, and sticky bits.
164+
func fileModeToUnixMode(mode os.FileMode) os.FileMode {
165+
goMode := mode & os.ModePerm
166+
if mode&04000 != 0 {
167+
goMode |= os.ModeSetuid
168+
}
169+
if mode&02000 != 0 {
170+
goMode |= os.ModeSetgid
171+
}
172+
if mode&01000 != 0 {
173+
goMode |= os.ModeSticky
174+
}
175+
return goMode
176+
}
177+
162178
// chmodIfPermissionMismatch only perform chmod when permission mismatches.
163-
// Uses syscall.Chmod to correctly handle setuid/setgid/sticky bits (e.g. 02770),
164-
// since os.Chmod maps os.FileMode bits differently from raw Unix mode bits.
179+
// The mode parameter is a raw Unix mode value (e.g. 02770) stored as os.FileMode.
180+
// Compares both regular permission bits (0777) and special bits (setuid/setgid/sticky)
181+
// to avoid unnecessary chmod calls while still detecting special-bit differences.
165182
func chmodIfPermissionMismatch(targetPath string, mode os.FileMode) error {
166183
info, err := os.Lstat(targetPath)
167184
if err != nil {
168185
return err
169186
}
170-
perm := info.Mode() & os.ModePerm
171-
if perm != mode {
172-
klog.V(2).Infof("chmod targetPath(%s, mode:0%o) with permissions(0%o)", targetPath, info.Mode(), mode)
173-
if err := syscall.Chmod(targetPath, uint32(mode)); err != nil {
187+
// Convert the raw Unix mode to Go's FileMode representation for comparison.
188+
desiredMode := fileModeToUnixMode(mode)
189+
// Mask for perm bits + special bits in Go's representation.
190+
mask := os.ModePerm | os.ModeSetuid | os.ModeSetgid | os.ModeSticky
191+
currentMode := info.Mode() & mask
192+
if currentMode != desiredMode {
193+
klog.V(2).Infof("chmod targetPath(%s, currentMode:0%o) with desiredMode(0%o)", targetPath, currentMode, desiredMode)
194+
if err := chmod(targetPath, uint32(mode)); err != nil {
174195
return err
175196
}
176197
} else {
177-
klog.V(2).Infof("skip chmod on targetPath(%s) since mode is already 0%o)", targetPath, info.Mode())
198+
klog.V(2).Infof("skip chmod on targetPath(%s) since mode is already 0%o)", targetPath, currentMode)
178199
}
179200
return nil
180201
}

0 commit comments

Comments
 (0)