sarutak commented on code in PR #59061:
URL: https://github.com/apache/spark/pull/59061#discussion_r4281977394


##########
core/src/main/scala/org/apache/spark/util/Utils.scala:
##########
@@ -223,19 +224,52 @@ private[spark] object Utils
     writeByteBufferImpl(bb, out.write)
   }
 
+  // fromString returns a mutable HashSet; wrap it so no future caller can 
mutate the shared set.
+  private val chmod700Perms =
+    Collections.unmodifiableSet(PosixFilePermissions.fromString("rwx------"))
+
   /**
    * JDK equivalent of `chmod 700 file`.
    *
+   * On POSIX filesystems this is a single `chmod` call, which is atomic: 
concurrent calls on
+   * the same path (e.g. Workers sharing a local root dir in local-cluster 
mode) cannot
+   * interleave into a broken permission set, unlike the `File.set*` fallback 
below, each call
+   * of which is a non-atomic stat-then-chmod.
+   *
    * @param file the file whose permissions will be modified
    * @return true if the permissions were successfully changed, false 
otherwise.
    */
   def chmod700(file: File): Boolean = {
-    file.setReadable(false, false) &&
-    file.setReadable(true, true) &&
-    file.setWritable(false, false) &&
-    file.setWritable(true, true) &&
-    file.setExecutable(false, false) &&
-    file.setExecutable(true, true)
+    if (file.getPath.isEmpty) {
+      // The NIO empty path resolves to the process working directory; the 
File.set* sequence
+      // returned false for it, so keep that behavior rather than chmod-ing 
the cwd.
+      return false
+    }
+    try {
+      Files.setPosixFilePermissions(file.toPath, chmod700Perms)

Review Comment:
   `setPosixFilePermissions` writes exactly the 9 rwx bits, so this now clears 
setgid/setuid/sticky. In JDK 17, [UnixFileAttributeViews.java 
L296-L299](https://github.com/openjdk/jdk17u/blob/0b592b7f04aae6cec666345be37c1456845e6e0d/src/java.base/unix/classes/sun/nio/fs/UnixFileAttributeViews.java#L296-L299)
 passes 
[UnixFileModeAttribute.toUnixMode](https://github.com/openjdk/jdk17u/blob/0b592b7f04aae6cec666345be37c1456845e6e0d/src/java.base/unix/classes/sun/nio/fs/UnixFileModeAttribute.java#L48-L65)
 (built from the 9 permission bits only) straight to `chmod` in 
[setMode](https://github.com/openjdk/jdk17u/blob/0b592b7f04aae6cec666345be37c1456845e6e0d/src/java.base/unix/classes/sun/nio/fs/UnixFileAttributeViews.java#L263-L267).
 The old `File.set*` path kept them, because each call chmods the stat'ed mode 
with one bit changed (GNU `chmod 700` also keeps setgid on a directory). 
Checked on macOS with a `2755` directory:
   
   1. Old sequence: `drwx--S---` (setgid kept)
   2. `setPosixFilePermissions`: `drwx------` (setgid cleared)
   
   This is reachable from `getOrCreateLocalRootDirsImpl`, for example when the 
`spark-*` dir is created under a setgid parent and inherits the bit. With mode 
700 the group has no access anyway, so I do not expect a practical impact. But 
it makes the Scaladoc's "JDK equivalent of `chmod 700`" and the "no user-facing 
change" statement slightly inaccurate. Could you mention it in the Scaladoc and 
the PR description? If preserving the special bits matters, `(mode & 07000) | 
0700` through `Files.setAttribute(path, "unix:mode", ...)` is still a single 
chmod, but documenting it seems sufficient to me.



##########
core/src/main/scala/org/apache/spark/util/Utils.scala:
##########
@@ -223,19 +224,52 @@ private[spark] object Utils
     writeByteBufferImpl(bb, out.write)
   }
 
+  // fromString returns a mutable HashSet; wrap it so no future caller can 
mutate the shared set.
+  private val chmod700Perms =
+    Collections.unmodifiableSet(PosixFilePermissions.fromString("rwx------"))
+
   /**
    * JDK equivalent of `chmod 700 file`.
    *
+   * On POSIX filesystems this is a single `chmod` call, which is atomic: 
concurrent calls on
+   * the same path (e.g. Workers sharing a local root dir in local-cluster 
mode) cannot
+   * interleave into a broken permission set, unlike the `File.set*` fallback 
below, each call
+   * of which is a non-atomic stat-then-chmod.
+   *
    * @param file the file whose permissions will be modified
    * @return true if the permissions were successfully changed, false 
otherwise.
    */
   def chmod700(file: File): Boolean = {
-    file.setReadable(false, false) &&
-    file.setReadable(true, true) &&
-    file.setWritable(false, false) &&
-    file.setWritable(true, true) &&
-    file.setExecutable(false, false) &&
-    file.setExecutable(true, true)
+    if (file.getPath.isEmpty) {
+      // The NIO empty path resolves to the process working directory; the 
File.set* sequence
+      // returned false for it, so keep that behavior rather than chmod-ing 
the cwd.
+      return false
+    }
+    try {
+      Files.setPosixFilePermissions(file.toPath, chmod700Perms)
+      true
+    } catch {
+      case _: UnsupportedOperationException =>
+        // Non-POSIX filesystem (e.g. Windows): best-effort, non-atomic 
fallback. Remove all
+        // permissions first so the file is locked down as early as possible, 
then grant the
+        // owner permissions; no intermediate state grants group/other access.
+        val succeeded =
+          file.setReadable(false, false) &&
+          file.setWritable(false, false) &&
+          file.setExecutable(false, false) &&
+          file.setReadable(true, true) &&
+          file.setWritable(true, true) &&
+          file.setExecutable(true, true)
+        if (!succeeded) {
+          // Structural on Windows: File.setReadable(false, false) always 
fails there (the
+          // filesystem has no read permission to revoke), so this fires on 
every call.
+          logWarning(log"Failed to set permissions to 700 on ${MDC(PATH, 
file.getAbsolutePath)}")

Review Comment:
   As the comment says, this fires on every call on Windows (the native 
`WinNTFileSystem.setPermission` returns `enable` for READ/EXECUTE, see 
[WinNTFileSystem_md.c 
L473-L476](https://github.com/openjdk/jdk17u/blob/0b592b7f04aae6cec666345be37c1456845e6e0d/src/java.base/windows/native/libjava/WinNTFileSystem_md.c#L473-L476)
 in JDK 17, so `setReadable(false, false)` is always `false`). It is hit once 
per local root dir, per executor app dir in `Worker`, per app store in 
`HistoryServerDiskManager`, and in `KVUtils`, and there is nothing the user can 
do about it. The old code was silent here. Would it make sense to log this 
structural case only once per JVM (or at DEBUG), and keep the WARN with the 
throwable for the `NonFatal` branch, which is the case my earlier comment was 
about?



##########
core/src/test/scala/org/apache/spark/util/UtilsSuite.scala:
##########
@@ -572,6 +573,73 @@ class UtilsSuite extends SparkFunSuite with 
ResetSystemProperties {
     assert(!Utils.createDirectory(scenario8))
   }
 
+  test("chmod700 leaves rwx------ under concurrent calls on the same path") {
+    // Workers in local-cluster mode share local root dirs and can chmod the 
same
+    // per-application directory concurrently; the stat-then-chmod File.set* 
sequence
+    // interleaves into broken permission sets, so chmod700 must be atomic. A 
post-join

Review Comment:
   I need to correct [my earlier 
"Agreed"](https://github.com/apache/spark/pull/59061#discussion_r4172425733) on 
the post-join discussion: the premise that the final mode is `rwx------` either 
way does not hold, so this comment is inaccurate.
   
   Each `File.set*` call is a read-modify-write: the native 
`UnixFileSystem.setPermission` stats the mode, then calls `chmod(path, mode | 
amode)` or `chmod(path, mode & ~amode)` ([UnixFileSystem_md.c 
L211-L216](https://github.com/openjdk/jdk17u/blob/0b592b7f04aae6cec666345be37c1456845e6e0d/src/java.base/unix/native/libjava/UnixFileSystem_md.c#L211-L216)
 in JDK 17). A grant computed from a stale stat can be the last write and bring 
back an earlier revoke. Example, starting at 700:
   
   1. Thread B runs `setWritable(false, false)`: 700 to 500.
   2. Thread A, in its final `setExecutable(true, true)`, stats 500 and is 
preempted before its chmod.
   3. Thread B finishes its remaining calls: the mode is back at 700.
   4. Thread A writes 500 | u+x = 500. The final mode is `r-x------`.
   
   I checked this with a standalone harness (the old six call sequence vs 
`setPosixFilePermissions`):
   - The PR's test shape on the old code failed the post-join assert at line 
620 in 4 of 5 runs, and the observer flagged intermediate states in 5 of 5 runs.
   - Post-join only, 2000 rounds of 8 threads x 50 calls on the old code: 170 
rounds ended in `-wx------`, `r-x------` or `--x------`.
   - The new code: 0 failures in all of the above.
   
   So the post-join assert is a weak but real guard, and the broken end state 
is the bug the description's "Why" section describes. The observer is still the 
right addition, since it catches the transient states far more reliably.



##########
core/src/main/scala/org/apache/spark/util/Utils.scala:
##########
@@ -223,19 +224,52 @@ private[spark] object Utils
     writeByteBufferImpl(bb, out.write)
   }
 
+  // fromString returns a mutable HashSet; wrap it so no future caller can 
mutate the shared set.
+  private val chmod700Perms =
+    Collections.unmodifiableSet(PosixFilePermissions.fromString("rwx------"))
+
   /**
    * JDK equivalent of `chmod 700 file`.
    *
+   * On POSIX filesystems this is a single `chmod` call, which is atomic: 
concurrent calls on
+   * the same path (e.g. Workers sharing a local root dir in local-cluster 
mode) cannot
+   * interleave into a broken permission set, unlike the `File.set*` fallback 
below, each call
+   * of which is a non-atomic stat-then-chmod.
+   *
    * @param file the file whose permissions will be modified
    * @return true if the permissions were successfully changed, false 
otherwise.
    */
   def chmod700(file: File): Boolean = {
-    file.setReadable(false, false) &&
-    file.setReadable(true, true) &&
-    file.setWritable(false, false) &&
-    file.setWritable(true, true) &&
-    file.setExecutable(false, false) &&
-    file.setExecutable(true, true)
+    if (file.getPath.isEmpty) {
+      // The NIO empty path resolves to the process working directory; the 
File.set* sequence
+      // returned false for it, so keep that behavior rather than chmod-ing 
the cwd.
+      return false
+    }
+    try {
+      Files.setPosixFilePermissions(file.toPath, chmod700Perms)
+      true
+    } catch {
+      case _: UnsupportedOperationException =>
+        // Non-POSIX filesystem (e.g. Windows): best-effort, non-atomic 
fallback. Remove all

Review Comment:
   On Windows, the first step `setReadable(false, false)` always fails and `&&` 
short-circuits, so nothing is stripped and none of the later steps run. That is 
what the comment at line 264 says, so these two comments contradict each other. 
Also, on a filesystem where the sequence did run, the intermediate states would 
still keep the existing group/other bits until each one is cleared; the 
accurate property is "never adds group/other bits", which the old order already 
had. Could you reword this comment and the matching sentence in the PR 
description, or keep the original order to make the diff smaller?
   



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to