Skip to content

Commit e978534

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 e978534

8 files changed

Lines changed: 181 additions & 15 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: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
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 uint32
33+
}{
34+
{
35+
desc: "setgid bit 02770",
36+
initialMode: 0770,
37+
requestedMode: 02770,
38+
},
39+
{
40+
desc: "sticky bit 01777",
41+
initialMode: 0777,
42+
requestedMode: 01777,
43+
},
44+
{
45+
desc: "setgid already set 02770 -> 02770 no change",
46+
initialMode: 02770,
47+
requestedMode: 02770,
48+
},
49+
{
50+
desc: "setuid bit 04755",
51+
initialMode: 0755,
52+
requestedMode: 04755,
53+
},
54+
}
55+
56+
for _, test := range tests {
57+
t.Run(test.desc, func(t *testing.T) {
58+
dir := t.TempDir()
59+
targetPath := dir + "/testdir"
60+
if err := os.Mkdir(targetPath, 0777); err != nil {
61+
t.Fatalf("failed to create test dir: %v", err)
62+
}
63+
// Set initial permissions using syscall to support special bits
64+
if err := syscall.Chmod(targetPath, test.initialMode); err != nil {
65+
t.Fatalf("failed to set initial mode: %v", err)
66+
}
67+
68+
if err := chmodIfPermissionMismatch(targetPath, test.requestedMode); err != nil {
69+
t.Fatalf("chmodIfPermissionMismatch failed: %v", err)
70+
}
71+
72+
// Verify the final permissions
73+
info, err := os.Lstat(targetPath)
74+
if err != nil {
75+
t.Fatalf("failed to stat: %v", err)
76+
}
77+
78+
// Get raw mode bits via syscall for accurate comparison
79+
stat := info.Sys().(*syscall.Stat_t)
80+
actualMode := uint32(stat.Mode) & 07777
81+
if actualMode != test.requestedMode {
82+
t.Errorf("expected mode 0%o, got 0%o", test.requestedMode, actualMode)
83+
}
84+
})
85+
}
86+
}

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/controllerserver.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ func (cs *ControllerServer) CreateVolume(ctx context.Context, req *csi.CreateVol
187187

188188
if mountPermissions > 0 {
189189
// Reset directory permissions because of umask problems
190-
if err := chmodIfPermissionMismatch(internalVolumePath, os.FileMode(mountPermissions)); err != nil {
190+
if err := chmodIfPermissionMismatch(internalVolumePath, uint32(mountPermissions)); err != nil {
191191
klog.Warningf("failed to chmod subdirectory: %v", err)
192192
}
193193
}

pkg/nfs/nodeserver.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ func (ns *NodeServer) NodePublishVolume(_ context.Context, req *csi.NodePublishV
122122
notMnt, err := ns.mounter.IsLikelyNotMountPoint(targetPath)
123123
if err != nil {
124124
if os.IsNotExist(err) {
125-
if err := os.MkdirAll(targetPath, 0777); err != nil {
125+
if err := os.MkdirAll(targetPath, 0755); err != nil {
126126
return nil, status.Error(codes.Internal, err.Error())
127127
}
128128
notMnt = true
@@ -152,7 +152,7 @@ func (ns *NodeServer) NodePublishVolume(_ context.Context, req *csi.NodePublishV
152152
}
153153

154154
if mountPermissions > 0 {
155-
if err := chmodIfPermissionMismatch(targetPath, os.FileMode(mountPermissions)); err != nil {
155+
if err := chmodIfPermissionMismatch(targetPath, uint32(mountPermissions)); err != nil {
156156
return nil, status.Error(codes.Internal, err.Error())
157157
}
158158
} else {

pkg/nfs/utils.go

Lines changed: 33 additions & 10 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,46 @@ func getMountOptions(context map[string]string) string {
159158
return ""
160159
}
161160

162-
// 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.
165-
func chmodIfPermissionMismatch(targetPath string, mode os.FileMode) error {
161+
// unixModeToFileMode converts a raw Unix mode_t value (e.g. 02770) into Go's
162+
// os.FileMode representation with correct bit positions for setuid, setgid,
163+
// and sticky bits.
164+
func unixModeToFileMode(mode uint32) os.FileMode {
165+
goMode := os.FileMode(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+
178+
// chmodIfPermissionMismatch only performs chmod when permission mismatches.
179+
// The mode parameter is a raw Unix mode_t value (e.g. 02770).
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.
182+
// Note: on Windows, the chmod fallback (os.Chmod) cannot apply special bits, so
183+
// modes with setuid/setgid/sticky will never fully converge there.
184+
func chmodIfPermissionMismatch(targetPath string, mode uint32) error {
166185
info, err := os.Lstat(targetPath)
167186
if err != nil {
168187
return err
169188
}
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 {
189+
// Convert the raw Unix mode to Go's FileMode representation for comparison.
190+
desiredMode := unixModeToFileMode(mode)
191+
// Mask for perm bits + special bits in Go's representation.
192+
mask := os.ModePerm | os.ModeSetuid | os.ModeSetgid | os.ModeSticky
193+
currentMode := info.Mode() & mask
194+
if currentMode != desiredMode {
195+
klog.V(2).Infof("chmod targetPath(%s, currentMode:0%o) with desiredMode(0%o)", targetPath, mode, mode)
196+
if err := chmod(targetPath, mode); err != nil {
174197
return err
175198
}
176199
} else {
177-
klog.V(2).Infof("skip chmod on targetPath(%s) since mode is already 0%o)", targetPath, info.Mode())
200+
klog.V(2).Infof("skip chmod on targetPath(%s) since mode is already 0%o", targetPath, mode)
178201
}
179202
return nil
180203
}

pkg/nfs/utils_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ func TestChmodIfPermissionMismatch(t *testing.T) {
173173
tests := []struct {
174174
desc string
175175
path string
176-
mode os.FileMode
176+
mode uint32
177177
expectedError error
178178
}{
179179
{

0 commit comments

Comments
 (0)