#576

&> and &>> to /dev/stdout or /dev/stderr write a regular file instead of the stream

OpenHazzngopened 17d ago

Summary

&> /dev/stdout, &> /dev/stderr, &>> /dev/stdout and &>> /dev/stderr write both streams to a regular file at that path instead of to the stream. #562 made /dev/null a device for every operator. > and 2> already treat /dev/stdout and /dev/stderr as streams, but the &>/&>> case in applyRedirections handles only /dev/null and sends everything else to a file sink.

The symptoms depend on the filesystem:

  • Default InMemoryFs: the output disappears. It lands in a regular file named /dev/stdout or /dev/stderr, and later &>> redirects keep appending to it.
  • A filesystem that initFilesystem can't seed (async-only, no /dev): bash.exec() rejects with ENOENT: no such file or directory, '/dev'. That is the same failure #558 fixed for /dev/null.

Repro (main at 3ebd1c2, after #561 and #562)

The async-only filesystem is InMemoryFs behind a proxy that hides the *Sync methods, so /dev is never created, and whose writeFile/appendFile throw ENOENT when the parent directory is missing, like a disk- or database-backed IFileSystem.

Scriptdefault InMemoryFsasync-only fsbash 5.1
{ echo out; echo err >&2; } &> /dev/stdout; echo after rc=$?stdout after rc=0, out/err lostexec() rejects: ENOENT ... '/dev'stdout out err after rc=0
{ echo out; echo err >&2; } &> /dev/stderr; echo after rc=$?stdout after rc=0, stderr emptyrejectsstdout after rc=0, stderr out err
ls /nope &>> /dev/stdout; echo after rc=$?stdout after rc=2, ls error lostrejectsstdout ls: ... No such file or directory after rc=2
echo g &>> /dev/stderr; echo after rc=$?stdout after rc=0, stderr emptyrejectsstdout after rc=0, stderr g
echo x > /dev/stdout; echo after rc=$?x after rc=0x after rc=0same
echo x &> /dev/null; echo after rc=$?after rc=0after rc=0same

The last two rows are controls: > to a stream and &> to /dev/null already work.

Where the output goes on the default filesystem:

const b = new Bash();
await b.exec("echo one &> /dev/stdout; echo two &>> /dev/stderr");
const r = await b.exec("cat /dev/stdout; echo ---; cat /dev/stderr; echo ---; ls -l /dev");
// one
// ---
// two
// ---
// -rw-r--r-- 1 user user     4 ... stderr
// -rw-r--r-- 1 user user     4 ... stdout

Where

packages/just-bash/src/interpreter/redirections.ts at 3ebd1c2:

  • L1233–L1257: for >, >| and >>, /dev/stdout and /dev/stderr re-point the fd to the other fd's current sink, or do nothing when the target is the fd's own stream.
  • L1303–L1314: the &>/&>> case handles filePath === DEV_NULL and builds { kind: "file", path: filePath } for everything else, including the two stream paths.

Suggested fix

Mirror the > handling in the &>/&>> case:

  • /dev/stdout: both fds take fd 1's current sink (fd2Sink = fd1Sink).
  • /dev/stderr: both fds take fd 2's current sink (fd1Sink = fd2Sink).
  • /dev/null: discard, as now.
  • anything else: a file sink, as now.

We carry exactly this as a local patch over 3.6.0 (a two-line change to that case), with tests for &> and &>> to both streams, the exit status, no filesystem writes for the device paths, and an unchanged regular-file &>.

Impact for us

We serve just-bash sandboxes to AI agents over HTTP on top of a Postgres-backed IFileSystem that is async-only. cmd &> /dev/stderr shows up in agent-written scripts and in scripts from the repos they clone. Without the patch, each one fails the whole request and rolls back the script's file writes. With #562 merged, these are the only stream redirects we still have to patch.

Activity

0

No activity yet.