Skip to content

Commit a4b306b

Browse files
authored
Merge pull request #1110 from k8s-infra-cherrypick-robot/cherry-pick-1106-to-release-4.13
[release-4.13] fix: use syscall.Chmod to correctly handle setgid/setuid/sticky bits in mountPermissions
2 parents adec3ac + 2bbcea5 commit a4b306b

9 files changed

Lines changed: 181 additions & 12 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 = os.Chmod(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: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ func (ns *NodeServer) NodePublishVolume(_ context.Context, req *csi.NodePublishV
168168
}
169169

170170
if mountPermissions > 0 {
171-
if err := chmodIfPermissionMismatch(targetPath, os.FileMode(mountPermissions)); err != nil {
171+
if err := chmodIfPermissionMismatch(targetPath, uint32(mountPermissions)); err != nil {
172172
return nil, status.Error(codes.Internal, err.Error())
173173
}
174174
} else {

pkg/nfs/utils.go

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -158,20 +158,46 @@ func getMountOptions(context map[string]string) string {
158158
return ""
159159
}
160160

161-
// chmodIfPermissionMismatch only perform chmod when permission mismatches
162-
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 {
163185
info, err := os.Lstat(targetPath)
164186
if err != nil {
165187
return err
166188
}
167-
perm := info.Mode() & os.ModePerm
168-
if perm != mode {
169-
klog.V(2).Infof("chmod targetPath(%s, mode:0%o) with permissions(0%o)", targetPath, info.Mode(), mode)
170-
if err := os.Chmod(targetPath, 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 {
171197
return err
172198
}
173199
} else {
174-
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)
175201
}
176202
return nil
177203
}

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
{

test/e2e/e2e_suite_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ var (
5050
"share": nfsShare,
5151
"csi.storage.k8s.io/provisioner-secret-name": "mount-options",
5252
"csi.storage.k8s.io/provisioner-secret-namespace": "default",
53-
"mountPermissions": "0755",
53+
"mountPermissions": "02770",
5454
}
5555
storageClassParametersWithZeroMountPermisssions = map[string]string{
5656
"server": nfsServerAddress,

0 commit comments

Comments
 (0)