Commit message: nbd-server: handle modern-style negotiation in a child process

Previously, the modern style negotiation was carried out in the root
server (listener) process before forking the actual client handler. This
made it possible for a malfunctioning or evil client to terminate the
root process simply by querying a non-existent export or aborting in the
middle of the negotation process (caused SIGPIPE in the server).

This commit moves the negotiation process to the child to keep the root
process up and running no matter what happens during the negotiation.

See http://sourceforge.net/mailarchive/message.php?msg_id=30410146

Signed-off-by: Tuomas Räsänen <tuomasjjrasanen@tjjr.fi>
CVE: CVE-2013-7441
CWE: CWE-399
Vulnerability description: The modern style negotiation in Network Block Device (nbd-server) 2.9.22 through 3.3 allows remote attackers to cause a denial of service (root process termination) by (1) closing the connection during negotiation or (2) specifying a name for a non-existent export.

Project: nbd

int append_serve(const SERVER *const s, GArray *const a) {
	SERVER *ns = NULL;
	struct addrinfo hints;
	struct addrinfo *ai = NULL;
	struct addrinfo *rp = NULL;
	char   host[NI_MAXHOST];
	gchar  *port = NULL;
	int e;
	int ret;

	assert(s != NULL);

	port = g_strdup_printf("%d", s->port);

	memset(&hints,'\0',sizeof(hints));
	hints.ai_family = AF_UNSPEC;
	hints.ai_socktype = SOCK_STREAM;
	hints.ai_flags = AI_ADDRCONFIG | AI_PASSIVE;
	hints.ai_protocol = IPPROTO_TCP;

	e = getaddrinfo(s->listenaddr, port, &hints, &ai);

	if (port)
		g_free(port);

	if(e == 0) {
		for (rp = ai; rp != NULL; rp = rp->ai_next) {
			e = getnameinfo(rp->ai_addr, rp->ai_addrlen, host, sizeof(host), NULL, 0, NI_NUMERICHOST);

			if (e != 0) { // error
				fprintf(stderr, "getnameinfo: %s\n", gai_strerror(e));
				continue;
			}

			ns = dup_serve (s);
			if (ns) {
				ns->listenaddr = g_strdup(host);
				ns->socket_family = rp->ai_family;
				g_array_append_val(a, *ns);
				free(ns);
				ns = NULL;
			}
		}

		ret = 0;
	} else {
		fprintf(stderr, "getaddrinfo failed on listen host/address: %s (%s)\n", s->listenaddr ? s->listenaddr : "any", gai_strerror(e));
		ret = -1;
	}

	if (ai)
		freeaddrinfo(ai);

	return ret;
}

Project: nbd
File: nbd-server.c

SERVER* dup_serve(const SERVER *const s) {
	SERVER *serve = NULL;

	serve=g_new0(SERVER, 1);
	if(serve == NULL)
		return NULL;

	if(s->exportname)
		serve->exportname = g_strdup(s->exportname);

	serve->expected_size = s->expected_size;

	if(s->listenaddr)
		serve->listenaddr = g_strdup(s->listenaddr);

	serve->port = s->port;

	if(s->authname)
		serve->authname = strdup(s->authname);

	serve->flags = s->flags;
	serve->socket = s->socket;
	serve->socket_family = s->socket_family;
	serve->virtstyle = s->virtstyle;
	serve->cidrlen = s->cidrlen;

	if(s->prerun)
		serve->prerun = g_strdup(s->prerun);

	if(s->postrun)
		serve->postrun = g_strdup(s->postrun);

	if(s->transactionlog)
		serve->transactionlog = g_strdup(s->transactionlog);
	
	if(s->servename)
		serve->servename = g_strdup(s->servename);

	serve->max_connections = s->max_connections;

	return serve;
}

Project: nbd
File: nbd-server.c

 handle_connection(GArray *servers, int net, SERVER *serve, CLIENT *client)
 {
	int sock_flags_old;
	int sock_flags_new;

	if(serve->max_connections > 0 &&
	   g_hash_table_size(children) >= serve->max_connections) {
		msg(LOG_INFO, "Max connections reached");
		goto handle_connection_out;
	}
	if((sock_flags_old = fcntl(net, F_GETFL, 0)) == -1) {
		err("fcntl F_GETFL");
	}
	sock_flags_new = sock_flags_old & ~O_NONBLOCK;
	if (sock_flags_new != sock_flags_old &&
	    fcntl(net, F_SETFL, sock_flags_new) == -1) {
		err("fcntl F_SETFL ~O_NONBLOCK");
	}
	if(!client) {
		client = g_new0(CLIENT, 1);
		client->server=serve;
		client->exportsize=OFFT_MAX;
		client->net=net;
		client->transactionlogfd = -1;
	}
	if (set_peername(net, client)) {
		goto handle_connection_out;
	}
	if (!authorized_client(client)) {
		msg(LOG_INFO, "Unauthorized client");
		goto handle_connection_out;
	}
	msg(LOG_INFO, "Authorized client");

	if (!dontfork) {
		pid_t pid;
		int i;
		sigset_t newset;
		sigset_t oldset;

		sigemptyset(&newset);
		sigaddset(&newset, SIGCHLD);
		sigaddset(&newset, SIGTERM);
		sigprocmask(SIG_BLOCK, &newset, &oldset);
		if ((pid = fork()) < 0) {
			msg(LOG_INFO, "Could not fork (%s)", strerror(errno));
			sigprocmask(SIG_SETMASK, &oldset, NULL);
			goto handle_connection_out;
		}
		if (pid > 0) { /* parent */
			pid_t *pidp;

			pidp = g_malloc(sizeof(pid_t));
			*pidp = pid;
			g_hash_table_insert(children, pidp, pidp);
			sigprocmask(SIG_SETMASK, &oldset, NULL);
			goto handle_connection_out;
		}
		/* child */
		signal(SIGCHLD, SIG_DFL);
		signal(SIGTERM, SIG_DFL);
		signal(SIGHUP, SIG_DFL);
		sigprocmask(SIG_SETMASK, &oldset, NULL);

		g_hash_table_destroy(children);
		children = NULL;
		for(i=0;i<servers->len;i++) {
			serve=&g_array_index(servers, SERVER, i);
			close(serve->socket);
		}
		/* FALSE does not free the
		   actual data. This is required,
		   because the client has a
		   direct reference into that
		   data, and otherwise we get a
		   segfault... */
		g_array_free(servers, FALSE);
		for(i=0;i<modernsocks->len;i++) {
			close(g_array_index(modernsocks, int, i));
		}
		g_array_free(modernsocks, TRUE);
	}

	msg(LOG_INFO, "Starting to serve");
	serveconnection(client);
	exit(EXIT_SUCCESS);

handle_connection_out:
	g_free(client);
	close(net);
}

Project: nbd
File: nbd-server.c

static void handle_list(uint32_t opt, int net, GArray* servers, uint32_t cflags) {
	uint32_t len;
	int i;
	char buf[1024];
	char *ptr = buf + sizeof(len);

	if (read(net, &len, sizeof(len)) < 0)
		err("Negotiation failed/8: %m");
	len = ntohl(len);
	if(len) {
		send_reply(opt, net, NBD_REP_ERR_INVALID, 0, NULL);
	}
	if(!(glob_flags & F_LIST)) {
		send_reply(opt, net, NBD_REP_ERR_POLICY, 0, NULL);
		err_nonfatal("Client tried disallowed list option");
		return;
	}
	for(i=0; i<servers->len; i++) {
		SERVER* serve = &(g_array_index(servers, SERVER, i));
		len = htonl(strlen(serve->servename));
		memcpy(buf, &len, sizeof(len));
		strcpy(ptr, serve->servename);
		send_reply(opt, net, NBD_REP_SERVER, strlen(serve->servename)+sizeof(len), buf);
	}
	send_reply(opt, net, NBD_REP_ACK, 0, NULL);
}

Project: nbd
File: nbd-server.c

off_t size_autodetect(int fhandle) {
	off_t es;
	u64 bytes __attribute__((unused));
	struct stat stat_buf;
	int error;

#ifdef HAVE_SYS_MOUNT_H
#ifdef HAVE_SYS_IOCTL_H
#ifdef BLKGETSIZE64
	DEBUG("looking for export size with ioctl BLKGETSIZE64\n");
	if (!ioctl(fhandle, BLKGETSIZE64, &bytes) && bytes) {
		return (off_t)bytes;
	}
#endif /* BLKGETSIZE64 */
#endif /* HAVE_SYS_IOCTL_H */
#endif /* HAVE_SYS_MOUNT_H */

	DEBUG("looking for fhandle size with fstat\n");
	stat_buf.st_size = 0;
	error = fstat(fhandle, &stat_buf);
	if (!error) {
		/* always believe stat if a regular file as it might really
		 * be zero length */
		if (S_ISREG(stat_buf.st_mode) || (stat_buf.st_size > 0))
			return (off_t)stat_buf.st_size;
        } else {
                err("fstat failed: %m");
        }

	DEBUG("looking for fhandle size with lseek SEEK_END\n");
	es = lseek(fhandle, (off_t)0, SEEK_END);
	if (es > ((off_t)0)) {
		return es;
        } else {
                DEBUG("lseek failed: %d", errno==EBADF?1:(errno==ESPIPE?2:(errno==EINVAL?3:4)));
        }

	err("Could not find size of exported block device: %m");
}