CVE-2016-5418 (C, published 2016-09-21T14:25Z)
Description: value: The sandboxing code in libarchive 3.2.0 and earlier mishandles hardlink archive entries of non-zero data size, which might allow remote attackers to write to arbitrary files via a crafted archive file.
Severity: MEDIUM (CVSS v3 base score: 7.5)
Weakness: CWE-20 \u2014 Improper Input Validation
The product receives input or data, but it does
        not validate or incorrectly validates that the input has the
        properties that are required to process the data safely and
        correctly.
Repository: https://github.com/libarchive/libarchive
Files changed:
libarchive/archive_write_disk_posix.c
Commit message: Fixes for Issue #745 and Issue #746 from Doran Moppert.


Vulnerable code:
check_symlinks(struct archive_write_disk *a)
	char *pn;
	/* Whatever we checked last time doesn't need to be re-checked. */
	pn = a->name;
	if (archive_strlen(&(a->path_safe)) > 0) {
		char *p = a->path_safe.s;
		while ((*pn != '\0') && (*p == *pn))
			++p, ++pn;
	}
	if(pn == a->name && pn[0] == '/')
		++pn;
	c = pn[0];
	/* Keep going until we've checked the entire name. */
	while (pn[0] != '\0' && (pn[0] != '/' || pn[1] != '\0')) {
		while (*pn != '\0' && *pn != '/')
			++pn;
		c = pn[0];
		pn[0] = '\0';
		r = lstat(a->name, &st);
				/* Note: This effectively disables deep directory
				return (ARCHIVE_FAILED);
			if (c == '\0') {
				if (unlink(a->name)) {
					archive_set_error(&a->archive, errno,
					    "Could not remove symlink %s",
					    a->name);
					pn[0] = c;
					return (ARCHIVE_FAILED);
				a->pst = NULL;
				if (!S_ISLNK(a->mode)) {
					archive_set_error(&a->archive, 0,
					    "Removing symlink %s",
					    a->name);
				pn[0] = c;
				return (0);
			} else if (a->flags & ARCHIVE_EXTRACT_UNLINK) {
				if (unlink(a->name) != 0) {
					archive_set_error(&a->archive, 0,
					    "Cannot remove intervening symlink %s",
					    a->name);
					pn[0] = c;
					return (ARCHIVE_FAILED);
				a->pst = NULL;
				archive_set_error(&a->archive, 0,
				    "Cannot extract through symlink %s",
				    a->name);
				pn[0] = c;
				return (ARCHIVE_FAILED);
		pn[0] = c;
		if (pn[0] != '\0')
			pn++; /* Advance to the next segment. */
	pn[0] = c;
	/* We've checked and/or cleaned the whole path, so remember it. */
	archive_strcpy(&a->path_safe, a->name);
	return (ARCHIVE_OK);
cleanup_pathname(struct archive_write_disk *a)
	dest = src = a->name;
		archive_set_error(&a->archive, ARCHIVE_ERRNO_MISC,
		    "Invalid empty pathname");
		if (a->flags & ARCHIVE_EXTRACT_SECURE_NOABSOLUTEPATHS) {
			archive_set_error(&a->archive, ARCHIVE_ERRNO_MISC,
			                  "Path is absolute");
					if (a->flags & ARCHIVE_EXTRACT_SECURE_NODOTDOT) {
						archive_set_error(&a->archive,
						    ARCHIVE_ERRNO_MISC,
						    "Path contains '..'");
	if (dest == a->name) {

Fixed code:
static int	check_symlinks_fsobj(char *path, int *error_number, struct archive_string *error_string, int flags);
static int	cleanup_pathname_fsobj(char *path, int *error_number, struct archive_string *error_string, int flags);
	/* these for check_symlinks_fsobj */
	char *linkname_copy;	/* non-const copy of linkname */
	struct archive_string error_string;
	int error_number;
		archive_string_init(&error_string);
		linkname_copy = strdup(linkname);
		if (linkname_copy == NULL) {
		    return (EPERM);
		}
		/* TODO: consider using the cleaned-up path as the link target? */
		r = cleanup_pathname_fsobj(linkname_copy, &error_number, &error_string, a->flags);
		if (r != ARCHIVE_OK) {
			archive_set_error(&a->archive, error_number, "%s", error_string.s);
			free(linkname_copy);
			/* EPERM is more appropriate than error_number for our callers */
			return (EPERM);
		}
		r = check_symlinks_fsobj(linkname_copy, &error_number, &error_string, a->flags);
		if (r != ARCHIVE_OK) {
			archive_set_error(&a->archive, error_number, "%s", error_string.s);
			free(linkname_copy);
			/* EPERM is more appropriate than error_number for our callers */
			return (EPERM);
		}
		free(linkname_copy);

/*
 * Checks the given path to see if any elements along it are symlinks.  Returns
 * ARCHIVE_OK if there are none, otherwise puts an error in errmsg.
 */
check_symlinks_fsobj(char *path, int *error_number, struct archive_string *error_string, int flags)
	(void)path; /* UNUSED */
	(void)error_number; /* UNUSED */
	(void)error_string; /* UNUSED */
	(void)flags; /* UNUSED */
	int res = ARCHIVE_OK;
	char *tail;
	char *head;
	int last;
	int restore_pwd;

	/* Nothing to do here if name is empty */
	if(path[0] == '\0')
	    return (ARCHIVE_OK);
	 *
	 * Walk the filename in chunks separated by '/'.  For each segment:
	 *  - if it doesn't exist, continue
	 *  - if it's symlink, abort or remove it
	 *  - if it's a directory and it's not the last chunk, cd into it
	 * As we go:
	 *  head points to the current (relative) path
	 *  tail points to the temporary \0 terminating the segment we're currently examining
	 *  c holds what used to be in *tail
	 *  last is 1 if this is the last tail
	restore_pwd = open(".", O_RDONLY | O_BINARY | O_CLOEXEC);
	__archive_ensure_cloexec_flag(restore_pwd);
	if (restore_pwd < 0)
		return (ARCHIVE_FATAL);
	head = path;
	tail = path;
	last = 0;
	/* TODO: reintroduce a safe cache here? */
	if(tail == path && tail[0] == '/')
		++tail;
	/* Keep going until we've checked the entire name.
	 * head, tail, path all alias the same string, which is
	 * temporarily zeroed at tail, so be careful restoring the
	 * stashed (c=tail[0]) for error messages.
	 * Exiting the loop with break is okay; continue is not.
	 */
	while (!last) {
		/* Skip the separator we just consumed, plus any adjacent ones */
		while (*tail == '/')
		    ++tail;
		while (*tail != '\0' && *tail != '/')
			++tail;
		/* is this the last path component? */
		last = (tail[0] == '\0') || (tail[0] == '/' && tail[1] == '\0');
		/* temporarily truncate the string here */
		c = tail[0];
		tail[0] = '\0';
		r = lstat(head, &st);
			tail[0] = c;
				/* Treat any other error as fatal - best to be paranoid here
				 * Note: This effectively disables deep directory
				if (error_number) *error_number = errno;
				if (error_string)
					archive_string_sprintf(error_string,
							"Could not stat %s",
							path);
				res = ARCHIVE_FAILED;
				break;
			}
		} else if (S_ISDIR(st.st_mode)) {
			if (!last) {
				if (chdir(head) != 0) {
					tail[0] = c;
					if (error_number) *error_number = errno;
					if (error_string)
						archive_string_sprintf(error_string,
								"Could not chdir %s",
								path);
					res = (ARCHIVE_FATAL);
					break;
				}
				/* Our view is now from inside this dir: */
				head = tail + 1;
			if (last) {
				if (unlink(head)) {
					tail[0] = c;
					if (error_number) *error_number = errno;
					if (error_string)
						archive_string_sprintf(error_string,
								"Could not remove symlink %s",
								path);
					res = ARCHIVE_FAILED;
					break;
				tail[0] = c;
				/* FIXME:  not sure how important this is to restore
				if (!S_ISLNK(path)) {
					if (error_number) *error_number = 0;
					if (error_string)
						archive_string_sprintf(error_string,
								"Removing symlink %s",
								path);
				*/
				res = ARCHIVE_OK;
				break;
			} else if (flags & ARCHIVE_EXTRACT_UNLINK) {
				if (unlink(head) != 0) {
					tail[0] = c;
					if (error_number) *error_number = 0;
					if (error_string)
						archive_string_sprintf(error_string,
								"Cannot remove intervening symlink %s",
								path);
					res = ARCHIVE_FAILED;
					break;
				tail[0] = c;
				tail[0] = c;
				if (error_number) *error_number = 0;
				if (error_string)
					archive_string_sprintf(error_string,
							"Cannot extract through symlink %s",
							path);
				res = ARCHIVE_FAILED;
				break;
		/* be sure to always maintain this */
		tail[0] = c;
		if (tail[0] != '\0')
			tail++; /* Advance to the next segment. */
	/* Catches loop exits via break */
	tail[0] = c;
#ifdef HAVE_FCHDIR
	/* If we changed directory above, restore it here. */
	if (restore_pwd >= 0) {
		r = fchdir(restore_pwd);
		if (r != 0) {
			if(error_number) *error_number = errno;
			if(error_string)
				archive_string_sprintf(error_string,
						"chdir() failure");
		}
		close(restore_pwd);
		restore_pwd = -1;
		if (r != 0) {
			res = (ARCHIVE_FATAL);
		}
	}
#endif
	/* TODO: reintroduce a safe cache here? */
	return res;
/*
 * Check a->name for symlinks, returning ARCHIVE_OK if its clean, otherwise
 * calls archive_set_error and returns ARCHIVE_{FATAL,FAILED}
 */
static int
check_symlinks(struct archive_write_disk *a)
{
	struct archive_string error_string;
	int error_number;
	int rc;
	archive_string_init(&error_string);
	rc = check_symlinks_fsobj(a->name, &error_number, &error_string, a->flags);
	if (rc != ARCHIVE_OK) {
		archive_set_error(&a->archive, error_number, "%s", error_string.s);
	}
	archive_string_free(&error_string);
	a->pst = NULL;	/* to be safe */
	return rc;
}


cleanup_pathname_fsobj(char *path, int *error_number, struct archive_string *error_string, int flags)
	dest = src = path;
		if (error_number) *error_number = ARCHIVE_ERRNO_MISC;
		if (error_string)
		    archive_string_sprintf(error_string,
			    "Invalid empty pathname");
		if (flags & ARCHIVE_EXTRACT_SECURE_NOABSOLUTEPATHS) {
			if (error_number) *error_number = ARCHIVE_ERRNO_MISC;
			if (error_string)
			    archive_string_sprintf(error_string,
				    "Path is absolute");
					if (flags & ARCHIVE_EXTRACT_SECURE_NODOTDOT) {
						if (error_number) *error_number = ARCHIVE_ERRNO_MISC;
						if (error_string)
						    archive_string_sprintf(error_string,
							    "Path contains '..'");
	if (dest == path) {
static int
cleanup_pathname(struct archive_write_disk *a)
{
	struct archive_string error_string;
	int error_number;
	int rc;
	archive_string_init(&error_string);
	rc = cleanup_pathname_fsobj(a->name, &error_number, &error_string, a->flags);
	if (rc != ARCHIVE_OK) {
		archive_set_error(&a->archive, error_number, "%s", error_string.s);
	}
	archive_string_free(&error_string);
	return rc;
}