smb_receive calls kernel_recvmsg with a size that's the minimum of the amount of buffer space in the kvec passed in or req->rq_rlen (which represents the length of the response). This does not take into account any data that was read in a request earlier pass through smb_receive. If the first pass through smb_receive receives some but not all of the response, then the next pass can call kernel_recvmsg with a size field that's too big. kernel_recvmsg can overrun into the next response, throwing off the alignment and making it unrecognizable. This causes messages like this to pop up in the ring buffer: smb_get_length: Invalid NBT packet, code=69 as well as other errors indicating that the response is unrecognizable. Typically this is seen on a smbfs mount under heavy I/O. This patch changes the code to use (req->rq_rlen - req->rq_bytes_recvd) instead instead of just req->rq_rlen, since that should represent the amount of unread data in the response. I think this is correct, but an ACK or NACK from someone more familiar with this code would be appreciated... Signed-off-by: Jeff Layton <jlayton@redhat.com> Signed-off-by: Andrew Morton <akpm@linux-foundation.org> Signed-off-by: Linus Torvalds <torvalds@linux-foundation.org>
		
			
				
	
	
		
			386 lines
		
	
	
	
		
			7.8 KiB
			
		
	
	
	
		
			C
		
	
	
	
	
	
			
		
		
	
	
			386 lines
		
	
	
	
		
			7.8 KiB
			
		
	
	
	
		
			C
		
	
	
	
	
	
/*
 | 
						|
 *  sock.c
 | 
						|
 *
 | 
						|
 *  Copyright (C) 1995, 1996 by Paal-Kr. Engstad and Volker Lendecke
 | 
						|
 *  Copyright (C) 1997 by Volker Lendecke
 | 
						|
 *
 | 
						|
 *  Please add a note about your changes to smbfs in the ChangeLog file.
 | 
						|
 */
 | 
						|
 | 
						|
#include <linux/fs.h>
 | 
						|
#include <linux/time.h>
 | 
						|
#include <linux/errno.h>
 | 
						|
#include <linux/socket.h>
 | 
						|
#include <linux/fcntl.h>
 | 
						|
#include <linux/file.h>
 | 
						|
#include <linux/in.h>
 | 
						|
#include <linux/net.h>
 | 
						|
#include <linux/mm.h>
 | 
						|
#include <linux/netdevice.h>
 | 
						|
#include <linux/workqueue.h>
 | 
						|
#include <net/scm.h>
 | 
						|
#include <net/tcp_states.h>
 | 
						|
#include <net/ip.h>
 | 
						|
 | 
						|
#include <linux/smb_fs.h>
 | 
						|
#include <linux/smb.h>
 | 
						|
#include <linux/smbno.h>
 | 
						|
 | 
						|
#include <asm/uaccess.h>
 | 
						|
#include <asm/ioctls.h>
 | 
						|
 | 
						|
#include "smb_debug.h"
 | 
						|
#include "proto.h"
 | 
						|
#include "request.h"
 | 
						|
 | 
						|
 | 
						|
static int
 | 
						|
_recvfrom(struct socket *socket, unsigned char *ubuf, int size, unsigned flags)
 | 
						|
{
 | 
						|
	struct kvec iov = {ubuf, size};
 | 
						|
	struct msghdr msg = {.msg_flags = flags};
 | 
						|
	msg.msg_flags |= MSG_DONTWAIT | MSG_NOSIGNAL;
 | 
						|
	return kernel_recvmsg(socket, &msg, &iov, 1, size, msg.msg_flags);
 | 
						|
}
 | 
						|
 | 
						|
/*
 | 
						|
 * Return the server this socket belongs to
 | 
						|
 */
 | 
						|
static struct smb_sb_info *
 | 
						|
server_from_socket(struct socket *socket)
 | 
						|
{
 | 
						|
	return socket->sk->sk_user_data;
 | 
						|
}
 | 
						|
 | 
						|
/*
 | 
						|
 * Called when there is data on the socket.
 | 
						|
 */
 | 
						|
void
 | 
						|
smb_data_ready(struct sock *sk, int len)
 | 
						|
{
 | 
						|
	struct smb_sb_info *server = server_from_socket(sk->sk_socket);
 | 
						|
	void (*data_ready)(struct sock *, int) = server->data_ready;
 | 
						|
 | 
						|
	data_ready(sk, len);
 | 
						|
	VERBOSE("(%p, %d)\n", sk, len);
 | 
						|
	smbiod_wake_up();
 | 
						|
}
 | 
						|
 | 
						|
int
 | 
						|
smb_valid_socket(struct inode * inode)
 | 
						|
{
 | 
						|
	return (inode && S_ISSOCK(inode->i_mode) && 
 | 
						|
		SOCKET_I(inode)->type == SOCK_STREAM);
 | 
						|
}
 | 
						|
 | 
						|
static struct socket *
 | 
						|
server_sock(struct smb_sb_info *server)
 | 
						|
{
 | 
						|
	struct file *file;
 | 
						|
 | 
						|
	if (server && (file = server->sock_file))
 | 
						|
	{
 | 
						|
#ifdef SMBFS_PARANOIA
 | 
						|
		if (!smb_valid_socket(file->f_path.dentry->d_inode))
 | 
						|
			PARANOIA("bad socket!\n");
 | 
						|
#endif
 | 
						|
		return SOCKET_I(file->f_path.dentry->d_inode);
 | 
						|
	}
 | 
						|
	return NULL;
 | 
						|
}
 | 
						|
 | 
						|
void
 | 
						|
smb_close_socket(struct smb_sb_info *server)
 | 
						|
{
 | 
						|
	struct file * file = server->sock_file;
 | 
						|
 | 
						|
	if (file) {
 | 
						|
		struct socket *sock = server_sock(server);
 | 
						|
 | 
						|
		VERBOSE("closing socket %p\n", sock);
 | 
						|
		sock->sk->sk_data_ready = server->data_ready;
 | 
						|
		server->sock_file = NULL;
 | 
						|
		fput(file);
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
static int
 | 
						|
smb_get_length(struct socket *socket, unsigned char *header)
 | 
						|
{
 | 
						|
	int result;
 | 
						|
 | 
						|
	result = _recvfrom(socket, header, 4, MSG_PEEK);
 | 
						|
	if (result == -EAGAIN)
 | 
						|
		return -ENODATA;
 | 
						|
	if (result < 0) {
 | 
						|
		PARANOIA("recv error = %d\n", -result);
 | 
						|
		return result;
 | 
						|
	}
 | 
						|
	if (result < 4)
 | 
						|
		return -ENODATA;
 | 
						|
 | 
						|
	switch (header[0]) {
 | 
						|
	case 0x00:
 | 
						|
	case 0x82:
 | 
						|
		break;
 | 
						|
 | 
						|
	case 0x85:
 | 
						|
		DEBUG1("Got SESSION KEEP ALIVE\n");
 | 
						|
		_recvfrom(socket, header, 4, 0);	/* read away */
 | 
						|
		return -ENODATA;
 | 
						|
 | 
						|
	default:
 | 
						|
		PARANOIA("Invalid NBT packet, code=%x\n", header[0]);
 | 
						|
		return -EIO;
 | 
						|
	}
 | 
						|
 | 
						|
	/* The length in the RFC NB header is the raw data length */
 | 
						|
	return smb_len(header);
 | 
						|
}
 | 
						|
 | 
						|
int
 | 
						|
smb_recv_available(struct smb_sb_info *server)
 | 
						|
{
 | 
						|
	mm_segment_t oldfs;
 | 
						|
	int avail, err;
 | 
						|
	struct socket *sock = server_sock(server);
 | 
						|
 | 
						|
	oldfs = get_fs();
 | 
						|
	set_fs(get_ds());
 | 
						|
	err = sock->ops->ioctl(sock, SIOCINQ, (unsigned long) &avail);
 | 
						|
	set_fs(oldfs);
 | 
						|
	return (err >= 0) ? avail : err;
 | 
						|
}
 | 
						|
 | 
						|
/*
 | 
						|
 * Adjust the kvec to move on 'n' bytes (from nfs/sunrpc)
 | 
						|
 */
 | 
						|
static int
 | 
						|
smb_move_iov(struct kvec **data, size_t *num, struct kvec *vec, unsigned amount)
 | 
						|
{
 | 
						|
	struct kvec *iv = *data;
 | 
						|
	int i;
 | 
						|
	int len;
 | 
						|
 | 
						|
	/*
 | 
						|
	 *	Eat any sent kvecs
 | 
						|
	 */
 | 
						|
	while (iv->iov_len <= amount) {
 | 
						|
		amount -= iv->iov_len;
 | 
						|
		iv++;
 | 
						|
		(*num)--;
 | 
						|
	}
 | 
						|
 | 
						|
	/*
 | 
						|
	 *	And chew down the partial one
 | 
						|
	 */
 | 
						|
	vec[0].iov_len = iv->iov_len-amount;
 | 
						|
	vec[0].iov_base =((unsigned char *)iv->iov_base)+amount;
 | 
						|
	iv++;
 | 
						|
 | 
						|
	len = vec[0].iov_len;
 | 
						|
 | 
						|
	/*
 | 
						|
	 *	And copy any others
 | 
						|
	 */
 | 
						|
	for (i = 1; i < *num; i++) {
 | 
						|
		vec[i] = *iv++;
 | 
						|
		len += vec[i].iov_len;
 | 
						|
	}
 | 
						|
 | 
						|
	*data = vec;
 | 
						|
	return len;
 | 
						|
}
 | 
						|
 | 
						|
/*
 | 
						|
 * smb_receive_header
 | 
						|
 * Only called by the smbiod thread.
 | 
						|
 */
 | 
						|
int
 | 
						|
smb_receive_header(struct smb_sb_info *server)
 | 
						|
{
 | 
						|
	struct socket *sock;
 | 
						|
	int result = 0;
 | 
						|
	unsigned char peek_buf[4];
 | 
						|
 | 
						|
	result = -EIO; 
 | 
						|
	sock = server_sock(server);
 | 
						|
	if (!sock)
 | 
						|
		goto out;
 | 
						|
	if (sock->sk->sk_state != TCP_ESTABLISHED)
 | 
						|
		goto out;
 | 
						|
 | 
						|
	if (!server->smb_read) {
 | 
						|
		result = smb_get_length(sock, peek_buf);
 | 
						|
		if (result < 0) {
 | 
						|
			if (result == -ENODATA)
 | 
						|
				result = 0;
 | 
						|
			goto out;
 | 
						|
		}
 | 
						|
		server->smb_len = result + 4;
 | 
						|
 | 
						|
		if (server->smb_len < SMB_HEADER_LEN) {
 | 
						|
			PARANOIA("short packet: %d\n", result);
 | 
						|
			server->rstate = SMB_RECV_DROP;
 | 
						|
			result = -EIO;
 | 
						|
			goto out;
 | 
						|
		}
 | 
						|
		if (server->smb_len > SMB_MAX_PACKET_SIZE) {
 | 
						|
			PARANOIA("long packet: %d\n", result);
 | 
						|
			server->rstate = SMB_RECV_DROP;
 | 
						|
			result = -EIO;
 | 
						|
			goto out;
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	result = _recvfrom(sock, server->header + server->smb_read,
 | 
						|
			   SMB_HEADER_LEN - server->smb_read, 0);
 | 
						|
	VERBOSE("_recvfrom: %d\n", result);
 | 
						|
	if (result < 0) {
 | 
						|
		VERBOSE("receive error: %d\n", result);
 | 
						|
		goto out;
 | 
						|
	}
 | 
						|
	server->smb_read += result;
 | 
						|
 | 
						|
	if (server->smb_read == SMB_HEADER_LEN)
 | 
						|
		server->rstate = SMB_RECV_HCOMPLETE;
 | 
						|
out:
 | 
						|
	return result;
 | 
						|
}
 | 
						|
 | 
						|
static char drop_buffer[PAGE_SIZE];
 | 
						|
 | 
						|
/*
 | 
						|
 * smb_receive_drop - read and throw away the data
 | 
						|
 * Only called by the smbiod thread.
 | 
						|
 *
 | 
						|
 * FIXME: we are in the kernel, could we just tell the socket that we want
 | 
						|
 * to drop stuff from the buffer?
 | 
						|
 */
 | 
						|
int
 | 
						|
smb_receive_drop(struct smb_sb_info *server)
 | 
						|
{
 | 
						|
	struct socket *sock;
 | 
						|
	unsigned int flags;
 | 
						|
	struct kvec iov;
 | 
						|
	struct msghdr msg;
 | 
						|
	int rlen = smb_len(server->header) - server->smb_read + 4;
 | 
						|
	int result = -EIO;
 | 
						|
 | 
						|
	if (rlen > PAGE_SIZE)
 | 
						|
		rlen = PAGE_SIZE;
 | 
						|
 | 
						|
	sock = server_sock(server);
 | 
						|
	if (!sock)
 | 
						|
		goto out;
 | 
						|
	if (sock->sk->sk_state != TCP_ESTABLISHED)
 | 
						|
		goto out;
 | 
						|
 | 
						|
	flags = MSG_DONTWAIT | MSG_NOSIGNAL;
 | 
						|
	iov.iov_base = drop_buffer;
 | 
						|
	iov.iov_len = PAGE_SIZE;
 | 
						|
	msg.msg_flags = flags;
 | 
						|
	msg.msg_name = NULL;
 | 
						|
	msg.msg_namelen = 0;
 | 
						|
	msg.msg_control = NULL;
 | 
						|
 | 
						|
	result = kernel_recvmsg(sock, &msg, &iov, 1, rlen, flags);
 | 
						|
 | 
						|
	VERBOSE("read: %d\n", result);
 | 
						|
	if (result < 0) {
 | 
						|
		VERBOSE("receive error: %d\n", result);
 | 
						|
		goto out;
 | 
						|
	}
 | 
						|
	server->smb_read += result;
 | 
						|
 | 
						|
	if (server->smb_read >= server->smb_len)
 | 
						|
		server->rstate = SMB_RECV_END;
 | 
						|
 | 
						|
out:
 | 
						|
	return result;
 | 
						|
}
 | 
						|
 | 
						|
/*
 | 
						|
 * smb_receive
 | 
						|
 * Only called by the smbiod thread.
 | 
						|
 */
 | 
						|
int
 | 
						|
smb_receive(struct smb_sb_info *server, struct smb_request *req)
 | 
						|
{
 | 
						|
	struct socket *sock;
 | 
						|
	unsigned int flags;
 | 
						|
	struct kvec iov[4];
 | 
						|
	struct kvec *p = req->rq_iov;
 | 
						|
	size_t num = req->rq_iovlen;
 | 
						|
	struct msghdr msg;
 | 
						|
	int rlen;
 | 
						|
	int result = -EIO;
 | 
						|
 | 
						|
	sock = server_sock(server);
 | 
						|
	if (!sock)
 | 
						|
		goto out;
 | 
						|
	if (sock->sk->sk_state != TCP_ESTABLISHED)
 | 
						|
		goto out;
 | 
						|
 | 
						|
	flags = MSG_DONTWAIT | MSG_NOSIGNAL;
 | 
						|
	msg.msg_flags = flags;
 | 
						|
	msg.msg_name = NULL;
 | 
						|
	msg.msg_namelen = 0;
 | 
						|
	msg.msg_control = NULL;
 | 
						|
 | 
						|
	/* Dont repeat bytes and count available bufferspace */
 | 
						|
	rlen = min_t(int, smb_move_iov(&p, &num, iov, req->rq_bytes_recvd),
 | 
						|
			(req->rq_rlen - req->rq_bytes_recvd));
 | 
						|
 | 
						|
	result = kernel_recvmsg(sock, &msg, p, num, rlen, flags);
 | 
						|
 | 
						|
	VERBOSE("read: %d\n", result);
 | 
						|
	if (result < 0) {
 | 
						|
		VERBOSE("receive error: %d\n", result);
 | 
						|
		goto out;
 | 
						|
	}
 | 
						|
	req->rq_bytes_recvd += result;
 | 
						|
	server->smb_read += result;
 | 
						|
 | 
						|
out:
 | 
						|
	return result;
 | 
						|
}
 | 
						|
 | 
						|
/*
 | 
						|
 * Try to send a SMB request. This may return after sending only parts of the
 | 
						|
 * request. SMB_REQ_TRANSMITTED will be set if a request was fully sent.
 | 
						|
 *
 | 
						|
 * Parts of this was taken from xprt_sendmsg from net/sunrpc/xprt.c
 | 
						|
 */
 | 
						|
int
 | 
						|
smb_send_request(struct smb_request *req)
 | 
						|
{
 | 
						|
	struct smb_sb_info *server = req->rq_server;
 | 
						|
	struct socket *sock;
 | 
						|
	struct msghdr msg = {.msg_flags = MSG_NOSIGNAL | MSG_DONTWAIT};
 | 
						|
        int slen = req->rq_slen - req->rq_bytes_sent;
 | 
						|
	int result = -EIO;
 | 
						|
	struct kvec iov[4];
 | 
						|
	struct kvec *p = req->rq_iov;
 | 
						|
	size_t num = req->rq_iovlen;
 | 
						|
 | 
						|
	sock = server_sock(server);
 | 
						|
	if (!sock)
 | 
						|
		goto out;
 | 
						|
	if (sock->sk->sk_state != TCP_ESTABLISHED)
 | 
						|
		goto out;
 | 
						|
 | 
						|
	/* Dont repeat bytes */
 | 
						|
	if (req->rq_bytes_sent)
 | 
						|
		smb_move_iov(&p, &num, iov, req->rq_bytes_sent);
 | 
						|
 | 
						|
	result = kernel_sendmsg(sock, &msg, p, num, slen);
 | 
						|
 | 
						|
	if (result >= 0) {
 | 
						|
		req->rq_bytes_sent += result;
 | 
						|
		if (req->rq_bytes_sent >= req->rq_slen)
 | 
						|
			req->rq_flags |= SMB_REQ_TRANSMITTED;
 | 
						|
	}
 | 
						|
out:
 | 
						|
	return result;
 | 
						|
}
 |