xref: /openbmc/linux/drivers/hv/channel_mgmt.c (revision 97da55fc)
1 /*
2  * Copyright (c) 2009, Microsoft Corporation.
3  *
4  * This program is free software; you can redistribute it and/or modify it
5  * under the terms and conditions of the GNU General Public License,
6  * version 2, as published by the Free Software Foundation.
7  *
8  * This program is distributed in the hope it will be useful, but WITHOUT
9  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
10  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
11  * more details.
12  *
13  * You should have received a copy of the GNU General Public License along with
14  * this program; if not, write to the Free Software Foundation, Inc., 59 Temple
15  * Place - Suite 330, Boston, MA 02111-1307 USA.
16  *
17  * Authors:
18  *   Haiyang Zhang <haiyangz@microsoft.com>
19  *   Hank Janssen  <hjanssen@microsoft.com>
20  */
21 #define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
22 
23 #include <linux/kernel.h>
24 #include <linux/sched.h>
25 #include <linux/wait.h>
26 #include <linux/mm.h>
27 #include <linux/slab.h>
28 #include <linux/list.h>
29 #include <linux/module.h>
30 #include <linux/completion.h>
31 #include <linux/hyperv.h>
32 
33 #include "hyperv_vmbus.h"
34 
35 struct vmbus_channel_message_table_entry {
36 	enum vmbus_channel_message_type message_type;
37 	void (*message_handler)(struct vmbus_channel_message_header *msg);
38 };
39 
40 
41 /**
42  * vmbus_prep_negotiate_resp() - Create default response for Hyper-V Negotiate message
43  * @icmsghdrp: Pointer to msg header structure
44  * @icmsg_negotiate: Pointer to negotiate message structure
45  * @buf: Raw buffer channel data
46  *
47  * @icmsghdrp is of type &struct icmsg_hdr.
48  * @negop is of type &struct icmsg_negotiate.
49  * Set up and fill in default negotiate response message.
50  *
51  * The max_fw_version specifies the maximum framework version that
52  * we can support and max _srv_version specifies the maximum service
53  * version we can support. A special value MAX_SRV_VER can be
54  * specified to indicate that we can handle the maximum version
55  * exposed by the host.
56  *
57  * Mainly used by Hyper-V drivers.
58  */
59 void vmbus_prep_negotiate_resp(struct icmsg_hdr *icmsghdrp,
60 				struct icmsg_negotiate *negop, u8 *buf,
61 				int max_fw_version, int max_srv_version)
62 {
63 	int icframe_vercnt;
64 	int icmsg_vercnt;
65 	int i;
66 
67 	icmsghdrp->icmsgsize = 0x10;
68 
69 	negop = (struct icmsg_negotiate *)&buf[
70 		sizeof(struct vmbuspipe_hdr) +
71 		sizeof(struct icmsg_hdr)];
72 
73 	icframe_vercnt = negop->icframe_vercnt;
74 	icmsg_vercnt = negop->icmsg_vercnt;
75 
76 	/*
77 	 * Select the framework version number we will
78 	 * support.
79 	 */
80 
81 	for (i = 0; i < negop->icframe_vercnt; i++) {
82 		if (negop->icversion_data[i].major <= max_fw_version)
83 			icframe_vercnt = negop->icversion_data[i].major;
84 	}
85 
86 	for (i = negop->icframe_vercnt;
87 		 (i < negop->icframe_vercnt + negop->icmsg_vercnt); i++) {
88 		if (negop->icversion_data[i].major <= max_srv_version)
89 			icmsg_vercnt = negop->icversion_data[i].major;
90 	}
91 
92 	/*
93 	 * Respond with the maximum framework and service
94 	 * version numbers we can support.
95 	 */
96 	negop->icframe_vercnt = 1;
97 	negop->icmsg_vercnt = 1;
98 	negop->icversion_data[0].major = icframe_vercnt;
99 	negop->icversion_data[0].minor = 0;
100 	negop->icversion_data[1].major = icmsg_vercnt;
101 	negop->icversion_data[1].minor = 0;
102 }
103 
104 EXPORT_SYMBOL_GPL(vmbus_prep_negotiate_resp);
105 
106 /*
107  * alloc_channel - Allocate and initialize a vmbus channel object
108  */
109 static struct vmbus_channel *alloc_channel(void)
110 {
111 	struct vmbus_channel *channel;
112 
113 	channel = kzalloc(sizeof(*channel), GFP_ATOMIC);
114 	if (!channel)
115 		return NULL;
116 
117 	spin_lock_init(&channel->inbound_lock);
118 
119 	channel->controlwq = create_workqueue("hv_vmbus_ctl");
120 	if (!channel->controlwq) {
121 		kfree(channel);
122 		return NULL;
123 	}
124 
125 	return channel;
126 }
127 
128 /*
129  * release_hannel - Release the vmbus channel object itself
130  */
131 static void release_channel(struct work_struct *work)
132 {
133 	struct vmbus_channel *channel = container_of(work,
134 						     struct vmbus_channel,
135 						     work);
136 
137 	destroy_workqueue(channel->controlwq);
138 
139 	kfree(channel);
140 }
141 
142 /*
143  * free_channel - Release the resources used by the vmbus channel object
144  */
145 static void free_channel(struct vmbus_channel *channel)
146 {
147 
148 	/*
149 	 * We have to release the channel's workqueue/thread in the vmbus's
150 	 * workqueue/thread context
151 	 * ie we can't destroy ourselves.
152 	 */
153 	INIT_WORK(&channel->work, release_channel);
154 	queue_work(vmbus_connection.work_queue, &channel->work);
155 }
156 
157 
158 
159 /*
160  * vmbus_process_rescind_offer -
161  * Rescind the offer by initiating a device removal
162  */
163 static void vmbus_process_rescind_offer(struct work_struct *work)
164 {
165 	struct vmbus_channel *channel = container_of(work,
166 						     struct vmbus_channel,
167 						     work);
168 
169 	vmbus_device_unregister(channel->device_obj);
170 }
171 
172 void vmbus_free_channels(void)
173 {
174 	struct vmbus_channel *channel;
175 
176 	list_for_each_entry(channel, &vmbus_connection.chn_list, listentry) {
177 		vmbus_device_unregister(channel->device_obj);
178 		kfree(channel->device_obj);
179 		free_channel(channel);
180 	}
181 }
182 
183 /*
184  * vmbus_process_offer - Process the offer by creating a channel/device
185  * associated with this offer
186  */
187 static void vmbus_process_offer(struct work_struct *work)
188 {
189 	struct vmbus_channel *newchannel = container_of(work,
190 							struct vmbus_channel,
191 							work);
192 	struct vmbus_channel *channel;
193 	bool fnew = true;
194 	int ret;
195 	unsigned long flags;
196 
197 	/* The next possible work is rescind handling */
198 	INIT_WORK(&newchannel->work, vmbus_process_rescind_offer);
199 
200 	/* Make sure this is a new offer */
201 	spin_lock_irqsave(&vmbus_connection.channel_lock, flags);
202 
203 	list_for_each_entry(channel, &vmbus_connection.chn_list, listentry) {
204 		if (!uuid_le_cmp(channel->offermsg.offer.if_type,
205 			newchannel->offermsg.offer.if_type) &&
206 			!uuid_le_cmp(channel->offermsg.offer.if_instance,
207 				newchannel->offermsg.offer.if_instance)) {
208 			fnew = false;
209 			break;
210 		}
211 	}
212 
213 	if (fnew)
214 		list_add_tail(&newchannel->listentry,
215 			      &vmbus_connection.chn_list);
216 
217 	spin_unlock_irqrestore(&vmbus_connection.channel_lock, flags);
218 
219 	if (!fnew) {
220 		free_channel(newchannel);
221 		return;
222 	}
223 
224 	/*
225 	 * Start the process of binding this offer to the driver
226 	 * We need to set the DeviceObject field before calling
227 	 * vmbus_child_dev_add()
228 	 */
229 	newchannel->device_obj = vmbus_device_create(
230 		&newchannel->offermsg.offer.if_type,
231 		&newchannel->offermsg.offer.if_instance,
232 		newchannel);
233 
234 	/*
235 	 * Add the new device to the bus. This will kick off device-driver
236 	 * binding which eventually invokes the device driver's AddDevice()
237 	 * method.
238 	 */
239 	ret = vmbus_device_register(newchannel->device_obj);
240 	if (ret != 0) {
241 		pr_err("unable to add child device object (relid %d)\n",
242 			   newchannel->offermsg.child_relid);
243 
244 		spin_lock_irqsave(&vmbus_connection.channel_lock, flags);
245 		list_del(&newchannel->listentry);
246 		spin_unlock_irqrestore(&vmbus_connection.channel_lock, flags);
247 		kfree(newchannel->device_obj);
248 
249 		free_channel(newchannel);
250 	} else {
251 		/*
252 		 * This state is used to indicate a successful open
253 		 * so that when we do close the channel normally, we
254 		 * can cleanup properly
255 		 */
256 		newchannel->state = CHANNEL_OPEN_STATE;
257 	}
258 }
259 
260 enum {
261 	IDE = 0,
262 	SCSI,
263 	NIC,
264 	MAX_PERF_CHN,
265 };
266 
267 /*
268  * This is an array of device_ids (device types) that are performance critical.
269  * We attempt to distribute the interrupt load for these devices across
270  * all available CPUs.
271  */
272 static const struct hv_vmbus_device_id hp_devs[] = {
273 	/* IDE */
274 	{ HV_IDE_GUID, },
275 	/* Storage - SCSI */
276 	{ HV_SCSI_GUID, },
277 	/* Network */
278 	{ HV_NIC_GUID, },
279 };
280 
281 
282 /*
283  * We use this state to statically distribute the channel interrupt load.
284  */
285 static u32  next_vp;
286 
287 /*
288  * Starting with Win8, we can statically distribute the incoming
289  * channel interrupt load by binding a channel to VCPU. We
290  * implement here a simple round robin scheme for distributing
291  * the interrupt load.
292  * We will bind channels that are not performance critical to cpu 0 and
293  * performance critical channels (IDE, SCSI and Network) will be uniformly
294  * distributed across all available CPUs.
295  */
296 static u32 get_vp_index(uuid_le *type_guid)
297 {
298 	u32 cur_cpu;
299 	int i;
300 	bool perf_chn = false;
301 	u32 max_cpus = num_online_cpus();
302 
303 	for (i = IDE; i < MAX_PERF_CHN; i++) {
304 		if (!memcmp(type_guid->b, hp_devs[i].guid,
305 				 sizeof(uuid_le))) {
306 			perf_chn = true;
307 			break;
308 		}
309 	}
310 	if ((vmbus_proto_version == VERSION_WS2008) ||
311 	    (vmbus_proto_version == VERSION_WIN7) || (!perf_chn)) {
312 		/*
313 		 * Prior to win8, all channel interrupts are
314 		 * delivered on cpu 0.
315 		 * Also if the channel is not a performance critical
316 		 * channel, bind it to cpu 0.
317 		 */
318 		return 0;
319 	}
320 	cur_cpu = (++next_vp % max_cpus);
321 	return cur_cpu;
322 }
323 
324 /*
325  * vmbus_onoffer - Handler for channel offers from vmbus in parent partition.
326  *
327  */
328 static void vmbus_onoffer(struct vmbus_channel_message_header *hdr)
329 {
330 	struct vmbus_channel_offer_channel *offer;
331 	struct vmbus_channel *newchannel;
332 
333 	offer = (struct vmbus_channel_offer_channel *)hdr;
334 
335 	/* Allocate the channel object and save this offer. */
336 	newchannel = alloc_channel();
337 	if (!newchannel) {
338 		pr_err("Unable to allocate channel object\n");
339 		return;
340 	}
341 
342 	/*
343 	 * By default we setup state to enable batched
344 	 * reading. A specific service can choose to
345 	 * disable this prior to opening the channel.
346 	 */
347 	newchannel->batched_reading = true;
348 
349 	/*
350 	 * Setup state for signalling the host.
351 	 */
352 	newchannel->sig_event = (struct hv_input_signal_event *)
353 				(ALIGN((unsigned long)
354 				&newchannel->sig_buf,
355 				HV_HYPERCALL_PARAM_ALIGN));
356 
357 	newchannel->sig_event->connectionid.asu32 = 0;
358 	newchannel->sig_event->connectionid.u.id = VMBUS_EVENT_CONNECTION_ID;
359 	newchannel->sig_event->flag_number = 0;
360 	newchannel->sig_event->rsvdz = 0;
361 
362 	if (vmbus_proto_version != VERSION_WS2008) {
363 		newchannel->is_dedicated_interrupt =
364 				(offer->is_dedicated_interrupt != 0);
365 		newchannel->sig_event->connectionid.u.id =
366 				offer->connection_id;
367 	}
368 
369 	newchannel->target_vp = get_vp_index(&offer->offer.if_type);
370 
371 	memcpy(&newchannel->offermsg, offer,
372 	       sizeof(struct vmbus_channel_offer_channel));
373 	newchannel->monitor_grp = (u8)offer->monitorid / 32;
374 	newchannel->monitor_bit = (u8)offer->monitorid % 32;
375 
376 	INIT_WORK(&newchannel->work, vmbus_process_offer);
377 	queue_work(newchannel->controlwq, &newchannel->work);
378 }
379 
380 /*
381  * vmbus_onoffer_rescind - Rescind offer handler.
382  *
383  * We queue a work item to process this offer synchronously
384  */
385 static void vmbus_onoffer_rescind(struct vmbus_channel_message_header *hdr)
386 {
387 	struct vmbus_channel_rescind_offer *rescind;
388 	struct vmbus_channel *channel;
389 
390 	rescind = (struct vmbus_channel_rescind_offer *)hdr;
391 	channel = relid2channel(rescind->child_relid);
392 
393 	if (channel == NULL)
394 		/* Just return here, no channel found */
395 		return;
396 
397 	/* work is initialized for vmbus_process_rescind_offer() from
398 	 * vmbus_process_offer() where the channel got created */
399 	queue_work(channel->controlwq, &channel->work);
400 }
401 
402 /*
403  * vmbus_onoffers_delivered -
404  * This is invoked when all offers have been delivered.
405  *
406  * Nothing to do here.
407  */
408 static void vmbus_onoffers_delivered(
409 			struct vmbus_channel_message_header *hdr)
410 {
411 }
412 
413 /*
414  * vmbus_onopen_result - Open result handler.
415  *
416  * This is invoked when we received a response to our channel open request.
417  * Find the matching request, copy the response and signal the requesting
418  * thread.
419  */
420 static void vmbus_onopen_result(struct vmbus_channel_message_header *hdr)
421 {
422 	struct vmbus_channel_open_result *result;
423 	struct vmbus_channel_msginfo *msginfo;
424 	struct vmbus_channel_message_header *requestheader;
425 	struct vmbus_channel_open_channel *openmsg;
426 	unsigned long flags;
427 
428 	result = (struct vmbus_channel_open_result *)hdr;
429 
430 	/*
431 	 * Find the open msg, copy the result and signal/unblock the wait event
432 	 */
433 	spin_lock_irqsave(&vmbus_connection.channelmsg_lock, flags);
434 
435 	list_for_each_entry(msginfo, &vmbus_connection.chn_msg_list,
436 				msglistentry) {
437 		requestheader =
438 			(struct vmbus_channel_message_header *)msginfo->msg;
439 
440 		if (requestheader->msgtype == CHANNELMSG_OPENCHANNEL) {
441 			openmsg =
442 			(struct vmbus_channel_open_channel *)msginfo->msg;
443 			if (openmsg->child_relid == result->child_relid &&
444 			    openmsg->openid == result->openid) {
445 				memcpy(&msginfo->response.open_result,
446 				       result,
447 				       sizeof(
448 					struct vmbus_channel_open_result));
449 				complete(&msginfo->waitevent);
450 				break;
451 			}
452 		}
453 	}
454 	spin_unlock_irqrestore(&vmbus_connection.channelmsg_lock, flags);
455 }
456 
457 /*
458  * vmbus_ongpadl_created - GPADL created handler.
459  *
460  * This is invoked when we received a response to our gpadl create request.
461  * Find the matching request, copy the response and signal the requesting
462  * thread.
463  */
464 static void vmbus_ongpadl_created(struct vmbus_channel_message_header *hdr)
465 {
466 	struct vmbus_channel_gpadl_created *gpadlcreated;
467 	struct vmbus_channel_msginfo *msginfo;
468 	struct vmbus_channel_message_header *requestheader;
469 	struct vmbus_channel_gpadl_header *gpadlheader;
470 	unsigned long flags;
471 
472 	gpadlcreated = (struct vmbus_channel_gpadl_created *)hdr;
473 
474 	/*
475 	 * Find the establish msg, copy the result and signal/unblock the wait
476 	 * event
477 	 */
478 	spin_lock_irqsave(&vmbus_connection.channelmsg_lock, flags);
479 
480 	list_for_each_entry(msginfo, &vmbus_connection.chn_msg_list,
481 				msglistentry) {
482 		requestheader =
483 			(struct vmbus_channel_message_header *)msginfo->msg;
484 
485 		if (requestheader->msgtype == CHANNELMSG_GPADL_HEADER) {
486 			gpadlheader =
487 			(struct vmbus_channel_gpadl_header *)requestheader;
488 
489 			if ((gpadlcreated->child_relid ==
490 			     gpadlheader->child_relid) &&
491 			    (gpadlcreated->gpadl == gpadlheader->gpadl)) {
492 				memcpy(&msginfo->response.gpadl_created,
493 				       gpadlcreated,
494 				       sizeof(
495 					struct vmbus_channel_gpadl_created));
496 				complete(&msginfo->waitevent);
497 				break;
498 			}
499 		}
500 	}
501 	spin_unlock_irqrestore(&vmbus_connection.channelmsg_lock, flags);
502 }
503 
504 /*
505  * vmbus_ongpadl_torndown - GPADL torndown handler.
506  *
507  * This is invoked when we received a response to our gpadl teardown request.
508  * Find the matching request, copy the response and signal the requesting
509  * thread.
510  */
511 static void vmbus_ongpadl_torndown(
512 			struct vmbus_channel_message_header *hdr)
513 {
514 	struct vmbus_channel_gpadl_torndown *gpadl_torndown;
515 	struct vmbus_channel_msginfo *msginfo;
516 	struct vmbus_channel_message_header *requestheader;
517 	struct vmbus_channel_gpadl_teardown *gpadl_teardown;
518 	unsigned long flags;
519 
520 	gpadl_torndown = (struct vmbus_channel_gpadl_torndown *)hdr;
521 
522 	/*
523 	 * Find the open msg, copy the result and signal/unblock the wait event
524 	 */
525 	spin_lock_irqsave(&vmbus_connection.channelmsg_lock, flags);
526 
527 	list_for_each_entry(msginfo, &vmbus_connection.chn_msg_list,
528 				msglistentry) {
529 		requestheader =
530 			(struct vmbus_channel_message_header *)msginfo->msg;
531 
532 		if (requestheader->msgtype == CHANNELMSG_GPADL_TEARDOWN) {
533 			gpadl_teardown =
534 			(struct vmbus_channel_gpadl_teardown *)requestheader;
535 
536 			if (gpadl_torndown->gpadl == gpadl_teardown->gpadl) {
537 				memcpy(&msginfo->response.gpadl_torndown,
538 				       gpadl_torndown,
539 				       sizeof(
540 					struct vmbus_channel_gpadl_torndown));
541 				complete(&msginfo->waitevent);
542 				break;
543 			}
544 		}
545 	}
546 	spin_unlock_irqrestore(&vmbus_connection.channelmsg_lock, flags);
547 }
548 
549 /*
550  * vmbus_onversion_response - Version response handler
551  *
552  * This is invoked when we received a response to our initiate contact request.
553  * Find the matching request, copy the response and signal the requesting
554  * thread.
555  */
556 static void vmbus_onversion_response(
557 		struct vmbus_channel_message_header *hdr)
558 {
559 	struct vmbus_channel_msginfo *msginfo;
560 	struct vmbus_channel_message_header *requestheader;
561 	struct vmbus_channel_version_response *version_response;
562 	unsigned long flags;
563 
564 	version_response = (struct vmbus_channel_version_response *)hdr;
565 	spin_lock_irqsave(&vmbus_connection.channelmsg_lock, flags);
566 
567 	list_for_each_entry(msginfo, &vmbus_connection.chn_msg_list,
568 				msglistentry) {
569 		requestheader =
570 			(struct vmbus_channel_message_header *)msginfo->msg;
571 
572 		if (requestheader->msgtype ==
573 		    CHANNELMSG_INITIATE_CONTACT) {
574 			memcpy(&msginfo->response.version_response,
575 			      version_response,
576 			      sizeof(struct vmbus_channel_version_response));
577 			complete(&msginfo->waitevent);
578 		}
579 	}
580 	spin_unlock_irqrestore(&vmbus_connection.channelmsg_lock, flags);
581 }
582 
583 /* Channel message dispatch table */
584 static struct vmbus_channel_message_table_entry
585 	channel_message_table[CHANNELMSG_COUNT] = {
586 	{CHANNELMSG_INVALID,			NULL},
587 	{CHANNELMSG_OFFERCHANNEL,		vmbus_onoffer},
588 	{CHANNELMSG_RESCIND_CHANNELOFFER,	vmbus_onoffer_rescind},
589 	{CHANNELMSG_REQUESTOFFERS,		NULL},
590 	{CHANNELMSG_ALLOFFERS_DELIVERED,	vmbus_onoffers_delivered},
591 	{CHANNELMSG_OPENCHANNEL,		NULL},
592 	{CHANNELMSG_OPENCHANNEL_RESULT,	vmbus_onopen_result},
593 	{CHANNELMSG_CLOSECHANNEL,		NULL},
594 	{CHANNELMSG_GPADL_HEADER,		NULL},
595 	{CHANNELMSG_GPADL_BODY,		NULL},
596 	{CHANNELMSG_GPADL_CREATED,		vmbus_ongpadl_created},
597 	{CHANNELMSG_GPADL_TEARDOWN,		NULL},
598 	{CHANNELMSG_GPADL_TORNDOWN,		vmbus_ongpadl_torndown},
599 	{CHANNELMSG_RELID_RELEASED,		NULL},
600 	{CHANNELMSG_INITIATE_CONTACT,		NULL},
601 	{CHANNELMSG_VERSION_RESPONSE,		vmbus_onversion_response},
602 	{CHANNELMSG_UNLOAD,			NULL},
603 };
604 
605 /*
606  * vmbus_onmessage - Handler for channel protocol messages.
607  *
608  * This is invoked in the vmbus worker thread context.
609  */
610 void vmbus_onmessage(void *context)
611 {
612 	struct hv_message *msg = context;
613 	struct vmbus_channel_message_header *hdr;
614 	int size;
615 
616 	hdr = (struct vmbus_channel_message_header *)msg->u.payload;
617 	size = msg->header.payload_size;
618 
619 	if (hdr->msgtype >= CHANNELMSG_COUNT) {
620 		pr_err("Received invalid channel message type %d size %d\n",
621 			   hdr->msgtype, size);
622 		print_hex_dump_bytes("", DUMP_PREFIX_NONE,
623 				     (unsigned char *)msg->u.payload, size);
624 		return;
625 	}
626 
627 	if (channel_message_table[hdr->msgtype].message_handler)
628 		channel_message_table[hdr->msgtype].message_handler(hdr);
629 	else
630 		pr_err("Unhandled channel message type %d\n", hdr->msgtype);
631 }
632 
633 /*
634  * vmbus_request_offers - Send a request to get all our pending offers.
635  */
636 int vmbus_request_offers(void)
637 {
638 	struct vmbus_channel_message_header *msg;
639 	struct vmbus_channel_msginfo *msginfo;
640 	int ret, t;
641 
642 	msginfo = kmalloc(sizeof(*msginfo) +
643 			  sizeof(struct vmbus_channel_message_header),
644 			  GFP_KERNEL);
645 	if (!msginfo)
646 		return -ENOMEM;
647 
648 	init_completion(&msginfo->waitevent);
649 
650 	msg = (struct vmbus_channel_message_header *)msginfo->msg;
651 
652 	msg->msgtype = CHANNELMSG_REQUESTOFFERS;
653 
654 
655 	ret = vmbus_post_msg(msg,
656 			       sizeof(struct vmbus_channel_message_header));
657 	if (ret != 0) {
658 		pr_err("Unable to request offers - %d\n", ret);
659 
660 		goto cleanup;
661 	}
662 
663 	t = wait_for_completion_timeout(&msginfo->waitevent, 5*HZ);
664 	if (t == 0) {
665 		ret = -ETIMEDOUT;
666 		goto cleanup;
667 	}
668 
669 
670 
671 cleanup:
672 	kfree(msginfo);
673 
674 	return ret;
675 }
676 
677 /* eof */
678