Commit message: s3: smbd: Don't loop infinitely on bad-symlink resolution.

In the FILE_OPEN_IF case we have O_CREAT, but not
O_EXCL. Previously we went into a loop trying first
~(O_CREAT|O_EXCL), and if that returned ENOENT
try (O_CREAT|O_EXCL). We kept looping indefinately
until we got an error, or the file was created or
opened.

The big problem here is dangling symlinks. Opening
without O_NOFOLLOW means both bad symlink
and missing path return -1, ENOENT from open(). As POSIX
is pathname based it's not possible to tell
the difference between these two cases in a
non-racy way, so change to try only two attempts before
giving up.

We don't have this problem for the O_NOFOLLOW
case as we just return NT_STATUS_OBJECT_PATH_NOT_FOUND
mapped from the ELOOP POSIX error and immediately
returned.

Unroll the loop logic to two tries instead.

BUG: https://bugzilla.samba.org/show_bug.cgi?id=12572

Pair-programmed-with: Ralph Boehme <slow@samba.org>

Signed-off-by: Jeremy Allison <jra@samba.org>
Signed-off-by: Ralph Boehme <slow@samba.org>
Reviewed-by: Ralph Boehme <slow@samba.org>
CVE: CVE-2017-9461
CWE: CWE-835
Vulnerability description: smbd in Samba before 4.4.10 and 4.5.x before 4.5.6 has a denial of service vulnerability (fd_open_atomic infinite loop with high CPU usage and memory consumption) due to wrongly handling dangling symlinks.

Project: samba

void change_file_owner_to_parent(connection_struct *conn,
					const char *inherit_from_dir,
					files_struct *fsp)
{
	struct smb_filename *smb_fname_parent;
	int ret;

	smb_fname_parent = synthetic_smb_fname(talloc_tos(),
					inherit_from_dir,
					NULL,
					NULL,
					0);
	if (smb_fname_parent == NULL) {
		return;
	}

	ret = SMB_VFS_STAT(conn, smb_fname_parent);
	if (ret == -1) {
		DEBUG(0,("change_file_owner_to_parent: failed to stat parent "
			 "directory %s. Error was %s\n",
			 smb_fname_str_dbg(smb_fname_parent),
			 strerror(errno)));
		TALLOC_FREE(smb_fname_parent);
		return;
	}

	if (smb_fname_parent->st.st_ex_uid == fsp->fsp_name->st.st_ex_uid) {
		/* Already this uid - no need to change. */
		DEBUG(10,("change_file_owner_to_parent: file %s "
			"is already owned by uid %d\n",
			fsp_str_dbg(fsp),
			(int)fsp->fsp_name->st.st_ex_uid ));
		TALLOC_FREE(smb_fname_parent);
		return;
	}

	become_root();
	ret = SMB_VFS_FCHOWN(fsp, smb_fname_parent->st.st_ex_uid, (gid_t)-1);
	unbecome_root();
	if (ret == -1) {
		DEBUG(0,("change_file_owner_to_parent: failed to fchown "
			 "file %s to parent directory uid %u. Error "
			 "was %s\n", fsp_str_dbg(fsp),
			 (unsigned int)smb_fname_parent->st.st_ex_uid,
			 strerror(errno) ));
	} else {
		DEBUG(10,("change_file_owner_to_parent: changed new file %s to "
			"parent directory uid %u.\n", fsp_str_dbg(fsp),
			(unsigned int)smb_fname_parent->st.st_ex_uid));
		/* Ensure the uid entry is updated. */
		fsp->fsp_name->st.st_ex_uid = smb_fname_parent->st.st_ex_uid;
	}

	TALLOC_FREE(smb_fname_parent);
}

Project: samba
File: open.c

static NTSTATUS check_base_file_access(struct connection_struct *conn,
				struct smb_filename *smb_fname,
				uint32_t access_mask)
{
	NTSTATUS status;

	status = smbd_calculate_access_mask(conn, smb_fname,
					false,
					access_mask,
					&access_mask);
	if (!NT_STATUS_IS_OK(status)) {
		DEBUG(10, ("smbd_calculate_access_mask "
			"on file %s returned %s\n",
			smb_fname_str_dbg(smb_fname),
			nt_errstr(status)));
		return status;
	}

	if (access_mask & (FILE_WRITE_DATA|FILE_APPEND_DATA)) {
		uint32_t dosattrs;
		if (!CAN_WRITE(conn)) {
			return NT_STATUS_ACCESS_DENIED;
		}
		dosattrs = dos_mode(conn, smb_fname);
		if (IS_DOS_READONLY(dosattrs)) {
			return NT_STATUS_ACCESS_DENIED;
		}
	}

	return smbd_check_access_rights(conn,
					smb_fname,
					false,
					access_mask);
}

Project: samba

NTSTATUS check_parent_access(struct connection_struct *conn,
				struct smb_filename *smb_fname,
				uint32_t access_mask)
{
	NTSTATUS status;
	char *parent_dir = NULL;
	struct security_descriptor *parent_sd = NULL;
	uint32_t access_granted = 0;
	struct smb_filename *parent_smb_fname = NULL;

	if (!parent_dirname(talloc_tos(),
				smb_fname->base_name,
				&parent_dir,
				NULL)) {
		return NT_STATUS_NO_MEMORY;
	}

	parent_smb_fname = synthetic_smb_fname(talloc_tos(),
				parent_dir,
				NULL,
				NULL,
				smb_fname->flags);
	if (parent_smb_fname == NULL) {
		return NT_STATUS_NO_MEMORY;
	}

	if (get_current_uid(conn) == (uid_t)0) {
		/* I'm sorry sir, I didn't know you were root... */
		DEBUG(10,("check_parent_access: root override "
			"on %s. Granting 0x%x\n",
			smb_fname_str_dbg(smb_fname),
			(unsigned int)access_mask ));
		return NT_STATUS_OK;
	}

	status = SMB_VFS_GET_NT_ACL(conn,
				parent_smb_fname,
				SECINFO_DACL,
				    talloc_tos(),
				&parent_sd);

	if (!NT_STATUS_IS_OK(status)) {
		DEBUG(5,("check_parent_access: SMB_VFS_GET_NT_ACL failed for "
			"%s with error %s\n",
			parent_dir,
			nt_errstr(status)));
		return status;
	}

 	/*
	 * If we can access the path to this file, by
	 * default we have FILE_READ_ATTRIBUTES from the
	 * containing directory. See the section:
	 * "Algorithm to Check Access to an Existing File"
	 * in MS-FSA.pdf.
	 *
	 * se_file_access_check() also takes care of
	 * owner WRITE_DAC and READ_CONTROL.
	 */
	status = se_file_access_check(parent_sd,
				get_current_nttok(conn),
				false,
				(access_mask & ~FILE_READ_ATTRIBUTES),
				&access_granted);
	if(!NT_STATUS_IS_OK(status)) {
		DEBUG(5,("check_parent_access: access check "
			"on directory %s for "
			"path %s for mask 0x%x returned (0x%x) %s\n",
			parent_dir,
			smb_fname->base_name,
			access_mask,
			access_granted,
			nt_errstr(status) ));
		return status;
	}

	return NT_STATUS_OK;
}

Project: samba
File: open.c

NTSTATUS fd_close(files_struct *fsp)
{
	int ret;

	if (fsp->dptr) {
		dptr_CloseDir(fsp);
	}
	if (fsp->fh->fd == -1) {
		return NT_STATUS_OK; /* What we used to call a stat open. */
	}
	if (fsp->fh->ref_count > 1) {
		return NT_STATUS_OK; /* Shared handle. Only close last reference. */
	}

	ret = SMB_VFS_CLOSE(fsp);
	fsp->fh->fd = -1;
	if (ret == -1) {
		return map_nt_error_from_unix(errno);
	}
	return NT_STATUS_OK;
}

Project: samba

NTSTATUS fd_open(struct connection_struct *conn,
		 files_struct *fsp,
		 int flags,
		 mode_t mode)
{
	struct smb_filename *smb_fname = fsp->fsp_name;
	NTSTATUS status = NT_STATUS_OK;

#ifdef O_NOFOLLOW
	/* 
	 * Never follow symlinks on a POSIX client. The
	 * client should be doing this.
	 */

	if ((fsp->posix_flags & FSP_POSIX_FLAGS_OPEN) || !lp_follow_symlinks(SNUM(conn))) {
		flags |= O_NOFOLLOW;
	}
#endif

	fsp->fh->fd = SMB_VFS_OPEN(conn, smb_fname, fsp, flags, mode);
	if (fsp->fh->fd == -1) {
		int posix_errno = errno;
#ifdef O_NOFOLLOW
#if defined(ENOTSUP) && defined(OSF1)
		/* handle special Tru64 errno */
		if (errno == ENOTSUP) {
			posix_errno = ELOOP;
		}
#endif /* ENOTSUP */
#ifdef EFTYPE
		/* fix broken NetBSD errno */
		if (errno == EFTYPE) {
			posix_errno = ELOOP;
		}
#endif /* EFTYPE */
		/* fix broken FreeBSD errno */
		if (errno == EMLINK) {
			posix_errno = ELOOP;
		}
#endif /* O_NOFOLLOW */
		status = map_nt_error_from_unix(posix_errno);
		if (errno == EMFILE) {
			static time_t last_warned = 0L;

			if (time((time_t *) NULL) > last_warned) {
				DEBUG(0,("Too many open files, unable "
					"to open more!  smbd's max "
					"open files = %d\n",
					lp_max_open_files()));
				last_warned = time((time_t *) NULL);
			}
		}

	}

	DEBUG(10,("fd_open: name %s, flags = 0%o mode = 0%o, fd = %d. %s\n",
		  smb_fname_str_dbg(smb_fname), flags, (int)mode, fsp->fh->fd,
		(fsp->fh->fd == -1) ? strerror(errno) : "" ));

	return status;
}

Project: samba
File: open.c

static bool parent_override_delete(connection_struct *conn,
					const struct smb_filename *smb_fname,
					uint32_t access_mask,
					uint32_t rejected_mask)
{
	if ((access_mask & DELETE_ACCESS) &&
		    (rejected_mask & DELETE_ACCESS) &&
		    can_delete_file_in_directory(conn, smb_fname)) {
		return true;
	}
	return false;
}

Project: samba

NTSTATUS smbd_check_access_rights(struct connection_struct *conn,
				const struct smb_filename *smb_fname,
				bool use_privs,
				uint32_t access_mask)
{
	/* Check if we have rights to open. */
	NTSTATUS status;
	struct security_descriptor *sd = NULL;
	uint32_t rejected_share_access;
	uint32_t rejected_mask = access_mask;
	uint32_t do_not_check_mask = 0;

	rejected_share_access = access_mask & ~(conn->share_access);

	if (rejected_share_access) {
		DEBUG(10, ("smbd_check_access_rights: rejected share access 0x%x "
			"on %s (0x%x)\n",
			(unsigned int)access_mask,
			smb_fname_str_dbg(smb_fname),
			(unsigned int)rejected_share_access ));
		return NT_STATUS_ACCESS_DENIED;
	}

	if (!use_privs && get_current_uid(conn) == (uid_t)0) {
		/* I'm sorry sir, I didn't know you were root... */
		DEBUG(10,("smbd_check_access_rights: root override "
			"on %s. Granting 0x%x\n",
			smb_fname_str_dbg(smb_fname),
			(unsigned int)access_mask ));
		return NT_STATUS_OK;
	}

	if ((access_mask & DELETE_ACCESS) && !lp_acl_check_permissions(SNUM(conn))) {
		DEBUG(10,("smbd_check_access_rights: not checking ACL "
			"on DELETE_ACCESS on file %s. Granting 0x%x\n",
			smb_fname_str_dbg(smb_fname),
			(unsigned int)access_mask ));
		return NT_STATUS_OK;
	}

	if (access_mask == DELETE_ACCESS &&
			VALID_STAT(smb_fname->st) &&
			S_ISLNK(smb_fname->st.st_ex_mode)) {
		/* We can always delete a symlink. */
		DEBUG(10,("smbd_check_access_rights: not checking ACL "
			"on DELETE_ACCESS on symlink %s.\n",
			smb_fname_str_dbg(smb_fname) ));
		return NT_STATUS_OK;
	}

	status = SMB_VFS_GET_NT_ACL(conn, smb_fname,
			(SECINFO_OWNER |
			SECINFO_GROUP |
			 SECINFO_DACL), talloc_tos(), &sd);

	if (!NT_STATUS_IS_OK(status)) {
		DEBUG(10, ("smbd_check_access_rights: Could not get acl "
			"on %s: %s\n",
			smb_fname_str_dbg(smb_fname),
			nt_errstr(status)));

		if (NT_STATUS_EQUAL(status, NT_STATUS_ACCESS_DENIED)) {
			goto access_denied;
		}

		return status;
	}

 	/*
	 * If we can access the path to this file, by
	 * default we have FILE_READ_ATTRIBUTES from the
	 * containing directory. See the section:
	 * "Algorithm to Check Access to an Existing File"
	 * in MS-FSA.pdf.
	 *
	 * se_file_access_check() also takes care of
	 * owner WRITE_DAC and READ_CONTROL.
	 */
	do_not_check_mask = FILE_READ_ATTRIBUTES;

	/*
	 * Samba 3.6 and earlier granted execute access even
	 * if the ACL did not contain execute rights.
	 * Samba 4.0 is more correct and checks it.
	 * The compatibilty mode allows one to skip this check
	 * to smoothen upgrades.
	 */
	if (lp_acl_allow_execute_always(SNUM(conn))) {
		do_not_check_mask |= FILE_EXECUTE;
	}

	status = se_file_access_check(sd,
				get_current_nttok(conn),
				use_privs,
				(access_mask & ~do_not_check_mask),
				&rejected_mask);

	DEBUG(10,("smbd_check_access_rights: file %s requesting "
		"0x%x returning 0x%x (%s)\n",
		smb_fname_str_dbg(smb_fname),
		(unsigned int)access_mask,
		(unsigned int)rejected_mask,
		nt_errstr(status) ));

	if (!NT_STATUS_IS_OK(status)) {
		if (DEBUGLEVEL >= 10) {
			DEBUG(10,("smbd_check_access_rights: acl for %s is:\n",
				smb_fname_str_dbg(smb_fname) ));
			NDR_PRINT_DEBUG(security_descriptor, sd);
		}
	}

	TALLOC_FREE(sd);

	if (NT_STATUS_IS_OK(status) ||
			!NT_STATUS_EQUAL(status, NT_STATUS_ACCESS_DENIED)) {
		return status;
	}

	/* Here we know status == NT_STATUS_ACCESS_DENIED. */

  access_denied:

	if ((access_mask & FILE_WRITE_ATTRIBUTES) &&
			(rejected_mask & FILE_WRITE_ATTRIBUTES) &&
			!lp_store_dos_attributes(SNUM(conn)) &&
			(lp_map_readonly(SNUM(conn)) ||
			lp_map_archive(SNUM(conn)) ||
			lp_map_hidden(SNUM(conn)) ||
			lp_map_system(SNUM(conn)))) {
		rejected_mask &= ~FILE_WRITE_ATTRIBUTES;

		DEBUG(10,("smbd_check_access_rights: "
			"overrode "
			"FILE_WRITE_ATTRIBUTES "
			"on file %s\n",
			smb_fname_str_dbg(smb_fname)));
	}

	if (parent_override_delete(conn,
				smb_fname,
				access_mask,
				rejected_mask)) {
		/* Were we trying to do an open
		 * for delete and didn't get DELETE
		 * access (only) ? Check if the
		 * directory allows DELETE_CHILD.
		 * See here:
		 * http://blogs.msdn.com/oldnewthing/archive/2004/06/04/148426.aspx
		 * for details. */

		rejected_mask &= ~DELETE_ACCESS;

		DEBUG(10,("smbd_check_access_rights: "
			"overrode "
			"DELETE_ACCESS on "
			"file %s\n",
			smb_fname_str_dbg(smb_fname)));
	}

	if (rejected_mask != 0) {
		return NT_STATUS_ACCESS_DENIED;
	}
	return NT_STATUS_OK;
}