libclamav/untar.c
a8b7c1dd
 /*
2023340a
  *  Copyright (C) 2007-2008 Sourcefire, Inc.
  *
  *  Authors: Nigel Horne
a8b7c1dd
  *
  *  This program is free software; you can redistribute it and/or modify
2023340a
  *  it under the terms of the GNU General Public License version 2 as
  *  published by the Free Software Foundation.
a8b7c1dd
  *
  *  This program is distributed in the hope that it will be useful,
  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  *  GNU General Public License for more details.
  *
  *  You should have received a copy of the GNU General Public License
  *  along with this program; if not, write to the Free Software
48b7b4a7
  *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
  *  MA 02110-1301, USA.
a8b7c1dd
  */
2023340a
 
95e11e5a
 static	char	const	rcsid[] = "$Id: untar.c,v 1.35 2007/02/12 20:46:09 njh Exp $";
dea34e7d
 
 #if HAVE_CONFIG_H
 #include "clamav-config.h"
 #endif
95fb46e5
 
 #include <stdio.h>
 #include <errno.h>
 #include <string.h>
45b28aba
 #ifdef	HAVE_UNISTD_H
95fb46e5
 #include <unistd.h>
45b28aba
 #endif
95fb46e5
 #include <sys/stat.h>
bb3fdd1b
 #include <fcntl.h>
45b28aba
 #ifdef	HAVE_SYS_PARAM_H
7c5a7a47
 #include <sys/param.h>	/* for NAME_MAX */
45b28aba
 #endif
95fb46e5
 
 #include "clamav.h"
 #include "others.h"
bb3fdd1b
 #include "untar.h"
11b50569
 #include "mbox.h"
 #include "blob.h"
96522097
 #include "scanners.h"
570b1d00
 #include "matcher.h"
95fb46e5
 
 #define BLOCKSIZE 512
c3c807d7
 #define TARSIZEOFFSET 124
 #define TARSIZELEN 12
 #define TARCHECKSUMOFFSET 148
 #define TARCHECKSUMLEN 8
 #define TARFILETYPEOFFSET 156
95fb46e5
 
bb3fdd1b
 static int
 octal(const char *str)
95fb46e5
 {
ed734e16
 	int ret;
95fb46e5
 
ed734e16
 	if(sscanf(str, "%o", (unsigned int *)&ret) != 1)
 		return -1;
95fb46e5
 	return ret;
 }
 
c3c807d7
 /**
  * Retrieve checksum values from a tar header block.
  * @param header Header data block, padded with zeroes to reach BLOCKSIZE
  * @return int value of checksum, -1 (from octal()) if bad value
  */
 static int
 getchecksum(const char *header)
 {
 	char ochecksum[TARCHECKSUMLEN + 1];
 	int checksum = -1;
 
 	strncpy(ochecksum, header+TARCHECKSUMOFFSET, TARCHECKSUMLEN);
 	ochecksum[TARCHECKSUMLEN] = '\0';
 	checksum = octal(ochecksum);
 	return checksum;
 }
 
 /**
  * Calculate checksum values for tar header blocks.
  * @param header Header data block, padded with zeroes to reach BLOCKSIZE
  * @param targetsum Check value to match (as int not octal!)
  * @return 0 if checksum matches target, -1 if not
  */
 static int
 testchecksum(const char *header, int targetsum)
 {
 	const unsigned char *posix;	
 	const signed char *legacy;
 	int posix_sum = 0, legacy_sum = 0;
 	int i;
 
 	// targetsum -1 represents an error from octal()
 	if (targetsum == -1) {
 		return -1;
 	}
 
 	/* Build checksums. POSIX is unsigned; some legacy tars use signed. */
 	posix = (unsigned char *)header;
 	legacy = (signed char *)header;
 	for (i = 0; i < BLOCKSIZE; i++ ) {
 		if ((i >= TARCHECKSUMOFFSET) && (i < TARCHECKSUMOFFSET + TARCHECKSUMLEN)) {
 			/* Use ascii value of space in place of checksum value */
 			posix_sum += 32;
 			legacy_sum += 32;
 		}
 		else {
 			posix_sum += posix[i];
 			legacy_sum += legacy[i];
 		}
 	}
 
 	if ((targetsum == posix_sum) || (targetsum == legacy_sum)) {
 		return 0;
 	}
 	return -1;
 }
 
95fb46e5
 int
d91ab809
 cli_untar(const char *dir, int desc, unsigned int posix, cli_ctx *ctx)
95fb46e5
 {
d0d1afd7
 	int size = 0, ret, fout=-1;
95fb46e5
 	int in_block = 0;
c3c807d7
 	int last_header_bad = 0;
8e199ae3
 	int limitnear = 0;
9140eb10
 	unsigned int files = 0;
bb3fdd1b
 	char fullname[NAME_MAX + 1];
8e199ae3
 	size_t currsize = 0;
fb0a54dd
 	unsigned int num_viruses = 0; 
95fb46e5
 
d0d1afd7
 	cli_dbgmsg("In untar(%s, %d)\n", dir, desc);
95fb46e5
 
 	for(;;) {
 		char block[BLOCKSIZE];
9c107190
 		const int nread = cli_readn(desc, block, (unsigned int)sizeof(block));
c3c807d7
 		cli_dbgmsg("cli_untar: nread = %d\n", nread);
95fb46e5
 
 		if(!in_block && nread == 0)
 			break;
 
f0931086
 		if(nread < 0) {
d0d1afd7
 			if(fout>=0)
 				close(fout);
f0931086
 			cli_errmsg("cli_untar: block read error\n");
871177cd
 			return CL_EREAD;
95fb46e5
 		}
 
 		if(!in_block) {
 			char type;
d0d1afd7
 			int directory, skipEntry = 0;
c3c807d7
 			int checksum = -1;
 			char magic[7], name[101], osize[TARSIZELEN + 1];
8e199ae3
 			currsize = 0;
bb3fdd1b
 
d0d1afd7
 			if(fout>=0) {
 				lseek(fout, 0, SEEK_SET);
 				ret = cli_magic_scandesc(fout, ctx);
 				close(fout);
33068e09
 				if (!ctx->engine->keeptmp)
871177cd
 					if (cli_unlink(fullname)) return CL_EUNLINK;
fb0a54dd
 				if (ret==CL_VIRUS) {
 				    if (!SCAN_ALL)
d0d1afd7
 					return CL_VIRUS;
fb0a54dd
 				    else
 					num_viruses++;
 				}
d0d1afd7
 				fout = -1;
bb3fdd1b
 			}
95fb46e5
 
7c5a7a47
 			if(block[0] == '\0')	/* We're done */
95fb46e5
 				break;
d91ab809
 			if((ret=cli_checklimits("cli_untar", ctx, 0, 0, 0))!=CL_CLEAN)
 				return ret;
9140eb10
 
c3c807d7
 			checksum = getchecksum(block);
 			cli_dbgmsg("cli_untar: Candidate checksum = %d, [%o in octal]\n", checksum, checksum);
 			if(testchecksum(block, checksum) != 0) {
 				// If checksum is bad, dump and look for next header block
 				cli_dbgmsg("cli_untar: Invalid checksum in tar header. Skip to next...\n");
 				if (last_header_bad == 0) {
 					last_header_bad++;
9d6be7c5
 					cli_dbgmsg("cli_untar: Invalid checksum found inside archive!\n");
c3c807d7
 				}
 				continue;
 			} else {
 				last_header_bad = 0;
 				cli_dbgmsg("cli_untar: Checksum %d is valid.\n", checksum);
 			}
 
90e80a54
 			/* Notice assumption that BLOCKSIZE > 262 */
a7f5fd00
 			if(posix) {
 				strncpy(magic, block+257, 5);
 				magic[5] = '\0';
 				if(strcmp(magic, "ustar") != 0) {
9fc9db81
 					cli_dbgmsg("cli_untar: Incorrect magic string '%s' in tar header\n", magic);
a7f5fd00
 					return CL_EFORMAT;
 				}
95fb46e5
 			}
 
c3c807d7
 			type = block[TARFILETYPEOFFSET];
95fb46e5
 
 			switch(type) {
45f8ad14
 				default:
9fc9db81
 					cli_dbgmsg("cli_untar: unknown type flag %c\n", type);
63943caf
 				case '0':	/* plain file */
 				case '\0':	/* plain file */
 				case '7':	/* contiguous file */
45f8ad14
 				case 'M':	/* continuation of a file from another volume; might as well scan it. */
9140eb10
 					files++;
95fb46e5
 					directory = 0;
 					break;
ab592ce9
 				case '1':	/* Link to already archived file */
63943caf
 				case '5':	/* directory */
 				case '2':	/* sym link */
 				case '3':	/* char device */
 				case '4':	/* block device */
 				case '6':	/* fifo special */
d2888b89
 				case 'V':	/* Volume header */
95fb46e5
 					directory = 1;
 					break;
d2888b89
 				case 'K':
 				case 'L':
 					/* GNU extension - ././@LongLink
 					 * Discard the blocks with the extended filename,
 					 * the last header will contain parts of it anyway
 					 */
45f8ad14
 				case 'N': 	/* Old GNU format way of storing long filenames. */
 				case 'A':	/* Solaris ACL */
 				case 'E':	/* Solaris Extended attribute s*/
 				case 'I':	/* Inode only */
 				case 'g':	/* Global extended header */
 				case 'x': 	/* Extended attributes */
 				case 'X':	/* Extended attributes (POSIX) */
d2888b89
 					directory = 0;
 					skipEntry = 1;
 					break;
95fb46e5
 			}
 
63943caf
 			if(directory) {
 				in_block = 0;
95fb46e5
 				continue;
63943caf
 			}
95fb46e5
 
c3c807d7
 			strncpy(osize, block+TARSIZEOFFSET, TARSIZELEN);
 			osize[TARSIZELEN] = '\0';
d2888b89
 			size = octal(osize);
 			if(size < 0) {
9fc9db81
 				cli_dbgmsg("cli_untar: Invalid size in tar header\n");
9140eb10
 				skipEntry++;
9fc9db81
 			} else {
 				cli_dbgmsg("cli_untar: size = %d\n", size);
8e199ae3
 				ret = cli_checklimits("cli_untar", ctx, size, 0, 0);
 				switch(ret) {
 					case CL_EMAXFILES: // Scan no more files
 						skipEntry++;
 						limitnear = 0;
 						break;
 					case CL_EMAXSIZE: // Either single file limit or total byte limit would be exceeded
 						cli_dbgmsg("cli_untar: would exceed limit, will try up to max");
 						limitnear = 1;
 						break;
 					default: // Ok based on reported content size
 						limitnear = 0;
 						break;
 				}
9140eb10
 			}
d2888b89
 
 			if(skipEntry) {
 				const int nskip = (size % BLOCKSIZE || !size) ? size + BLOCKSIZE - (size % BLOCKSIZE) : size;
2995f1c7
 				
 				if(nskip < 0) {
c3c807d7
 					cli_dbgmsg("cli_untar: got negative skip size, giving up\n");
2995f1c7
 					return CL_CLEAN;
 				}
9140eb10
 				cli_dbgmsg("cli_untar: skipping entry\n");
d2888b89
 				lseek(desc, nskip, SEEK_CUR);
 				continue;
 			}
 
bb3fdd1b
 			strncpy(name, block, 100);
 			name[100] = '\0';
fb0a54dd
 			if(cli_matchmeta(ctx, name, size, size, 0, files, 0, NULL) == CL_VIRUS) {
 			    if (!SCAN_ALL)
 				return CL_VIRUS;
 			    else
 				num_viruses++;
 			}
570b1d00
 
58481352
 			snprintf(fullname, sizeof(fullname)-1, "%s"PATHSEP"tar%02u", dir, files);
d0d1afd7
 			fullname[sizeof(fullname)-1] = '\0';
 			fout = open(fullname, O_RDWR|O_CREAT|O_EXCL|O_TRUNC|O_BINARY, 0600);
95fb46e5
 
d0d1afd7
 			if(fout < 0) {
e68d70e7
 				char err[128];
 				cli_errmsg("cli_untar: Can't create temporary file %s: %s\n", fullname, cli_strerror(errno, err, sizeof(err)));
bb3fdd1b
 				return CL_ETMPFILE;
 			}
e68d70e7
 
d0d1afd7
 			cli_dbgmsg("cli_untar: extracting to %s\n", fullname);
bb3fdd1b
 
 			in_block = 1;
95fb46e5
 		} else { /* write or continue writing file contents */
8e199ae3
 			int nbytes, nwritten;
 			int skipwrite = 0;
 			char err[128];
 
 			nbytes = size>512? 512:size;
 			if (nread && nread < nbytes)
 				nbytes = nread;
 
 			if (limitnear > 0) {
 				currsize += nbytes;
 				cli_dbgmsg("cli_untar: Approaching limit...\n");
 				if (cli_checklimits("cli_untar", ctx, (unsigned long)currsize, 0, 0) != CL_SUCCESS) {
 					// Limit would be exceeded by this file, suppress writing beyond limit
 					// Need to keep reading to get to end of file chunk
 					skipwrite++;
 				}
 			}
95fb46e5
 
8e199ae3
 			if (skipwrite == 0) {
 				nwritten = (int)write(fout, block, (size_t)nbytes);
 
 				if(nwritten != nbytes) {
 					cli_errmsg("cli_untar: only wrote %d bytes to file %s (out of disc space?)\n",
 						nwritten, fullname);
 					close(fout);
 					return CL_EWRITE;
 				}
95fb46e5
 			}
 			size -= nbytes;
8e199ae3
 			if ((size != 0) && (nread == 0)) {
 				// Truncated tar file, so end file content like tar behavior
 				cli_dbgmsg("cli_untar: No bytes read! Forcing end of file content.\n");
 				size = 0;
 			}
95fb46e5
 		}
d4112005
 		if (size == 0)
 			in_block = 0;
d91ab809
         }	
d0d1afd7
 	if(fout>=0) {
 		lseek(fout, 0, SEEK_SET);
 		ret = cli_magic_scandesc(fout, ctx);
 		close(fout);
33068e09
 		if (!ctx->engine->keeptmp)
871177cd
 			if (cli_unlink(fullname)) return CL_EUNLINK;
d0d1afd7
 		if (ret==CL_VIRUS)
 			return CL_VIRUS;
 	}
fb0a54dd
 	if (num_viruses)
 	    return CL_VIRUS;
d0d1afd7
 	return CL_CLEAN;
95fb46e5
 }