Browse code

vendor: opencontainers/selinux v1.7.0

full diff: https://github.com/opencontainers/selinux/compare/v1.6.0...v1.7.0

- Implement get_default_context_with_level() from libselinux
- Wrap some syscalls (lgetattr, lsetattr, fstatfs, statfs) to retry on EINTR.
- Improve code quality by turning fixing many problems found by linters
- Use bufio.Scanner for parsing labels and policy confilabelg
- Cache the value for SELinux policy directory
- test on ppc64le and go 1.15

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>

Sebastiaan van Stijn authored on 2020/12/15 22:00:08
Showing 9 changed files
... ...
@@ -176,7 +176,7 @@ github.com/morikuni/aec                             39771216ff4c63d11f5e604076f9
176 176
 # metrics
177 177
 github.com/docker/go-metrics                        b619b3592b65de4f087d9f16863a7e6ff905973c # v0.0.1
178 178
 
179
-github.com/opencontainers/selinux                   25504e34a9826d481f6e2903963ecaa881749124 # v1.6.0
179
+github.com/opencontainers/selinux                   63ad55b76fd78d4c76c2f5491f68516e60c9d523 # v1.7.0
180 180
 github.com/willf/bitset                             559910e8471e48d76d9e5a1ba15842dee77ad45d # v1.1.11
181 181
 
182 182
 
... ...
@@ -18,5 +18,5 @@ Participation in the OpenContainers community is governed by [OpenContainer's Co
18 18
 
19 19
 If you find an issue, please follow the [security][security] protocol to report it.
20 20
 
21
-[security]: https://github.com/opencontainers/org/blob/master/security
21
+[security]: https://github.com/opencontainers/org/blob/master/SECURITY.md
22 22
 [code-of-conduct]: https://github.com/opencontainers/org/blob/master/CODE_OF_CONDUCT.md
... ...
@@ -27,14 +27,14 @@ var ErrIncompatibleLabel = errors.New("Bad SELinux option z and Z can not be use
27 27
 // the container.  A list of options can be passed into this function to alter
28 28
 // the labels.  The labels returned will include a random MCS String, that is
29 29
 // guaranteed to be unique.
30
-func InitLabels(options []string) (plabel string, mlabel string, Err error) {
30
+func InitLabels(options []string) (plabel string, mlabel string, retErr error) {
31 31
 	if !selinux.GetEnabled() {
32 32
 		return "", "", nil
33 33
 	}
34 34
 	processLabel, mountLabel := selinux.ContainerLabels()
35 35
 	if processLabel != "" {
36 36
 		defer func() {
37
-			if Err != nil {
37
+			if retErr != nil {
38 38
 				selinux.ReleaseLabel(mountLabel)
39 39
 			}
40 40
 		}()
... ...
@@ -57,7 +57,6 @@ func InitLabels(options []string) (plabel string, mlabel string, Err error) {
57 57
 			con := strings.SplitN(opt, ":", 2)
58 58
 			if !validOptions[con[0]] {
59 59
 				return "", "", errors.Errorf("Bad label option %q, valid options 'disable, user, role, level, type, filetype'", con[0])
60
-
61 60
 			}
62 61
 			if con[0] == "filetype" {
63 62
 				mcon["type"] = con[1]
... ...
@@ -30,6 +30,11 @@ var (
30 30
 	// ErrLevelSyntax is returned when a sensitivity or category do not have correct syntax in a level
31 31
 	ErrLevelSyntax = errors.New("invalid level syntax")
32 32
 
33
+	// ErrContextMissing is returned if a requested context is not found in a file.
34
+	ErrContextMissing = errors.New("context does not have a match")
35
+	// ErrVerifierNil is returned when a context verifier function is nil.
36
+	ErrVerifierNil = errors.New("verifier function is nil")
37
+
33 38
 	// CategoryRange allows the upper bound on the category range to be adjusted
34 39
 	CategoryRange = DefaultCategoryRange
35 40
 )
... ...
@@ -63,8 +68,12 @@ func FileLabel(fpath string) (string, error) {
63 63
 	return fileLabel(fpath)
64 64
 }
65 65
 
66
-// SetFSCreateLabel tells kernel the label to create all file system objects
67
-// created by this task. Setting label="" to return to default.
66
+// SetFSCreateLabel tells the kernel what label to use for all file system objects
67
+// created by this task.
68
+// Set the label to an empty string to return to the default label. Calls to SetFSCreateLabel
69
+// should be wrapped in runtime.LockOSThread()/runtime.UnlockOSThread() until file system
70
+// objects created by this task are finished to guarantee another goroutine does not migrate
71
+// to the current thread before execution is complete.
68 72
 func SetFSCreateLabel(label string) error {
69 73
 	return setFSCreateLabel(label)
70 74
 }
... ...
@@ -113,19 +122,27 @@ func CalculateGlbLub(sourceRange, targetRange string) (string, error) {
113 113
 }
114 114
 
115 115
 // SetExecLabel sets the SELinux label that the kernel will use for any programs
116
-// that are executed by the current process thread, or an error.
116
+// that are executed by the current process thread, or an error. Calls to SetExecLabel
117
+// should  be wrapped in runtime.LockOSThread()/runtime.UnlockOSThread() until execution
118
+// of the program is finished to guarantee another goroutine does not migrate to the current
119
+// thread before execution is complete.
117 120
 func SetExecLabel(label string) error {
118 121
 	return setExecLabel(label)
119 122
 }
120 123
 
121 124
 // SetTaskLabel sets the SELinux label for the current thread, or an error.
122
-// This requires the dyntransition permission.
125
+// This requires the dyntransition permission. Calls to SetTaskLabel should
126
+// be wrapped in runtime.LockOSThread()/runtime.UnlockOSThread() to guarantee
127
+// the current thread does not run in a new mislabeled thread.
123 128
 func SetTaskLabel(label string) error {
124 129
 	return setTaskLabel(label)
125 130
 }
126 131
 
127 132
 // SetSocketLabel takes a process label and tells the kernel to assign the
128
-// label to the next socket that gets created
133
+// label to the next socket that gets created. Calls to SetSocketLabel
134
+// should be wrapped in runtime.LockOSThread()/runtime.UnlockOSThread() until
135
+// the the socket is created to guarantee another goroutine does not migrate
136
+// to the current thread before execution is complete.
129 137
 func SetSocketLabel(label string) error {
130 138
 	return setSocketLabel(label)
131 139
 }
... ...
@@ -141,7 +158,10 @@ func PeerLabel(fd uintptr) (string, error) {
141 141
 }
142 142
 
143 143
 // SetKeyLabel takes a process label and tells the kernel to assign the
144
-// label to the next kernel keyring that gets created
144
+// label to the next kernel keyring that gets created. Calls to SetKeyLabel
145
+// should be wrapped in runtime.LockOSThread()/runtime.UnlockOSThread() until
146
+// the kernel keyring is created to guarantee another goroutine does not migrate
147
+// to the current thread before execution is complete.
145 148
 func SetKeyLabel(label string) error {
146 149
 	return setKeyLabel(label)
147 150
 }
... ...
@@ -247,3 +267,12 @@ func DupSecOpt(src string) ([]string, error) {
247 247
 func DisableSecOpt() []string {
248 248
 	return disableSecOpt()
249 249
 }
250
+
251
+// GetDefaultContextWithLevel gets a single context for the specified SELinux user
252
+// identity that is reachable from the specified scon context. The context is based
253
+// on the per-user /etc/selinux/{SELINUXTYPE}/contexts/users/<username> if it exists,
254
+// and falls back to the global /etc/selinux/{SELINUXTYPE}/contexts/default_contexts
255
+// file.
256
+func GetDefaultContextWithLevel(user, level, scon string) (string, error) {
257
+	return getDefaultContextWithLevel(user, level, scon)
258
+}
... ...
@@ -28,6 +28,8 @@ const (
28 28
 	minSensLen       = 2
29 29
 	contextFile      = "/usr/share/containers/selinux/contexts"
30 30
 	selinuxDir       = "/etc/selinux/"
31
+	selinuxUsersDir  = "contexts/users"
32
+	defaultContexts  = "contexts/default_contexts"
31 33
 	selinuxConfig    = selinuxDir + "config"
32 34
 	selinuxfsMount   = "/sys/fs/selinux"
33 35
 	selinuxTypeTag   = "SELINUXTYPE"
... ...
@@ -35,6 +37,8 @@ const (
35 35
 	xattrNameSelinux = "security.selinux"
36 36
 )
37 37
 
38
+var policyRoot = filepath.Join(selinuxDir, readConfig(selinuxTypeTag))
39
+
38 40
 type selinuxState struct {
39 41
 	enabledSet    bool
40 42
 	enabled       bool
... ...
@@ -54,6 +58,13 @@ type mlsRange struct {
54 54
 	high *level
55 55
 }
56 56
 
57
+type defaultSECtx struct {
58
+	user, level, scon   string
59
+	userRdr, defaultRdr io.Reader
60
+
61
+	verifier func(string) error
62
+}
63
+
57 64
 type levelItem byte
58 65
 
59 66
 const (
... ...
@@ -111,7 +122,7 @@ func verifySELinuxfsMount(mnt string) bool {
111 111
 		if err == nil {
112 112
 			break
113 113
 		}
114
-		if err == unix.EAGAIN {
114
+		if err == unix.EAGAIN || err == unix.EINTR {
115 115
 			continue
116 116
 		}
117 117
 		return false
... ...
@@ -205,28 +216,16 @@ func getEnabled() bool {
205 205
 }
206 206
 
207 207
 func readConfig(target string) string {
208
-	var (
209
-		val, key string
210
-		bufin    *bufio.Reader
211
-	)
212
-
213 208
 	in, err := os.Open(selinuxConfig)
214 209
 	if err != nil {
215 210
 		return ""
216 211
 	}
217 212
 	defer in.Close()
218 213
 
219
-	bufin = bufio.NewReader(in)
214
+	scanner := bufio.NewScanner(in)
220 215
 
221
-	for done := false; !done; {
222
-		var line string
223
-		if line, err = bufin.ReadString('\n'); err != nil {
224
-			if err != io.EOF {
225
-				return ""
226
-			}
227
-			done = true
228
-		}
229
-		line = strings.TrimSpace(line)
216
+	for scanner.Scan() {
217
+		line := strings.TrimSpace(scanner.Text())
230 218
 		if len(line) == 0 {
231 219
 			// Skip blank lines
232 220
 			continue
... ...
@@ -236,7 +235,7 @@ func readConfig(target string) string {
236 236
 			continue
237 237
 		}
238 238
 		if groups := assignRegex.FindStringSubmatch(line); groups != nil {
239
-			key, val = strings.TrimSpace(groups[1]), strings.TrimSpace(groups[2])
239
+			key, val := strings.TrimSpace(groups[1]), strings.TrimSpace(groups[2])
240 240
 			if key == target {
241 241
 				return strings.Trim(val, "\"")
242 242
 			}
... ...
@@ -245,15 +244,17 @@ func readConfig(target string) string {
245 245
 	return ""
246 246
 }
247 247
 
248
-func getSELinuxPolicyRoot() string {
249
-	return filepath.Join(selinuxDir, readConfig(selinuxTypeTag))
250
-}
251
-
252 248
 func isProcHandle(fh *os.File) error {
253 249
 	var buf unix.Statfs_t
254
-	err := unix.Fstatfs(int(fh.Fd()), &buf)
255
-	if err != nil {
256
-		return errors.Wrapf(err, "statfs(%q) failed", fh.Name())
250
+
251
+	for {
252
+		err := unix.Fstatfs(int(fh.Fd()), &buf)
253
+		if err == nil {
254
+			break
255
+		}
256
+		if err != unix.EINTR {
257
+			return errors.Wrapf(err, "statfs(%q) failed", fh.Name())
258
+		}
257 259
 	}
258 260
 	if buf.Type != unix.PROC_SUPER_MAGIC {
259 261
 		return errors.Errorf("file %q is not on procfs", fh.Name())
... ...
@@ -307,9 +308,16 @@ func setFileLabel(fpath string, label string) error {
307 307
 	if fpath == "" {
308 308
 		return ErrEmptyPath
309 309
 	}
310
-	if err := unix.Lsetxattr(fpath, xattrNameSelinux, []byte(label), 0); err != nil {
311
-		return errors.Wrapf(err, "failed to set file label on %s", fpath)
310
+	for {
311
+		err := unix.Lsetxattr(fpath, xattrNameSelinux, []byte(label), 0)
312
+		if err == nil {
313
+			break
314
+		}
315
+		if err != unix.EINTR {
316
+			return errors.Wrapf(err, "failed to set file label on %s", fpath)
317
+		}
312 318
 	}
319
+
313 320
 	return nil
314 321
 }
315 322
 
... ...
@@ -751,7 +759,7 @@ func reserveLabel(label string) {
751 751
 	if len(label) != 0 {
752 752
 		con := strings.SplitN(label, ":", 4)
753 753
 		if len(con) > 3 {
754
-			mcsAdd(con[3])
754
+			_ = mcsAdd(con[3])
755 755
 		}
756 756
 	}
757 757
 }
... ...
@@ -828,11 +836,11 @@ func intToMcs(id int, catRange uint32) string {
828 828
 	}
829 829
 
830 830
 	for ORD > TIER {
831
-		ORD = ORD - TIER
831
+		ORD -= TIER
832 832
 		TIER--
833 833
 	}
834 834
 	TIER = SETSIZE - TIER
835
-	ORD = ORD + TIER
835
+	ORD += TIER
836 836
 	return fmt.Sprintf("s0:c%d,c%d", TIER, ORD)
837 837
 }
838 838
 
... ...
@@ -844,16 +852,14 @@ func uniqMcs(catRange uint32) string {
844 844
 	)
845 845
 
846 846
 	for {
847
-		binary.Read(rand.Reader, binary.LittleEndian, &n)
847
+		_ = binary.Read(rand.Reader, binary.LittleEndian, &n)
848 848
 		c1 = n % catRange
849
-		binary.Read(rand.Reader, binary.LittleEndian, &n)
849
+		_ = binary.Read(rand.Reader, binary.LittleEndian, &n)
850 850
 		c2 = n % catRange
851 851
 		if c1 == c2 {
852 852
 			continue
853
-		} else {
854
-			if c1 > c2 {
855
-				c1, c2 = c2, c1
856
-			}
853
+		} else if c1 > c2 {
854
+			c1, c2 = c2, c1
857 855
 		}
858 856
 		mcs = fmt.Sprintf("s0:c%d,c%d", c1, c2)
859 857
 		if err := mcsAdd(mcs); err != nil {
... ...
@@ -884,18 +890,13 @@ func openContextFile() (*os.File, error) {
884 884
 	if f, err := os.Open(contextFile); err == nil {
885 885
 		return f, nil
886 886
 	}
887
-	lxcPath := filepath.Join(getSELinuxPolicyRoot(), "/contexts/lxc_contexts")
887
+	lxcPath := filepath.Join(policyRoot, "/contexts/lxc_contexts")
888 888
 	return os.Open(lxcPath)
889 889
 }
890 890
 
891 891
 var labels = loadLabels()
892 892
 
893 893
 func loadLabels() map[string]string {
894
-	var (
895
-		val, key string
896
-		bufin    *bufio.Reader
897
-	)
898
-
899 894
 	labels := make(map[string]string)
900 895
 	in, err := openContextFile()
901 896
 	if err != nil {
... ...
@@ -903,18 +904,10 @@ func loadLabels() map[string]string {
903 903
 	}
904 904
 	defer in.Close()
905 905
 
906
-	bufin = bufio.NewReader(in)
906
+	scanner := bufio.NewScanner(in)
907 907
 
908
-	for done := false; !done; {
909
-		var line string
910
-		if line, err = bufin.ReadString('\n'); err != nil {
911
-			if err == io.EOF {
912
-				done = true
913
-			} else {
914
-				break
915
-			}
916
-		}
917
-		line = strings.TrimSpace(line)
908
+	for scanner.Scan() {
909
+		line := strings.TrimSpace(scanner.Text())
918 910
 		if len(line) == 0 {
919 911
 			// Skip blank lines
920 912
 			continue
... ...
@@ -924,7 +917,7 @@ func loadLabels() map[string]string {
924 924
 			continue
925 925
 		}
926 926
 		if groups := assignRegex.FindStringSubmatch(line); groups != nil {
927
-			key, val = strings.TrimSpace(groups[1]), strings.TrimSpace(groups[2])
927
+			key, val := strings.TrimSpace(groups[1]), strings.TrimSpace(groups[2])
928 928
 			labels[key] = strings.Trim(val, "\"")
929 929
 		}
930 930
 	}
... ...
@@ -1015,7 +1008,7 @@ func copyLevel(src, dest string) (string, error) {
1015 1015
 		return "", err
1016 1016
 	}
1017 1017
 	mcsDelete(tcon["level"])
1018
-	mcsAdd(scon["level"])
1018
+	_ = mcsAdd(scon["level"])
1019 1019
 	tcon["level"] = scon["level"]
1020 1020
 	return tcon.Get(), nil
1021 1021
 }
... ...
@@ -1095,3 +1088,124 @@ func dupSecOpt(src string) ([]string, error) {
1095 1095
 func disableSecOpt() []string {
1096 1096
 	return []string{"disable"}
1097 1097
 }
1098
+
1099
+// findUserInContext scans the reader for a valid SELinux context
1100
+// match that is verified with the verifier. Invalid contexts are
1101
+// skipped. It returns a matched context or an empty string if no
1102
+// match is found. If a scanner error occurs, it is returned.
1103
+func findUserInContext(context Context, r io.Reader, verifier func(string) error) (string, error) {
1104
+	fromRole := context["role"]
1105
+	fromType := context["type"]
1106
+	scanner := bufio.NewScanner(r)
1107
+
1108
+	for scanner.Scan() {
1109
+		fromConns := strings.Fields(scanner.Text())
1110
+		if len(fromConns) == 0 {
1111
+			// Skip blank lines
1112
+			continue
1113
+		}
1114
+
1115
+		line := fromConns[0]
1116
+
1117
+		if line[0] == ';' || line[0] == '#' {
1118
+			// Skip comments
1119
+			continue
1120
+		}
1121
+
1122
+		// user context files contexts are formatted as
1123
+		// role_r:type_t:s0 where the user is missing.
1124
+		lineArr := strings.SplitN(line, ":", 4)
1125
+		// skip context with typo, or role and type do not match
1126
+		if len(lineArr) != 3 ||
1127
+			lineArr[0] != fromRole ||
1128
+			lineArr[1] != fromType {
1129
+			continue
1130
+		}
1131
+
1132
+		for _, cc := range fromConns[1:] {
1133
+			toConns := strings.SplitN(cc, ":", 4)
1134
+			if len(toConns) != 3 {
1135
+				continue
1136
+			}
1137
+
1138
+			context["role"] = toConns[0]
1139
+			context["type"] = toConns[1]
1140
+
1141
+			outConn := context.get()
1142
+			if err := verifier(outConn); err != nil {
1143
+				continue
1144
+			}
1145
+
1146
+			return outConn, nil
1147
+		}
1148
+	}
1149
+
1150
+	if err := scanner.Err(); err != nil {
1151
+		return "", errors.Wrap(err, "failed to scan for context")
1152
+	}
1153
+
1154
+	return "", nil
1155
+}
1156
+
1157
+func getDefaultContextFromReaders(c *defaultSECtx) (string, error) {
1158
+	if c.verifier == nil {
1159
+		return "", ErrVerifierNil
1160
+	}
1161
+
1162
+	context, err := newContext(c.scon)
1163
+	if err != nil {
1164
+		return "", errors.Wrapf(err, "failed to create label for %s", c.scon)
1165
+	}
1166
+
1167
+	// set so the verifier validates the matched context with the provided user and level.
1168
+	context["user"] = c.user
1169
+	context["level"] = c.level
1170
+
1171
+	conn, err := findUserInContext(context, c.userRdr, c.verifier)
1172
+	if err != nil {
1173
+		return "", err
1174
+	}
1175
+
1176
+	if conn != "" {
1177
+		return conn, nil
1178
+	}
1179
+
1180
+	conn, err = findUserInContext(context, c.defaultRdr, c.verifier)
1181
+	if err != nil {
1182
+		return "", err
1183
+	}
1184
+
1185
+	if conn != "" {
1186
+		return conn, nil
1187
+	}
1188
+
1189
+	return "", errors.Wrapf(ErrContextMissing, "context not found: %q", c.scon)
1190
+}
1191
+
1192
+func getDefaultContextWithLevel(user, level, scon string) (string, error) {
1193
+	userPath := filepath.Join(policyRoot, selinuxUsersDir, user)
1194
+	defaultPath := filepath.Join(policyRoot, defaultContexts)
1195
+
1196
+	fu, err := os.Open(userPath)
1197
+	if err != nil {
1198
+		return "", err
1199
+	}
1200
+	defer fu.Close()
1201
+
1202
+	fd, err := os.Open(defaultPath)
1203
+	if err != nil {
1204
+		return "", err
1205
+	}
1206
+	defer fd.Close()
1207
+
1208
+	c := defaultSECtx{
1209
+		user:       user,
1210
+		level:      level,
1211
+		scon:       scon,
1212
+		userRdr:    fu,
1213
+		defaultRdr: fd,
1214
+		verifier:   securityCheckContext,
1215
+	}
1216
+
1217
+	return getDefaultContextFromReaders(&c)
1218
+}
... ...
@@ -146,3 +146,7 @@ func dupSecOpt(src string) ([]string, error) {
146 146
 func disableSecOpt() []string {
147 147
 	return []string{"disable"}
148 148
 }
149
+
150
+func getDefaultContextWithLevel(user, level, scon string) (string, error) {
151
+	return "", nil
152
+}
... ...
@@ -6,21 +6,21 @@ import (
6 6
 	"golang.org/x/sys/unix"
7 7
 )
8 8
 
9
-// Returns a []byte slice if the xattr is set and nil otherwise
10
-// Requires path and its attribute as arguments
11
-func lgetxattr(path string, attr string) ([]byte, error) {
9
+// lgetxattr returns a []byte slice containing the value of
10
+// an extended attribute attr set for path.
11
+func lgetxattr(path, attr string) ([]byte, error) {
12 12
 	// Start with a 128 length byte array
13 13
 	dest := make([]byte, 128)
14
-	sz, errno := unix.Lgetxattr(path, attr, dest)
14
+	sz, errno := doLgetxattr(path, attr, dest)
15 15
 	for errno == unix.ERANGE {
16 16
 		// Buffer too small, use zero-sized buffer to get the actual size
17
-		sz, errno = unix.Lgetxattr(path, attr, []byte{})
17
+		sz, errno = doLgetxattr(path, attr, []byte{})
18 18
 		if errno != nil {
19 19
 			return nil, errno
20 20
 		}
21 21
 
22 22
 		dest = make([]byte, sz)
23
-		sz, errno = unix.Lgetxattr(path, attr, dest)
23
+		sz, errno = doLgetxattr(path, attr, dest)
24 24
 	}
25 25
 	if errno != nil {
26 26
 		return nil, errno
... ...
@@ -28,3 +28,13 @@ func lgetxattr(path string, attr string) ([]byte, error) {
28 28
 
29 29
 	return dest[:sz], nil
30 30
 }
31
+
32
+// doLgetxattr is a wrapper that retries on EINTR
33
+func doLgetxattr(path, attr string, dest []byte) (int, error) {
34
+	for {
35
+		sz, err := unix.Lgetxattr(path, attr, dest)
36
+		if err != unix.EINTR {
37
+			return sz, err
38
+		}
39
+	}
40
+}
... ...
@@ -4,6 +4,6 @@ go 1.13
4 4
 
5 5
 require (
6 6
 	github.com/pkg/errors v0.9.1
7
-	github.com/willf/bitset v1.1.11-0.20200630133818-d5bec3311243
7
+	github.com/willf/bitset v1.1.11
8 8
 	golang.org/x/sys v0.0.0-20191115151921-52ab43148777
9 9
 )
... ...
@@ -20,17 +20,16 @@ type WalkFunc = filepath.WalkFunc
20 20
 //
21 21
 // Note that this implementation only supports primitive error handling:
22 22
 //
23
-// * no errors are ever passed to WalkFn
23
+// - no errors are ever passed to WalkFn;
24 24
 //
25
-// * once a walkFn returns any error, all further processing stops
26
-//   and the error is returned to the caller of Walk;
25
+// - once a walkFn returns any error, all further processing stops
26
+// and the error is returned to the caller of Walk;
27 27
 //
28
-// * filepath.SkipDir is not supported;
29
-//
30
-// * if more than one walkFn instance will return an error, only one
31
-//   of such errors will be propagated and returned by Walk, others
32
-//   will be silently discarded.
28
+// - filepath.SkipDir is not supported;
33 29
 //
30
+// - if more than one walkFn instance will return an error, only one
31
+// of such errors will be propagated and returned by Walk, others
32
+// will be silently discarded.
34 33
 func Walk(root string, walkFn WalkFunc) error {
35 34
 	return WalkN(root, walkFn, runtime.NumCPU()*2)
36 35
 }
... ...
@@ -38,6 +37,8 @@ func Walk(root string, walkFn WalkFunc) error {
38 38
 // WalkN is a wrapper for filepath.Walk which can call multiple walkFn
39 39
 // in parallel, allowing to handle each item concurrently. A maximum of
40 40
 // num walkFn will be called at any one time.
41
+//
42
+// Please see Walk documentation for caveats of using this function.
41 43
 func WalkN(root string, walkFn WalkFunc, num int) error {
42 44
 	// make sure limit is sensible
43 45
 	if num < 1 {