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-19 \u2014 Data Processing Errors
Weaknesses in this category are typically found in functionality that processes data. Data processing is the manipulation of input to retrieve or save information.
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;
}