[ I'm not subscribed to the list, so please keep me in Cc ]

Greetings tar users and developers,

For the past 2 days I've been hunting a tar-related failure that I could only
reproduce by running dpkg-source, running the tar incantation dpkg-source uses
internally on its own doesn't appear to work. I'll document my debugging
endeavour here to make sure I have my facts straight, as well as to help anyone
else stumbling onto this.

The error is as follows:

$ dpkg-source -b .
dpkg-source: info: using source format '3.0 (quilt)'
dpkg-source: info: building newsboat using existing 
./newsboat_2.20.1.orig.tar.xz
dpkg-source: info: building newsboat using existing 
./newsboat_2.20.1.orig.tar.xz.asc
dpkg-source: info: building newsboat in newsboat_2.20.1-1.debian.tar.xz
tar: debian/changelog: File shrank by 36 bytes; padding with zeros
dpkg-source: error: tar -cf - subprocess returned exit status 1

On further investigation, this bug is triggered if all of the following
conditions are met (on my system):

1. Have a changelog file >4096 bytes
2. The build directory should be in a 9p filesystem (in a VM)
3. Kernel version should be >=5.6 (it doesn't error on 4.19)
4. Do the exact combination of steps dpkg-source does (running the tar command 
directly
doesn't appear to cause it)

Condition 3 threw me off and I started looking into the kernel as the source of
this, however, I now believe it's some change in the 9p driver that simply
uncovered it.

$ strace -f dpkg-source -b .
[...]
72606 execve("/home/nikos/tar-1.32/src//tar", ["tar", "-cf", "-", 
"--format=gnu", "--sort=name", "--mtime", "@1595708407", "--clamp-mtime", 
"--null", "--numeric-owner", "--owner=0", "--group=0", "--exclude=*.a", 
"--exclude=*.la", "--exclude=*.o", "--exclude=*.so", "--exclude=.*.sw?", 
"--exclude=*/*~", "--exclude=,,*", "--exclude=.[#~]*", "--exclude=.arch-ids", 
"--exclude=.arch-inventory", "--exclude=.be", "--exclude=.bzr", 
"--exclude=.bzr.backup", "--exclude=.bzr.tags", "--exclude=.bzrignore", 
"--exclude=.cvsignore", "--exclude=.deps", "--exclude=.git", 
"--exclude=.gitattributes", "--exclude=.gitignore", ...], 0x55b9e7b2b4d0 /* 22 
vars */ <unfinished ...>
[...]
72606 newfstatat(3, "changelog", {st_mode=S_IFREG|0644, st_size=4132, ...}, 
AT_SYMLINK_NOFOLLOW) = 0
72606 openat(3, "changelog", O_RDONLY|O_NOCTTY|O_NONBLOCK|O_NOFOLLOW|O_CLOEXEC) 
= 4
72606 fstat(4, {st_mode=S_IFREG|0644, st_size=4132, ...}) = 0
72606 read(4, "newsboat (2.20.1-1) UNRELEASED; "..., 4132) = 4096
72606 write(2, "tar: ", 5)              = 5
72606 write(2, "debian/changelog: File shrank by"..., 61) = 61
72606 write(2, "\n", 1)                 = 1
72606 close(4)                          = 0
[...]

The read() call returns less bytes than requested and then tar assumed it was
truncated and issues a warning. I traced this call to create.c:dump_regular_file
which indeed doesn't seem to handle this possibility.

Attached are two patches, the first replaces the safe_read call in 
blocking_read with
full_read from gnulib, I have verified that this change fixes the bug.

I can't seem to understand what's the point of distinguishing between
safe_{read,write} and full_{read,write}, is there a situation where not handling
short read/writes would be important? Why not merge these functions?

So finally, since I couldn't find any reason not to, the second patch replaces
all instances of safe_read with full_read to avoid such problems in other areas
of the code. (note: Without much verification!)
>From b5a8b6a29a11cded297f18095d522b87f8aee406 Mon Sep 17 00:00:00 2001
From: Nikos Tsipinakis <ni...@tsipinakis.com>
Date: Sun, 26 Jul 2020 23:32:12 +0300
Subject: [PATCH] Use full_read to avoid errors due to short reads

read(2) can return any number of bytes short of the requested without
raising an error or being interrupted (which were already
checked/handled), however, in that case, tar did not attempt to read the
remaining bytes and raised errors.
---
 gnulib.modules | 1 +
 src/common.h   | 1 +
 src/misc.c     | 4 ++--
 3 files changed, 4 insertions(+), 2 deletions(-)

diff --git a/gnulib.modules b/gnulib.modules
index 82f5e1c..9ded7da 100644
--- a/gnulib.modules
+++ b/gnulib.modules
@@ -44,6 +44,7 @@ fprintftime
 fseeko
 fstatat
 full-write
+full-read
 futimens
 getline
 getopt-gnu
diff --git a/src/common.h b/src/common.h
index a451999..7f696c5 100644
--- a/src/common.h
+++ b/src/common.h
@@ -57,6 +57,7 @@
 #include "arith.h"
 #include <backupfile.h>
 #include <exclude.h>
+#include <full-read.h>
 #include <full-write.h>
 #include <modechange.h>
 #include <quote.h>
diff --git a/src/misc.c b/src/misc.c
index d833b8d..81e2a96 100644
--- a/src/misc.c
+++ b/src/misc.c
@@ -804,7 +804,7 @@ deref_stat (char const *name, struct stat *buf)
 size_t
 blocking_read (int fd, void *buf, size_t count)
 {
-  size_t bytes = safe_read (fd, buf, count);
+  size_t bytes = full_read (fd, buf, count);
 
 #if defined F_SETFL && O_NONBLOCK
   if (bytes == SAFE_READ_ERROR && errno == EAGAIN)
@@ -812,7 +812,7 @@ blocking_read (int fd, void *buf, size_t count)
       int flags = fcntl (fd, F_GETFL);
       if (0 <= flags && flags & O_NONBLOCK
 	  && fcntl (fd, F_SETFL, flags & ~O_NONBLOCK) != -1)
-	bytes = safe_read (fd, buf, count);
+	bytes = full_read (fd, buf, count);
     }
 #endif
 
-- 
2.28.0.rc2

>From a8f4ef2723fc29ac8f3135a157b234c9f61ce704 Mon Sep 17 00:00:00 2001
From: Nikos Tsipinakis <ni...@tsipinakis.com>
Date: Mon, 27 Jul 2020 01:06:30 +0300
Subject: [PATCH 2/2] Remove remaining usages of safe_read

Continuation of the previous commit, remove all remaining usages of
safe_read to avoid errors due to short reads, and instead replace it
with full_read which handles them gracefully.
---
 src/sparse.c | 6 +++---
 src/system.c | 2 +-
 src/update.c | 2 +-
 3 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/src/sparse.c b/src/sparse.c
index cc3c515..39e1627 100644
--- a/src/sparse.c
+++ b/src/sparse.c
@@ -417,7 +417,7 @@ sparse_dump_region (struct tar_sparse_file *file, size_t i)
       size_t bytes_read;
 
       blk = find_next_block ();
-      bytes_read = safe_read (file->fd, blk->buffer, bufsize);
+      bytes_read = full_read (file->fd, blk->buffer, bufsize);
       if (bytes_read == SAFE_READ_ERROR)
 	{
           read_diag_details (file->stat_info->orig_file_name,
@@ -614,7 +614,7 @@ check_sparse_region (struct tar_sparse_file *file, off_t beg, off_t end)
       size_t rdsize = BLOCKSIZE < end - beg ? BLOCKSIZE : end - beg;
       char diff_buffer[BLOCKSIZE];
 
-      bytes_read = safe_read (file->fd, diff_buffer, rdsize);
+      bytes_read = full_read (file->fd, diff_buffer, rdsize);
       if (bytes_read == SAFE_READ_ERROR)
 	{
           read_diag_details (file->stat_info->orig_file_name,
@@ -667,7 +667,7 @@ check_data_region (struct tar_sparse_file *file, size_t i)
 	}
       set_next_block_after (blk);
       file->dumped_size += BLOCKSIZE;      
-      bytes_read = safe_read (file->fd, diff_buffer, rdsize);
+      bytes_read = full_read (file->fd, diff_buffer, rdsize);
       if (bytes_read == SAFE_READ_ERROR)
 	{
           read_diag_details (file->stat_info->orig_file_name,
diff --git a/src/system.c b/src/system.c
index f90e47d..5b93228 100644
--- a/src/system.c
+++ b/src/system.c
@@ -431,7 +431,7 @@ sys_child_open_for_compress (void)
 	{
 	  size_t size = record_size - length;
 
-	  status = safe_read (STDIN_FILENO, cursor, size);
+	  status = full_read (STDIN_FILENO, cursor, size);
 	  if (status == SAFE_READ_ERROR)
 	    read_fatal (use_compress_program_option);
 	  if (status == 0)
diff --git a/src/update.c b/src/update.c
index 511df18..b1add03 100644
--- a/src/update.c
+++ b/src/update.c
@@ -77,7 +77,7 @@ append_file (char *file_name)
 		memset (start->buffer + bytes_left, 0, BLOCKSIZE - status);
 	    }
 
-	  status = safe_read (handle, start->buffer, buffer_size);
+	  status = full_read (handle, start->buffer, buffer_size);
 	  if (status == SAFE_READ_ERROR)
 	    read_fatal_details (file_name, stat_data.st_size - bytes_left,
 				buffer_size);
-- 
2.28.0.rc2

Reply via email to