diff mbox series

[RFC,4/4] PCI: hv: Fix synchronization between channel callback and hv_compose_msi_msg()

Message ID 20220328144244.100228-5-parri.andrea@gmail.com (mailing list archive)
State Superseded
Delegated to: Lorenzo Pieralisi
Headers show
Series PCI: hv: Miscellaneous changes | expand

Commit Message

Andrea Parri March 28, 2022, 2:42 p.m. UTC
Dexuan wrote:

  "[...]  when we disable AccelNet, the host PCI VSP driver sends a
   PCI_EJECT message first, and the channel callback may set
   hpdev->state to hv_pcichild_ejecting on a different CPU.  This can
   cause hv_compose_msi_msg() to exit from the loop and 'return', and
   the on-stack variable 'ctxt' is invalid.  Now, if the response
   message from the host arrives, the channel callback will try to
   access the invalid 'ctxt' variable, and this may cause a crash."

Schematically:

  Hyper-V sends PCI_EJECT msg
    hv_pci_onchannelcallback()
      state = hv_pcichild_ejecting
                                       hv_compose_msi_msg()
                                         alloc and init comp_pkt
                                         state == hv_pcichild_ejecting
  Hyper-V sends VM_PKT_COMP msg
    hv_pci_onchannelcallback()
      retrieve address of comp_pkt
                                         'free' comp_pkt and return
      comp_pkt->completion_func()

Dexuan also showed how the crash can be triggered after introducing
suitable delays in the driver code, thus validating the 'assumption'
that the host can still normally respond to the guest's compose_msi
request after the host has started to eject the PCI device.

Fix the synchronization by leveraging the requestor lock as follows:

  - Before 'return'-ing in hv_compose_msi_msg(), remove the ID (while
    holding the requestor lock) associated to the completion packet.

  - Retrieve the address *and call ->completion_func() within a same
    (requestor) critical section in hv_pci_onchannelcallback().

Fixes: de0aa7b2f97d3 ("PCI: hv: Fix 2 hang issues in hv_compose_msi_msg()")
Reported-by: Wei Hu <weh@microsoft.com>
Reported-by: Dexuan Cui <decui@microsoft.com>
Suggested-by: Michael Kelley <mikelley@microsoft.com>
Signed-off-by: Andrea Parri (Microsoft) <parri.andrea@gmail.com>
---
 drivers/pci/controller/pci-hyperv.c | 83 ++++++++++++++++++++++++++---
 1 file changed, 77 insertions(+), 6 deletions(-)

Comments

Michael Kelley (LINUX) March 31, 2022, 8:04 p.m. UTC | #1
From: Andrea Parri (Microsoft) <parri.andrea@gmail.com> Sent: Monday, March 28, 2022 7:43 AM
> 
> Dexuan wrote:
> 
>   "[...]  when we disable AccelNet, the host PCI VSP driver sends a
>    PCI_EJECT message first, and the channel callback may set
>    hpdev->state to hv_pcichild_ejecting on a different CPU.  This can
>    cause hv_compose_msi_msg() to exit from the loop and 'return', and
>    the on-stack variable 'ctxt' is invalid.  Now, if the response
>    message from the host arrives, the channel callback will try to
>    access the invalid 'ctxt' variable, and this may cause a crash."
> 
> Schematically:
> 
>   Hyper-V sends PCI_EJECT msg
>     hv_pci_onchannelcallback()
>       state = hv_pcichild_ejecting
>                                        hv_compose_msi_msg()
>                                          alloc and init comp_pkt
>                                          state == hv_pcichild_ejecting
>   Hyper-V sends VM_PKT_COMP msg
>     hv_pci_onchannelcallback()
>       retrieve address of comp_pkt
>                                          'free' comp_pkt and return
>       comp_pkt->completion_func()
> 
> Dexuan also showed how the crash can be triggered after introducing
> suitable delays in the driver code, thus validating the 'assumption'
> that the host can still normally respond to the guest's compose_msi
> request after the host has started to eject the PCI device.
> 
> Fix the synchronization by leveraging the requestor lock as follows:
> 
>   - Before 'return'-ing in hv_compose_msi_msg(), remove the ID (while
>     holding the requestor lock) associated to the completion packet.
> 
>   - Retrieve the address *and call ->completion_func() within a same
>     (requestor) critical section in hv_pci_onchannelcallback().
> 
> Fixes: de0aa7b2f97d3 ("PCI: hv: Fix 2 hang issues in hv_compose_msi_msg()")
> Reported-by: Wei Hu <weh@microsoft.com>
> Reported-by: Dexuan Cui <decui@microsoft.com>
> Suggested-by: Michael Kelley <mikelley@microsoft.com>
> Signed-off-by: Andrea Parri (Microsoft) <parri.andrea@gmail.com>
> ---
>  drivers/pci/controller/pci-hyperv.c | 83 ++++++++++++++++++++++++++---
>  1 file changed, 77 insertions(+), 6 deletions(-)
> 
> diff --git a/drivers/pci/controller/pci-hyperv.c b/drivers/pci/controller/pci-hyperv.c
> index 9f963a46b8298..8876b318173f0 100644
> --- a/drivers/pci/controller/pci-hyperv.c
> +++ b/drivers/pci/controller/pci-hyperv.c
> @@ -1662,6 +1662,55 @@ static u32 hv_compose_msi_req_v3(
>  	return sizeof(*int_pkt);
>  }
> 
> +/* As in vmbus_request_addr() but without the requestor lock */
> +static u64 __hv_pci_request_addr(struct vmbus_channel *channel, u64 trans_id)
> +{
> +	struct vmbus_requestor *rqstor = &channel->requestor;
> +	u64 req_addr;
> +
> +	if (trans_id >= rqstor->size ||
> +	    !test_bit(trans_id, rqstor->req_bitmap))
> +		return VMBUS_RQST_ERROR;
> +
> +	req_addr = rqstor->req_arr[trans_id];
> +	rqstor->req_arr[trans_id] = rqstor->next_request_id;
> +	rqstor->next_request_id = trans_id;
> +
> +	bitmap_clear(rqstor->req_bitmap, trans_id, 1);
> +
> +	return req_addr;
> +}
> +
> +/*
> + * Clear/remove @trans_id from @channel's requestor, provided the memory
> + * address stored at @trans_id equals @rqst_addr.
> + */
> +static void hv_pci_request_addr_match(struct vmbus_channel *channel,
> +				      u64 trans_id, u64 rqst_addr)
> +{
> +	struct vmbus_requestor *rqstor = &channel->requestor;
> +	unsigned long flags;
> +	u64 req_addr;
> +
> +	spin_lock_irqsave(&rqstor->req_lock, flags);
> +
> +	if (trans_id >= rqstor->size ||
> +	    !test_bit(trans_id, rqstor->req_bitmap)) {
> +		spin_unlock_irqrestore(&rqstor->req_lock, flags);
> +		return;
> +	}
> +
> +	req_addr = rqstor->req_arr[trans_id];
> +	if (req_addr == rqst_addr) {
> +		rqstor->req_arr[trans_id] = rqstor->next_request_id;
> +		rqstor->next_request_id = trans_id;
> +
> +		bitmap_clear(rqstor->req_bitmap, trans_id, 1);
> +	}
> +
> +	spin_unlock_irqrestore(&rqstor->req_lock, flags);
> +}
> +

Even though these two new functions are used only in the Hyper-V
vPCI driver, it seems like they should go in drivers/hv/channel.c
along with vmbus_next_request_id() and vmbus_request_addr().
And maybe vmbus_request_addr(), which gets the spin lock,
could be implemented to call the new version above that
assumes the spin lock is already held.  Also, the new function
that requires matching on the rqst_addr might also be folded
into common code via an optional rqst_addr argument.

>  /**
>   * hv_compose_msi_msg() - Supplies a valid MSI address/data
>   * @data:	Everything about this MSI
> @@ -1691,7 +1740,7 @@ static void hv_compose_msi_msg(struct irq_data *data,
> struct msi_msg *msg)
>  			struct pci_create_interrupt3 v3;
>  		} int_pkts;
>  	} __packed ctxt;
> -
> +	u64 trans_id;
>  	u32 size;
>  	int ret;
> 
> @@ -1753,10 +1802,10 @@ static void hv_compose_msi_msg(struct irq_data *data,
> struct msi_msg *msg)
>  		goto free_int_desc;
>  	}
> 
> -	ret = vmbus_sendpacket(hpdev->hbus->hdev->channel, &ctxt.int_pkts,
> -			       size, (unsigned long)&ctxt.pci_pkt,
> -			       VM_PKT_DATA_INBAND,
> -			       VMBUS_DATA_PACKET_FLAG_COMPLETION_REQUESTED);
> +	ret = vmbus_sendpacket_getid(hpdev->hbus->hdev->channel, &ctxt.int_pkts,
> +				     size, (unsigned long)&ctxt.pci_pkt,
> +				     &trans_id, VM_PKT_DATA_INBAND,
> +
> VMBUS_DATA_PACKET_FLAG_COMPLETION_REQUESTED);
>  	if (ret) {
>  		dev_err(&hbus->hdev->device,
>  			"Sending request for interrupt failed: 0x%x",
> @@ -1835,6 +1884,16 @@ static void hv_compose_msi_msg(struct irq_data *data,
> struct msi_msg *msg)
> 
>  enable_tasklet:
>  	tasklet_enable(&channel->callback_event);
> +	/*
> +	 * The completion packet on the stack becomes invalid after 'return';
> +	 * remove the ID from the VMbus requestor if the identifier is still
> +	 * mapped to/associated with the packet.  (The identifier could have
> +	 * been 're-used', i.e., already removed and (re-)mapped.)
> +	 *
> +	 * Cf. hv_pci_onchannelcallback().
> +	 */
> +	hv_pci_request_addr_match(channel, trans_id,
> +				  (unsigned long)&ctxt.pci_pkt);
>  free_int_desc:
>  	kfree(int_desc);
>  drop_reference:
> @@ -2700,6 +2759,8 @@ static void hv_pci_onchannelcallback(void *context)
>  	int ret;
>  	struct hv_pcibus_device *hbus = context;
>  	struct vmbus_channel *chan = hbus->hdev->channel;
> +	struct vmbus_requestor *rqstor = &chan->requestor;
> +	unsigned long flags;
>  	u32 bytes_recvd;
>  	u64 req_id, req_addr;
>  	struct vmpacket_descriptor *desc;
> @@ -2747,17 +2808,27 @@ static void hv_pci_onchannelcallback(void *context)
>  		switch (desc->type) {
>  		case VM_PKT_COMP:
> 
> -			req_addr = chan->request_addr_callback(chan, req_id);
> +			spin_lock_irqsave(&rqstor->req_lock, flags);

Obtaining the lock (and releasing it below) might be better abstracted into
a lock_requestor() and unlock_requestor() pair that are implemented in
drivers/hv/channel.c along with the other related functions.

> +			req_addr = __hv_pci_request_addr(chan, req_id);
>  			if (req_addr == VMBUS_RQST_ERROR) {
> +				spin_unlock_irqrestore(&rqstor->req_lock, flags);
>  				dev_warn_ratelimited(&hbus->hdev->device,
>  						     "Invalid request ID\n");
>  				break;
>  			}
>  			comp_packet = (struct pci_packet *)req_addr;
>  			response = (struct pci_response *)buffer;
> +			/*
> +			 * Call ->completion_func() within the critical section to make
> +			 * sure that the packet pointer is still valid during the call:
> +			 * here 'valid' means that there's a task still waiting for the
> +			 * completion, and that the packet data is still on the waiting
> +			 * task's stack.  Cf. hv_compose_msi_msg().
> +			 */
>  			comp_packet->completion_func(comp_packet->compl_ctxt,
>  						     response,
>  						     bytes_recvd);
> +			spin_unlock_irqrestore(&rqstor->req_lock, flags);
>  			break;
> 
>  		case VM_PKT_DATA_INBAND:
> --
> 2.25.1
Andrea Parri April 1, 2022, 4:30 p.m. UTC | #2
> > @@ -1662,6 +1662,55 @@ static u32 hv_compose_msi_req_v3(
> >  	return sizeof(*int_pkt);
> >  }
> > 
> > +/* As in vmbus_request_addr() but without the requestor lock */
> > +static u64 __hv_pci_request_addr(struct vmbus_channel *channel, u64 trans_id)
> > +{
> > +	struct vmbus_requestor *rqstor = &channel->requestor;
> > +	u64 req_addr;
> > +
> > +	if (trans_id >= rqstor->size ||
> > +	    !test_bit(trans_id, rqstor->req_bitmap))
> > +		return VMBUS_RQST_ERROR;
> > +
> > +	req_addr = rqstor->req_arr[trans_id];
> > +	rqstor->req_arr[trans_id] = rqstor->next_request_id;
> > +	rqstor->next_request_id = trans_id;
> > +
> > +	bitmap_clear(rqstor->req_bitmap, trans_id, 1);
> > +
> > +	return req_addr;
> > +}
> > +
> > +/*
> > + * Clear/remove @trans_id from @channel's requestor, provided the memory
> > + * address stored at @trans_id equals @rqst_addr.
> > + */
> > +static void hv_pci_request_addr_match(struct vmbus_channel *channel,
> > +				      u64 trans_id, u64 rqst_addr)
> > +{
> > +	struct vmbus_requestor *rqstor = &channel->requestor;
> > +	unsigned long flags;
> > +	u64 req_addr;
> > +
> > +	spin_lock_irqsave(&rqstor->req_lock, flags);
> > +
> > +	if (trans_id >= rqstor->size ||
> > +	    !test_bit(trans_id, rqstor->req_bitmap)) {
> > +		spin_unlock_irqrestore(&rqstor->req_lock, flags);
> > +		return;
> > +	}
> > +
> > +	req_addr = rqstor->req_arr[trans_id];
> > +	if (req_addr == rqst_addr) {
> > +		rqstor->req_arr[trans_id] = rqstor->next_request_id;
> > +		rqstor->next_request_id = trans_id;
> > +
> > +		bitmap_clear(rqstor->req_bitmap, trans_id, 1);
> > +	}
> > +
> > +	spin_unlock_irqrestore(&rqstor->req_lock, flags);
> > +}
> > +
> 
> Even though these two new functions are used only in the Hyper-V
> vPCI driver, it seems like they should go in drivers/hv/channel.c
> along with vmbus_next_request_id() and vmbus_request_addr().
> And maybe vmbus_request_addr(), which gets the spin lock,
> could be implemented to call the new version above that
> assumes the spin lock is already held.  Also, the new function
> that requires matching on the rqst_addr might also be folded
> into common code via an optional rqst_addr argument.

Noted.


> > @@ -2747,17 +2808,27 @@ static void hv_pci_onchannelcallback(void *context)
> >  		switch (desc->type) {
> >  		case VM_PKT_COMP:
> > 
> > -			req_addr = chan->request_addr_callback(chan, req_id);
> > +			spin_lock_irqsave(&rqstor->req_lock, flags);
> 
> Obtaining the lock (and releasing it below) might be better abstracted into
> a lock_requestor() and unlock_requestor() pair that are implemented in
> drivers/hv/channel.c along with the other related functions.

Seems like these helpers should go 'inline' in <linux/hyper.h>, let me
do it...

Thanks,
  Andrea
diff mbox series

Patch

diff --git a/drivers/pci/controller/pci-hyperv.c b/drivers/pci/controller/pci-hyperv.c
index 9f963a46b8298..8876b318173f0 100644
--- a/drivers/pci/controller/pci-hyperv.c
+++ b/drivers/pci/controller/pci-hyperv.c
@@ -1662,6 +1662,55 @@  static u32 hv_compose_msi_req_v3(
 	return sizeof(*int_pkt);
 }
 
+/* As in vmbus_request_addr() but without the requestor lock */
+static u64 __hv_pci_request_addr(struct vmbus_channel *channel, u64 trans_id)
+{
+	struct vmbus_requestor *rqstor = &channel->requestor;
+	u64 req_addr;
+
+	if (trans_id >= rqstor->size ||
+	    !test_bit(trans_id, rqstor->req_bitmap))
+		return VMBUS_RQST_ERROR;
+
+	req_addr = rqstor->req_arr[trans_id];
+	rqstor->req_arr[trans_id] = rqstor->next_request_id;
+	rqstor->next_request_id = trans_id;
+
+	bitmap_clear(rqstor->req_bitmap, trans_id, 1);
+
+	return req_addr;
+}
+
+/*
+ * Clear/remove @trans_id from @channel's requestor, provided the memory
+ * address stored at @trans_id equals @rqst_addr.
+ */
+static void hv_pci_request_addr_match(struct vmbus_channel *channel,
+				      u64 trans_id, u64 rqst_addr)
+{
+	struct vmbus_requestor *rqstor = &channel->requestor;
+	unsigned long flags;
+	u64 req_addr;
+
+	spin_lock_irqsave(&rqstor->req_lock, flags);
+
+	if (trans_id >= rqstor->size ||
+	    !test_bit(trans_id, rqstor->req_bitmap)) {
+		spin_unlock_irqrestore(&rqstor->req_lock, flags);
+		return;
+	}
+
+	req_addr = rqstor->req_arr[trans_id];
+	if (req_addr == rqst_addr) {
+		rqstor->req_arr[trans_id] = rqstor->next_request_id;
+		rqstor->next_request_id = trans_id;
+
+		bitmap_clear(rqstor->req_bitmap, trans_id, 1);
+	}
+
+	spin_unlock_irqrestore(&rqstor->req_lock, flags);
+}
+
 /**
  * hv_compose_msi_msg() - Supplies a valid MSI address/data
  * @data:	Everything about this MSI
@@ -1691,7 +1740,7 @@  static void hv_compose_msi_msg(struct irq_data *data, struct msi_msg *msg)
 			struct pci_create_interrupt3 v3;
 		} int_pkts;
 	} __packed ctxt;
-
+	u64 trans_id;
 	u32 size;
 	int ret;
 
@@ -1753,10 +1802,10 @@  static void hv_compose_msi_msg(struct irq_data *data, struct msi_msg *msg)
 		goto free_int_desc;
 	}
 
-	ret = vmbus_sendpacket(hpdev->hbus->hdev->channel, &ctxt.int_pkts,
-			       size, (unsigned long)&ctxt.pci_pkt,
-			       VM_PKT_DATA_INBAND,
-			       VMBUS_DATA_PACKET_FLAG_COMPLETION_REQUESTED);
+	ret = vmbus_sendpacket_getid(hpdev->hbus->hdev->channel, &ctxt.int_pkts,
+				     size, (unsigned long)&ctxt.pci_pkt,
+				     &trans_id, VM_PKT_DATA_INBAND,
+				     VMBUS_DATA_PACKET_FLAG_COMPLETION_REQUESTED);
 	if (ret) {
 		dev_err(&hbus->hdev->device,
 			"Sending request for interrupt failed: 0x%x",
@@ -1835,6 +1884,16 @@  static void hv_compose_msi_msg(struct irq_data *data, struct msi_msg *msg)
 
 enable_tasklet:
 	tasklet_enable(&channel->callback_event);
+	/*
+	 * The completion packet on the stack becomes invalid after 'return';
+	 * remove the ID from the VMbus requestor if the identifier is still
+	 * mapped to/associated with the packet.  (The identifier could have
+	 * been 're-used', i.e., already removed and (re-)mapped.)
+	 *
+	 * Cf. hv_pci_onchannelcallback().
+	 */
+	hv_pci_request_addr_match(channel, trans_id,
+				  (unsigned long)&ctxt.pci_pkt);
 free_int_desc:
 	kfree(int_desc);
 drop_reference:
@@ -2700,6 +2759,8 @@  static void hv_pci_onchannelcallback(void *context)
 	int ret;
 	struct hv_pcibus_device *hbus = context;
 	struct vmbus_channel *chan = hbus->hdev->channel;
+	struct vmbus_requestor *rqstor = &chan->requestor;
+	unsigned long flags;
 	u32 bytes_recvd;
 	u64 req_id, req_addr;
 	struct vmpacket_descriptor *desc;
@@ -2747,17 +2808,27 @@  static void hv_pci_onchannelcallback(void *context)
 		switch (desc->type) {
 		case VM_PKT_COMP:
 
-			req_addr = chan->request_addr_callback(chan, req_id);
+			spin_lock_irqsave(&rqstor->req_lock, flags);
+			req_addr = __hv_pci_request_addr(chan, req_id);
 			if (req_addr == VMBUS_RQST_ERROR) {
+				spin_unlock_irqrestore(&rqstor->req_lock, flags);
 				dev_warn_ratelimited(&hbus->hdev->device,
 						     "Invalid request ID\n");
 				break;
 			}
 			comp_packet = (struct pci_packet *)req_addr;
 			response = (struct pci_response *)buffer;
+			/*
+			 * Call ->completion_func() within the critical section to make
+			 * sure that the packet pointer is still valid during the call:
+			 * here 'valid' means that there's a task still waiting for the
+			 * completion, and that the packet data is still on the waiting
+			 * task's stack.  Cf. hv_compose_msi_msg().
+			 */
 			comp_packet->completion_func(comp_packet->compl_ctxt,
 						     response,
 						     bytes_recvd);
+			spin_unlock_irqrestore(&rqstor->req_lock, flags);
 			break;
 
 		case VM_PKT_DATA_INBAND: