Commit message: None
CVE: CVE-2018-20482
CWE: CWE-835
Vulnerability description: GNU Tar through 1.30, when --sparse is used, mishandles file shrinkage during read access, which allows local users to cause a denial of service (infinite read loop in sparse_dump_region in sparse.c) by modifying a file that is supposed to be archived by a different user's process (e.g., a system backup running as root).

Project: savannah

check_data_region (struct tar_sparse_file *file, size_t i)
{
  off_t size_left;

  if (!lseek_or_error (file, file->stat_info->sparse_map[i].offset))
    return false;
 			     rdsize);
 	  return false;
 	}

Project: savannah

check_sparse_region (struct tar_sparse_file *file, off_t beg, off_t end)
{
  if (!lseek_or_error (file, beg))
    return false;

  while (beg < end)
    {
      size_t bytes_read;
      size_t rdsize = BLOCKSIZE < end - beg ? BLOCKSIZE : end - beg;
      char diff_buffer[BLOCKSIZE];

      bytes_read = safe_read (file->fd, diff_buffer, rdsize);
      if (bytes_read == SAFE_READ_ERROR)
	{
          read_diag_details (file->stat_info->orig_file_name,
	                     beg,
			     rdsize);
	  return false;
	}
      if (!zero_block_p (diff_buffer, bytes_read))
	{
	  char begbuf[INT_BUFSIZE_BOUND (off_t)];
 	  report_difference (file->stat_info,
			     _("File fragment at %s is not a hole"),
			     offtostr (beg, begbuf));
	  return false;
	}

      beg += bytes_read;
    }
  return true;
}

Project: savannah

dump_zeros (struct tar_sparse_file *file, off_t offset)
{
  static char const zero_buf[BLOCKSIZE];

  if (offset < file->offset)
    {
      errno = EINVAL;
      return false;
    }

  while (file->offset < offset)
    {
      size_t size = (BLOCKSIZE < offset - file->offset
		     ? BLOCKSIZE
		     : offset - file->offset);
      ssize_t wrbytes;

      wrbytes = write (file->fd, zero_buf, size);
      if (wrbytes <= 0)
	{
	  if (wrbytes == 0)
	    errno = EINVAL;
	  return false;
	}
      file->offset += wrbytes;
    }

  return true;
}

Project: savannah

lseek_or_error (struct tar_sparse_file *file, off_t offset)
{
  if (file->seekable
      ? lseek (file->fd, offset, SEEK_SET) < 0
      : ! dump_zeros (file, offset))
    {
      seek_diag_details (file->stat_info->orig_file_name, offset);
      return false;
    }
  return true;
}

Project: savannah

sparse_add_map (struct tar_stat_info *st, struct sp_array const *sp)
{
  struct sp_array *sparse_map = st->sparse_map;
  size_t avail = st->sparse_map_avail;
  if (avail == st->sparse_map_size)
    st->sparse_map = sparse_map =
      x2nrealloc (sparse_map, &st->sparse_map_size, sizeof *sparse_map);
  sparse_map[avail] = *sp;
  st->sparse_map_avail = avail + 1;
}

Project: savannah

sparse_extract_file (int fd, struct tar_stat_info *st, off_t *size)
{
  bool rc = true;
  struct tar_sparse_file file;
  size_t i;

  if (!tar_sparse_init (&file))
    return dump_status_not_implemented;

  file.stat_info = st;
  file.fd = fd;
  file.seekable = lseek (fd, 0, SEEK_SET) == 0;
  file.offset = 0;

  rc = tar_sparse_decode_header (&file);
  for (i = 0; rc && i < file.stat_info->sparse_map_avail; i++)
    rc = tar_sparse_extract_region (&file, i);
  *size = file.stat_info->archive_file_size - file.dumped_size;
  return (tar_sparse_done (&file) && rc) ? dump_status_ok : dump_status_short;
}

Project: savannah

sparse_scan_file (struct tar_sparse_file *file)
{
  /* always check for completely sparse files */
  if (sparse_scan_file_wholesparse (file))
    return true;

  switch (hole_detection)
    {
    case HOLE_DETECTION_DEFAULT:
    case HOLE_DETECTION_SEEK:
#ifdef SEEK_HOLE
      if (sparse_scan_file_seek (file))
        return true;
#else
      if (hole_detection == HOLE_DETECTION_SEEK)
	WARN((0, 0,
	      _("\"seek\" hole detection is not supported, using \"raw\".")));
      /* fall back to "raw" for this and all other files */
      hole_detection = HOLE_DETECTION_RAW;
#endif
      FALLTHROUGH;
    case HOLE_DETECTION_RAW:
      if (sparse_scan_file_raw (file))
	return true;
    }

  return false;
}

Project: savannah

sparse_scan_file_raw (struct tar_sparse_file *file)
{
  struct tar_stat_info *st = file->stat_info;
  int fd = file->fd;
  char buffer[BLOCKSIZE];
  size_t count = 0;
  off_t offset = 0;
  struct sp_array sp = {0, 0};

  st->archive_file_size = 0;

  if (!tar_sparse_scan (file, scan_begin, NULL))
    return false;

  while ((count = blocking_read (fd, buffer, sizeof buffer)) != 0
         && count != SAFE_READ_ERROR)
    {
      /* Analyze the block.  */
      if (zero_block_p (buffer, count))
        {
          if (sp.numbytes)
            {
              sparse_add_map (st, &sp);
              sp.numbytes = 0;
              if (!tar_sparse_scan (file, scan_block, NULL))
                return false;
            }
        }
      else
        {
          if (sp.numbytes == 0)
            sp.offset = offset;
          sp.numbytes += count;
          st->archive_file_size += count;
          if (!tar_sparse_scan (file, scan_block, buffer))
            return false;
        }

      offset += count;
    }

  /* save one more sparse segment of length 0 to indicate that
     the file ends with a hole */
  if (sp.numbytes == 0)
    sp.offset = offset;

  sparse_add_map (st, &sp);
  st->archive_file_size += count;
  return tar_sparse_scan (file, scan_end, NULL);
}

Project: savannah

sparse_scan_file_seek (struct tar_sparse_file *file)
{
  struct tar_stat_info *st = file->stat_info;
  int fd = file->fd;
  struct sp_array sp = {0, 0};
  off_t offset = 0;
  off_t data_offset;
  off_t hole_offset;

  st->archive_file_size = 0;

  for (;;)
    {
      /* locate first chunk of data */
      data_offset = lseek (fd, offset, SEEK_DATA);

      if (data_offset == (off_t)-1)
        /* ENXIO == EOF; error otherwise */
        {
          if (errno == ENXIO)
            {
              /* file ends with hole, add one more empty chunk of data */
              sp.numbytes = 0;
              sp.offset = st->stat.st_size;
              sparse_add_map (st, &sp);
              return true;
            }
          return false;
        }

      hole_offset = lseek (fd, data_offset, SEEK_HOLE);

      /* according to specs, if FS does not fully support
	 SEEK_DATA/SEEK_HOLE it may just implement kind of "wrapper" around
	 classic lseek() call.  We must detect it here and try to use other
	 hole-detection methods. */
      if (offset == 0 /* first loop */
          && data_offset == 0
          && hole_offset == st->stat.st_size)
        {
          lseek (fd, 0, SEEK_SET);
          return false;
        }

      sp.offset = data_offset;
      sp.numbytes = hole_offset - data_offset;
      sparse_add_map (st, &sp);

      st->archive_file_size += sp.numbytes;
      offset = hole_offset;
    }

  return true;
}

Project: savannah

sparse_scan_file_wholesparse (struct tar_sparse_file *file)
{
  struct tar_stat_info *st = file->stat_info;
  struct sp_array sp = {0, 0};

  /* Note that this function is called only for truly sparse files of size >= 1
     block size (checked via ST_IS_SPARSE before).  See the thread
     http://www.mail-archive.com/bug-tar@gnu.org/msg04209.html for more info */
  if (ST_NBLOCKS (st->stat) == 0)
    {
      st->archive_file_size = 0;
      sp.offset = st->stat.st_size;
      sparse_add_map (st, &sp);
      return true;
    }

  return false;
}

Project: savannah

sparse_select_optab (struct tar_sparse_file *file)
{
  switch (current_format == DEFAULT_FORMAT ? archive_format : current_format)
    {
    case V7_FORMAT:
    case USTAR_FORMAT:
      return false;

    case OLDGNU_FORMAT:
    case GNU_FORMAT: /*FIXME: This one should disappear? */
      file->optab = &oldgnu_optab;
      break;

    case POSIX_FORMAT:
      file->optab = &pax_optab;
      break;

    case STAR_FORMAT:
      file->optab = &star_optab;
      break;

    default:
      return false;
    }
  return true;
}

Project: savannah

sparse_skip_file (struct tar_stat_info *st)
{
  bool rc = true;
  struct tar_sparse_file file;

  if (!tar_sparse_init (&file))
    return dump_status_not_implemented;

  file.stat_info = st;
  file.fd = -1;

  rc = tar_sparse_decode_header (&file);
  skip_file (file.stat_info->archive_file_size - file.dumped_size);
  return (tar_sparse_done (&file) && rc) ? dump_status_ok : dump_status_short;
}

Project: savannah

tar_sparse_done (struct tar_sparse_file *file)
{
  if (file->optab->done)
    return file->optab->done (file);
  return true;
}

Project: savannah

tar_sparse_dump_header (struct tar_sparse_file *file)
{
  if (file->optab->dump_header)
    return file->optab->dump_header (file);
  return false;
}

Project: savannah

tar_sparse_dump_region (struct tar_sparse_file *file, size_t i)
{
  if (file->optab->dump_region)
    return file->optab->dump_region (file, i);
  return false;
}

Project: savannah

tar_sparse_init (struct tar_sparse_file *file)
{
  memset (file, 0, sizeof *file);

  if (!sparse_select_optab (file))
    return false;

  if (file->optab->init)
    return file->optab->init (file);

  return true;
}

Project: savannah

tar_sparse_member_p (struct tar_sparse_file *file)
{
  if (file->optab->sparse_member_p)
    return file->optab->sparse_member_p (file);
  return false;
}

Project: savannah

tar_sparse_scan (struct tar_sparse_file *file, enum sparse_scan_state state,
		 void *block)
{
  if (file->optab->scan_block)
    return file->optab->scan_block (file, state, block);
  return true;
}

Project: savannah

zero_block_p (char const *buffer, size_t size)
{
  while (size--)
    if (*buffer++)
      return false;
  return true;
}