Bug 241559 - cat(1) endless loop when writing to special device file
Summary: cat(1) endless loop when writing to special device file
Status: New
Alias: None
Product: Base System
Classification: Unclassified
Component: bin (show other bugs)
Version: 12.1-RELEASE
Hardware: Any Any
: --- Affects Only Me
Assignee: freebsd-bugs (Nobody)
URL:
Keywords:
Depends on:
Blocks:
 
Reported: 2019-10-29 01:43 UTC by sigsys
Modified: 2019-10-29 02:51 UTC (History)
0 users

See Also:


Attachments

Note You need to log in before you can comment on or make changes to this bug.
Description sigsys 2019-10-29 01:43:10 UTC
Using cat to write to a disk directly loops forever when it reaches the end of the disk and there's still data to write (like when using /dev/zero or /dev/random for example).  write() returns 0 (without setting errno) when that happens (one of the very rare cases where that can happen it seems) and cat handles this by retrying the write forever.  It would be better to error out in this case AFAIK.  There must be tons of programs that react very poorly when making them write to a device file directly like that, but one might expect that cat always work (more or less).

$ mdconfig -s 1m -u 10
$ cat /dev/zero > /dev/md10 # loops forever

cp seems to handle it better:

$ cp /dev/zero /dev/md10
cp: /dev/md10: No error: 0

Which isn't a very good error message but still better since it doesn't loop forever.

With this change:

Index: bin/cat/cat.c
===================================================================
--- bin/cat/cat.c	(revision 354128)
+++ bin/cat/cat.c	(working copy)
@@ -327,7 +327,7 @@
 	}
 	while ((nr = read(rfd, buf, bsize)) > 0)
 		for (off = 0; nr; nr -= nw, off += nw)
-			if ((nw = write(wfd, buf + off, (size_t)nr)) < 0)
+			if ((nw = write(wfd, buf + off, (size_t)nr)) <= 0)
 				err(1, "stdout");
 	if (nr < 0) {
 		warn("%s", filename);

It errors out like cp instead:

$ cat /dev/zero > /dev/md10
cat: stdout: No error: 0



That's when cat is in "raw mode".  In "cooked mode", it already errors out, but it picks up a bogus errno for it:

$ cat -v /dev/zero > /dev/md10
cat: stdout: Inappropriate ioctl for device

Which comes from an isatty() call in stdio.  errno should be saved at some point, dunno if it should be done by cat or stdio (before its call to isatty() in __smakebuf()).  Not very important though I guess.
Comment 1 Conrad Meyer freebsd_committer 2019-10-29 02:09:04 UTC
Unfortunately write() => 0 has no special semantics in POSIX or FreeBSD.  It just means partial progress / no error.  (So I don't think the proposed change to cat is correct.)  This interacts poorly with block devices in FreeBSD, which may truncate IO silently at end of device so long as the offset is in-bounds.
Comment 2 sigsys 2019-10-29 02:51:56 UTC
Isn't it still less bad with this change?  It still prints an error message if it gets a write returning 0.  That way you know it had to stop before it wrote everything.

From what I found, POSIX says that write() must never return 0 (and that historically that's something that could happen with non-blocking writes, but now it must return an EWOUDLBLOCK error instead).  But special files are special and it's undefined there.

Are you saying that cat could get a write returning 0 *before* it reaches the end of the disk, depending on how the I/O is aligned?  And that continuing to try to write could get more data on the disk (even if it'll keep trying to write forever at the end)?  Checking for a write of 0 seems to be how dd detects the end of the disk, but then again it is being careful to do aligned I/O.