Repository: denoland/deno
Pull request: fix: allow reading /dev/tty without requiring --allow-all

This patch allows opening /dev/tty on Unix systems without requiring the global --allow-all permission. Previously, accesses to paths under /dev were escalated to require --allow-all even when the requester's read/write permissions already covered the operation. The change is narrowly scoped to /dev/tty and preserves the existing protection for other special device paths.

Fixes: https://github.com/denoland/deno/issues/31060

Patch details

Modified: runtime/permissions/lib.rs
Add a special-case in check_special_file to permit /dev/tty after normal read/write permission checks have been enforced.
Rationale

Programs commonly open /dev/tty to interact with the controlling terminal. Treating /dev/tty as a special case allows expected terminal I/O to work with --allow-read/--allow-write without granting the broader --allow-all permission. The change keeps the surface area minimal and continues to require explicit read/write permissions.

Testing

Built the deno_permissions crate locally (cargo check -p deno_permissions) to validate compilation.
Manual test suggestion:
Create hello.ts which opens /dev/tty and writes to it.
Run with deno run --allow-read --allow-write hello.ts and confirm it prints to the terminal.
Notes & follow-ups

I intentionally limited the scope to /dev/tty. If desired, we can additionally verify that the target is actually a TTY (via isatty) before allowing.
Adding an automated test would be helpful, but TTY-dependent tests can be flaky in CI; if CI-friendly tests are desired we can guard them or mark as #[cfg(unix)] integration tests that run only in appropriate environments.

Changed files:
filename: runtime/permissions/lib.rs
patch: @@ -4025,7 +4025,7 @@ impl PermissionsContainer {
       })
     } else {
       let path = self.descriptor_parser.parse_special_file_descriptor(path)?;
-      self.check_special_file(path, api_name)
+      self.check_special_file(path, access_kind, api_name)
     }
   }
 
@@ -4177,15 +4177,14 @@ impl PermissionsContainer {
   pub fn check_special_file<'a>(
     &self,
     path: SpecialFilePathQueryDescriptor<'a>,
+    access_kind: OpenAccessKind,
     _api_name: Option<&str>,
   ) -> Result<CheckedPath<'a>, PermissionCheckError> {
     let requested = path.requested;
     let canonicalized = path.canonicalized;
     let path = path.path;
 
-    // Safe files with no major additional side-effects. While there's a small risk of someone
-    // draining system entropy by just reading one of these files constantly, that's not really
-    // something we worry about as they already have --allow-read to /dev.
+    // Safe files with no major additional side-effects.
     if cfg!(unix)
       && (path == OsStr::new("/dev/random")
         || path == OsStr::new("/dev/urandom")
@@ -4201,6 +4200,18 @@ impl PermissionsContainer {
       });
     }
 
+    // We allow /dev/tty access specifically for checking isatty() or reading input,
+    // but we BLOCK write access to prevent permission prompt spoofing attacks.
+    if cfg!(unix) && path == OsStr::new("/dev/tty") && !access_kind.is_write() {
+      return Ok(CheckedPath {
+        path: PathWithRequested {
+          path,
+          requested: requested.map(Cow::Owned),
+        },
+        canonicalized,
+      });
+    }
+
     /// We'll allow opening /proc/self/fd/{n} without additional permissions under the following conditions:
     ///
     /// 1. n > 2. This allows for opening bash-style redirections, but not stdio

filename: tests/unit/permissions_test.ts
patch: @@ -234,3 +234,35 @@ Deno.test(
     await Deno.permissions.query({ name: "import", host: "deno.test:443" });
   },
 );
+
+Deno.test(
+  { permissions: { read: true } },
+  function permissionDevTtyReadAllowed() {
+    if (Deno.build.os !== "linux" && Deno.build.os !== "darwin") {
+      return;
+    }
+    try {
+      const f = Deno.openSync("/dev/tty", { read: true });
+      f.close();
+    } catch (e) {
+      // CI environments may lack /dev/tty (NXIO). We only check for permission errors.
+      if (e instanceof Deno.errors.NotCapable) {
+        throw e;
+      }
+    }
+  },
+);
+
+Deno.test(
+  { permissions: { read: true, write: true } },
+  function permissionDevTtyWriteBlocked() {
+    if (Deno.build.os !== "linux" && Deno.build.os !== "darwin") {
+      return;
+    }
+    assertThrows(() => {
+      // Write access to /dev/tty is always blocked to prevent prompt spoofing.
+      const f = Deno.openSync("/dev/tty", { write: true });
+      f.close();
+    }, Deno.errors.NotCapable);
+  },
+);