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

NTSTATUS change_dir_owner_to_parent(connection_struct *conn,
				       const char *inherit_from_dir,
				       const char *fname,
				       SMB_STRUCT_STAT *psbuf)
{
	struct smb_filename *smb_fname_parent;
	struct smb_filename *smb_fname_cwd = NULL;
	char *saved_dir = NULL;
	TALLOC_CTX *ctx = talloc_tos();
	NTSTATUS status = NT_STATUS_OK;
	int ret;

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

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

	/* We've already done an lstat into psbuf, and we know it's a
	   directory. If we can cd into the directory and the dev/ino
	   are the same then we can safely chown without races as
	   we're locking the directory in place by being in it.  This
	   should work on any UNIX (thanks tridge :-). JRA.
	*/

	saved_dir = vfs_GetWd(ctx,conn);
	if (!saved_dir) {
		status = map_nt_error_from_unix(errno);
		DEBUG(0,("change_dir_owner_to_parent: failed to get "
			 "current working directory. Error was %s\n",
			 strerror(errno)));
		goto out;
	}

	/* Chdir into the new path. */
	if (vfs_ChDir(conn, fname) == -1) {
		status = map_nt_error_from_unix(errno);
		DEBUG(0,("change_dir_owner_to_parent: failed to change "
			 "current working directory to %s. Error "
			 "was %s\n", fname, strerror(errno) ));
		goto chdir;
	}

	smb_fname_cwd = synthetic_smb_fname(ctx, ".", NULL, NULL, 0);
	if (smb_fname_cwd == NULL) {
		status = NT_STATUS_NO_MEMORY;
		goto chdir;
	}

	ret = SMB_VFS_STAT(conn, smb_fname_cwd);
	if (ret == -1) {
		status = map_nt_error_from_unix(errno);
		DEBUG(0,("change_dir_owner_to_parent: failed to stat "
			 "directory '.' (%s) Error was %s\n",
			 fname, strerror(errno)));
		goto chdir;
	}

	/* Ensure we're pointing at the same place. */
	if (smb_fname_cwd->st.st_ex_dev != psbuf->st_ex_dev ||
	    smb_fname_cwd->st.st_ex_ino != psbuf->st_ex_ino) {
		DEBUG(0,("change_dir_owner_to_parent: "
			 "device/inode on directory %s changed. "
			 "Refusing to chown !\n", fname ));
		status = NT_STATUS_ACCESS_DENIED;
		goto chdir;
	}

	if (smb_fname_parent->st.st_ex_uid == smb_fname_cwd->st.st_ex_uid) {
		/* Already this uid - no need to change. */
		DEBUG(10,("change_dir_owner_to_parent: directory %s "
			"is already owned by uid %d\n",
			fname,
			(int)smb_fname_cwd->st.st_ex_uid ));
		status = NT_STATUS_OK;
		goto chdir;
	}

	become_root();
	ret = SMB_VFS_LCHOWN(conn,
			smb_fname_cwd,
			smb_fname_parent->st.st_ex_uid,
			(gid_t)-1);
	unbecome_root();
	if (ret == -1) {
		status = map_nt_error_from_unix(errno);
		DEBUG(10,("change_dir_owner_to_parent: failed to chown "
			  "directory %s to parent directory uid %u. "
			  "Error was %s\n", fname,
			  (unsigned int)smb_fname_parent->st.st_ex_uid,
			  strerror(errno) ));
	} else {
		DEBUG(10,("change_dir_owner_to_parent: changed ownership of new "
			"directory %s to parent directory uid %u.\n",
			fname, (unsigned int)smb_fname_parent->st.st_ex_uid ));
		/* Ensure the uid entry is updated. */
		psbuf->st_ex_uid = smb_fname_parent->st.st_ex_uid;
	}

 chdir:
	vfs_ChDir(conn,saved_dir);
 out:
	TALLOC_FREE(smb_fname_parent);
	TALLOC_FREE(smb_fname_cwd);
	return status;
}