drbd: rcu_read_lock() and rcu_dereference() for tconn->net_conf

Removing the get_net_conf()/put_net_conf() calls

Signed-off-by: Philipp Reisner <philipp.reisner@linbit.com>
Signed-off-by: Lars Ellenberg <lars.ellenberg@linbit.com>
diff --git a/drivers/block/drbd/drbd_nl.c b/drivers/block/drbd/drbd_nl.c
index d4b29fd6..34be842 100644
--- a/drivers/block/drbd/drbd_nl.c
+++ b/drivers/block/drbd/drbd_nl.c
@@ -257,27 +257,30 @@
 static void setup_khelper_env(struct drbd_tconn *tconn, char **envp)
 {
 	char *afs;
+	struct net_conf *nc;
 
-	if (get_net_conf(tconn)) {
-		switch (((struct sockaddr *)tconn->net_conf->peer_addr)->sa_family) {
+	rcu_read_lock();
+	nc = rcu_dereference(tconn->net_conf);
+	if (nc) {
+		switch (((struct sockaddr *)nc->peer_addr)->sa_family) {
 		case AF_INET6:
 			afs = "ipv6";
 			snprintf(envp[4], 60, "DRBD_PEER_ADDRESS=%pI6",
-				 &((struct sockaddr_in6 *)tconn->net_conf->peer_addr)->sin6_addr);
+				 &((struct sockaddr_in6 *)nc->peer_addr)->sin6_addr);
 			break;
 		case AF_INET:
 			afs = "ipv4";
 			snprintf(envp[4], 60, "DRBD_PEER_ADDRESS=%pI4",
-				 &((struct sockaddr_in *)tconn->net_conf->peer_addr)->sin_addr);
+				 &((struct sockaddr_in *)nc->peer_addr)->sin_addr);
 			break;
 		default:
 			afs = "ssocks";
 			snprintf(envp[4], 60, "DRBD_PEER_ADDRESS=%pI4",
-				 &((struct sockaddr_in *)tconn->net_conf->peer_addr)->sin_addr);
+				 &((struct sockaddr_in *)nc->peer_addr)->sin_addr);
 		}
 		snprintf(envp[3], 20, "DRBD_PEER_AF=%s", afs);
-		put_net_conf(tconn);
 	}
+	rcu_read_unlock();
 }
 
 int drbd_khelper(struct drbd_conf *mdev, char *cmd)
@@ -493,6 +496,7 @@
 {
 	const int max_tries = 4;
 	enum drbd_state_rv rv = SS_UNKNOWN_ERROR;
+	struct net_conf *nc;
 	int try = 0;
 	int forced = 0;
 	union drbd_state mask, val;
@@ -550,7 +554,12 @@
 		if (rv == SS_TWO_PRIMARIES) {
 			/* Maybe the peer is detected as dead very soon...
 			   retry at most once more in this case. */
-			schedule_timeout_interruptible((mdev->tconn->net_conf->ping_timeo+1)*HZ/10);
+			int timeo;
+			rcu_read_lock();
+			nc = rcu_dereference(mdev->tconn->net_conf);
+			timeo = nc ? (nc->ping_timeo + 1) * HZ / 10 : 1;
+			rcu_read_unlock();
+			schedule_timeout_interruptible(timeo);
 			if (try < max_tries)
 				try = max_tries - 1;
 			continue;
@@ -580,10 +589,11 @@
 			put_ldev(mdev);
 		}
 	} else {
-		if (get_net_conf(mdev->tconn)) {
-			mdev->tconn->net_conf->want_lose = 0;
-			put_net_conf(mdev->tconn);
-		}
+		rcu_read_lock();
+		nc = rcu_dereference(mdev->tconn->net_conf);
+		if (nc)
+			nc->want_lose = 0;
+		rcu_read_unlock();
 		set_disk_ro(mdev->vdisk, false);
 		if (get_ldev(mdev)) {
 			if (((mdev->state.conn < C_CONNECTED ||
@@ -1193,6 +1203,7 @@
 	struct lru_cache *resync_lru = NULL;
 	union drbd_state ns, os;
 	enum drbd_state_rv rv;
+	struct net_conf *nc;
 	int cp_discovered = 0;
 
 	retcode = drbd_adm_prepare(skb, info, DRBD_ADM_NEED_MINOR);
@@ -1256,14 +1267,16 @@
 		goto fail;
 	}
 
-	if (get_net_conf(mdev->tconn)) {
-		int prot = mdev->tconn->net_conf->wire_protocol;
-		put_net_conf(mdev->tconn);
-		if (nbc->dc.fencing == FP_STONITH && prot == DRBD_PROT_A) {
+	rcu_read_lock();
+	nc = rcu_dereference(mdev->tconn->net_conf);
+	if (nc) {
+		if (nbc->dc.fencing == FP_STONITH && nc->wire_protocol == DRBD_PROT_A) {
+			rcu_read_unlock();
 			retcode = ERR_STONITH_AND_PROT_A;
 			goto fail;
 		}
 	}
+	rcu_read_unlock();
 
 	bdev = blkdev_get_by_path(nbc->dc.backing_dev,
 				  FMODE_READ | FMODE_WRITE | FMODE_EXCL, mdev);
@@ -1666,42 +1679,30 @@
 }
 
 static enum drbd_ret_code
-check_net_options(struct drbd_tconn *tconn, struct net_conf *new_conf)
+_check_net_options(struct drbd_tconn *tconn, struct net_conf *old_conf, struct net_conf *new_conf)
 {
 	struct drbd_conf *mdev;
 	int i;
 
-	if (tconn->net_conf && tconn->agreed_pro_version < 100 &&
+	if (old_conf && tconn->agreed_pro_version < 100 &&
 	    tconn->cstate == C_WF_REPORT_PARAMS &&
-	    new_conf->wire_protocol != tconn->net_conf->wire_protocol)
+	    new_conf->wire_protocol != old_conf->wire_protocol)
 		return ERR_NEED_APV_100;
 
 	if (new_conf->two_primaries &&
 	    (new_conf->wire_protocol != DRBD_PROT_C))
 		return ERR_NOT_PROTO_C;
 
-	rcu_read_lock();
 	idr_for_each_entry(&tconn->volumes, mdev, i) {
 		if (get_ldev(mdev)) {
 			enum drbd_fencing_p fp = mdev->ldev->dc.fencing;
 			put_ldev(mdev);
-			if (new_conf->wire_protocol == DRBD_PROT_A && fp == FP_STONITH) {
-				rcu_read_unlock();
+			if (new_conf->wire_protocol == DRBD_PROT_A && fp == FP_STONITH)
 				return ERR_STONITH_AND_PROT_A;
-			}
 		}
-		if (mdev->state.role == R_PRIMARY && new_conf->want_lose) {
-			rcu_read_unlock();
+		if (mdev->state.role == R_PRIMARY && new_conf->want_lose)
 			return ERR_DISCARD;
-		}
-		if (!mdev->bitmap) {
-			if(drbd_bm_init(mdev)) {
-				rcu_read_unlock();
-				return ERR_NOMEM;
-			}
-		}
 	}
-	rcu_read_unlock();
 
 	if (new_conf->on_congestion != OC_BLOCK && new_conf->wire_protocol != DRBD_PROT_A)
 		return ERR_CONG_NOT_PROTO_A;
@@ -1709,11 +1710,33 @@
 	return NO_ERROR;
 }
 
+static enum drbd_ret_code
+check_net_options(struct drbd_tconn *tconn, struct net_conf *new_conf)
+{
+	static enum drbd_ret_code rv;
+	struct drbd_conf *mdev;
+	int i;
+
+	rcu_read_lock();
+	rv = _check_net_options(tconn, rcu_dereference(tconn->net_conf), new_conf);
+	rcu_read_unlock();
+
+	/* tconn->volumes protected by genl_lock() here */
+	idr_for_each_entry(&tconn->volumes, mdev, i) {
+		if (!mdev->bitmap) {
+			if(drbd_bm_init(mdev))
+				return ERR_NOMEM;
+		}
+	}
+
+	return rv;
+}
+
 int drbd_adm_net_opts(struct sk_buff *skb, struct genl_info *info)
 {
 	enum drbd_ret_code retcode;
 	struct drbd_tconn *tconn;
-	struct net_conf *new_conf = NULL;
+	struct net_conf *old_conf, *new_conf = NULL;
 	int err;
 	int ovr; /* online verify running */
 	int rsr; /* re-sync running */
@@ -1735,17 +1758,20 @@
 		goto out;
 	}
 
-	/* we also need a net config
-	 * to change the options on */
-	if (!get_net_conf(tconn)) {
-		drbd_msg_put_info("net conf missing, try connect");
-		retcode = ERR_INVALID_REQUEST;
-		goto out;
-	}
-
 	conn_reconfig_start(tconn);
 
-	memcpy(new_conf, tconn->net_conf, sizeof(*new_conf));
+	rcu_read_lock();
+	old_conf = rcu_dereference(tconn->net_conf);
+
+	if (!old_conf) {
+		drbd_msg_put_info("net conf missing, try connect");
+		retcode = ERR_INVALID_REQUEST;
+		goto fail_rcu_unlock;
+	}
+
+	*new_conf = *old_conf;
+	rcu_read_unlock();
+
 	err = net_conf_from_attrs_for_change(new_conf, info);
 	if (err) {
 		retcode = ERR_MANDATORY_TAG;
@@ -1759,10 +1785,13 @@
 
 	/* re-sync running */
 	rsr = conn_resync_running(tconn);
-	if (rsr && strcmp(new_conf->csums_alg, tconn->net_conf->csums_alg)) {
+	rcu_read_lock();
+	old_conf = rcu_dereference(tconn->net_conf);
+	if (rsr && old_conf && strcmp(new_conf->csums_alg, old_conf->csums_alg)) {
 		retcode = ERR_CSUMS_RESYNC_RUNNING;
-		goto fail;
+		goto fail_rcu_unlock;
 	}
+	rcu_read_unlock();
 
 	if (!rsr && new_conf->csums_alg[0]) {
 		csums_tfm = crypto_alloc_hash(new_conf->csums_alg, 0, CRYPTO_ALG_ASYNC);
@@ -1780,12 +1809,15 @@
 
 	/* online verify running */
 	ovr = conn_ov_running(tconn);
-	if (ovr) {
-		if (strcmp(new_conf->verify_alg, tconn->net_conf->verify_alg)) {
+	rcu_read_lock();
+	old_conf = rcu_dereference(tconn->net_conf);
+	if (ovr && old_conf) {
+		if (strcmp(new_conf->verify_alg, old_conf->verify_alg)) {
 			retcode = ERR_VERIFY_RUNNING;
-			goto fail;
+			goto fail_rcu_unlock;
 		}
 	}
+	rcu_read_unlock();
 
 	if (!ovr && new_conf->verify_alg[0]) {
 		verify_tfm = crypto_alloc_hash(new_conf->verify_alg, 0, CRYPTO_ALG_ASYNC);
@@ -1801,16 +1833,9 @@
 		}
 	}
 
-
-	/* For now, use struct assignment, not pointer assignment.
-	 * We don't have any means to determine who might still
-	 * keep a local alias into the struct,
-	 * so we cannot just free it and hope for the best :(
-	 * FIXME
-	 * To avoid someone looking at a half-updated struct, we probably
-	 * should have a rw-semaphor on net_conf and disk_conf.
-	 */
-	*tconn->net_conf = *new_conf;
+	rcu_assign_pointer(tconn->net_conf, new_conf);
+	synchronize_rcu();
+	kfree(old_conf);
 
 	if (!rsr) {
 		crypto_free_hash(tconn->csums_tfm);
@@ -1826,11 +1851,12 @@
 	if (tconn->cstate >= C_WF_REPORT_PARAMS)
 		drbd_send_sync_param(minor_to_mdev(conn_lowest_minor(tconn)));
 
+ fail_rcu_unlock:
+	rcu_read_unlock();
  fail:
 	crypto_free_hash(csums_tfm);
 	crypto_free_hash(verify_tfm);
 	kfree(new_conf);
-	put_net_conf(tconn);
 	conn_reconfig_done(tconn);
  out:
 	drbd_adm_finish(info, retcode);
@@ -1841,7 +1867,7 @@
 {
 	char hmac_name[CRYPTO_MAX_ALG_NAME];
 	struct drbd_conf *mdev;
-	struct net_conf *new_conf = NULL;
+	struct net_conf *old_conf, *new_conf = NULL;
 	struct crypto_hash *tfm = NULL;
 	struct crypto_hash *integrity_w_tfm = NULL;
 	struct crypto_hash *integrity_r_tfm = NULL;
@@ -1929,23 +1955,26 @@
 	 * strictly serialized on genl_lock(). We are protected against
 	 * concurrent reconfiguration/addition/deletion */
 	list_for_each_entry(oconn, &drbd_tconns, all_tconn) {
+		struct net_conf *nc;
 		if (oconn == tconn)
 			continue;
-		if (get_net_conf(oconn)) {
-			taken_addr = (struct sockaddr *)&oconn->net_conf->my_addr;
-			if (new_conf->my_addr_len == oconn->net_conf->my_addr_len &&
+
+		rcu_read_lock();
+		nc = rcu_dereference(oconn->net_conf);
+		if (nc) {
+			taken_addr = (struct sockaddr *)&nc->my_addr;
+			if (new_conf->my_addr_len == nc->my_addr_len &&
 			    !memcmp(new_my_addr, taken_addr, new_conf->my_addr_len))
 				retcode = ERR_LOCAL_ADDR;
 
-			taken_addr = (struct sockaddr *)&oconn->net_conf->peer_addr;
-			if (new_conf->peer_addr_len == oconn->net_conf->peer_addr_len &&
+			taken_addr = (struct sockaddr *)&nc->peer_addr;
+			if (new_conf->peer_addr_len == nc->peer_addr_len &&
 			    !memcmp(new_peer_addr, taken_addr, new_conf->peer_addr_len))
 				retcode = ERR_PEER_ADDR;
-
-			put_net_conf(oconn);
-			if (retcode != NO_ERROR)
-				goto fail;
 		}
+		rcu_read_unlock();
+		if (retcode != NO_ERROR)
+			goto fail;
 	}
 
 	if (new_conf->cram_hmac_alg[0] != 0) {
@@ -2004,12 +2033,15 @@
 
 	conn_flush_workqueue(tconn);
 	spin_lock_irq(&tconn->req_lock);
-	if (tconn->net_conf != NULL) {
+	rcu_read_lock();
+	old_conf = rcu_dereference(tconn->net_conf);
+	if (old_conf != NULL) {
 		retcode = ERR_NET_CONFIGURED;
+		rcu_read_unlock();
 		spin_unlock_irq(&tconn->req_lock);
 		goto fail;
 	}
-	tconn->net_conf = new_conf;
+	rcu_assign_pointer(tconn->net_conf, new_conf);
 
 	crypto_free_hash(tconn->cram_hmac_tfm);
 	tconn->cram_hmac_tfm = tfm;
@@ -2464,9 +2496,9 @@
 		const struct sib_info *sib)
 {
 	struct state_info *si = NULL; /* for sizeof(si->member); */
+	struct net_conf *nc;
 	struct nlattr *nla;
 	int got_ldev;
-	int got_net;
 	int err = 0;
 	int exclude_sensitive;
 
@@ -2484,7 +2516,6 @@
 	exclude_sensitive = sib || !capable(CAP_SYS_ADMIN);
 
 	got_ldev = get_ldev(mdev);
-	got_net = get_net_conf(mdev->tconn);
 
 	/* We need to add connection name and volume number information still.
 	 * Minor number is in drbd_genlmsghdr. */
@@ -2497,9 +2528,14 @@
 	if (got_ldev)
 		if (disk_conf_to_skb(skb, &mdev->ldev->dc, exclude_sensitive))
 			goto nla_put_failure;
-	if (got_net)
-		if (net_conf_to_skb(skb, mdev->tconn->net_conf, exclude_sensitive))
-			goto nla_put_failure;
+
+	rcu_read_lock();
+	nc = rcu_dereference(mdev->tconn->net_conf);
+	if (nc)
+		err = net_conf_to_skb(skb, nc, exclude_sensitive);
+	rcu_read_unlock();
+	if (err)
+		goto nla_put_failure;
 
 	nla = nla_nest_start(skb, DRBD_NLA_STATE_INFO);
 	if (!nla)
@@ -2546,8 +2582,6 @@
 		err = -EMSGSIZE;
 	if (got_ldev)
 		put_ldev(mdev);
-	if (got_net)
-		put_net_conf(mdev->tconn);
 	return err;
 }