Skip site navigation (1)Skip section navigation (2)
Date:      Sat, 18 May 2002 00:00:42 -0700 (PDT)
From:      David Xu <davidx@viasoft.com.cn>
To:        freebsd-gnats-submit@FreeBSD.org
Subject:   i386/38223: fix vm86 bios call crash bug (updated)
Message-ID:  <200205180700.g4I70goR075276@www.freebsd.org>

next in thread | raw e-mail | index | archive | help

>Number:         38223
>Category:       i386
>Synopsis:       fix vm86 bios call crash bug (updated)
>Confidential:   no
>Severity:       critical
>Priority:       high
>Responsible:    freebsd-bugs
>State:          open
>Quarter:        
>Keywords:       
>Date-Required:
>Class:          sw-bug
>Submitter-Id:   current-users
>Arrival-Date:   Sat May 18 00:10:02 PDT 2002
>Closed-Date:
>Last-Modified:
>Originator:     David Xu
>Release:        FreeBSD 5.0-CURRENT
>Organization:
Viatech
>Environment:
FreeBSD davidbsd.viasoft.com.cn 5.0-CURRENT FreeBSD 5.0-CURRENT #22: 
Tue Apr 30 16:10:05 CST 2002     
davidx@davidbsd.viasoft.com.cn:/usr/src/sys/i386/compile/xu  i386      
>Description:
this PR is updated version of PR 38012.
http://www.freebsd.org/cgi/query-pr.cgi?pr=i386/38012
in that PR, a patch for vm86bios.s does not fully restore orignal
thread PCB's tss descriptor into gdt, the patch fixes it.

there is a bug in vm86 bios call, our current implementation assumes 
the vm86 bios call will never be preempted, but it is not true because
interrupt thread was implemented in current source. original vm86 
bios call uses a static pcb(vm86pcb) to call bios and stores some 
arguments in the pcb, problem is vm86pcb is not current thread's pcb,
when the thread is preempted and later gets cpu again, the working pcb
is no longer vm86pcb, cause machine hangs.

>How-To-Repeat:
repeat following command several times, machine will die.

#vidcontrol -g100x37 VESA_800x600

this commmand will trigger the bug, because the command use vesa.ko
which turns to call bios vesa service.
      
>Fix:
my solution is to let vm86pcb become current thread's pcb, because 
it's current pcb, some parameters stored in vm86pcb will be modified
by cpu_switch(), so I store these parameters on stack, before every bios
call, I reset some parameters in the pcb, this is in vm86.c, 
I also remove variable in_vm86call, and let the flag becames
pcb attribute.


--- /usr/src/sys/i386/i386/genassym.c	Wed Mar 27 13:39:18 2002
+++ /usr/src/sys/i386/i386/genassym.c.new	Sun May 12 22:15:29 2002
@@ -150,6 +150,8 @@
 
 ASSYM(PCB_SIZE, sizeof(struct pcb));
 
+ASSYM(PCB_VM86CALL, PCB_VM86CALL);
+
 ASSYM(TF_TRAPNO, offsetof(struct trapframe, tf_trapno));
 ASSYM(TF_ERR, offsetof(struct trapframe, tf_err));
 ASSYM(TF_CS, offsetof(struct trapframe, tf_cs));

--- /usr/src/sys/i386/isa/ipl.s	Sun May  5 11:19:48 2002
+++ /usr/src/sys/i386/isa/ipl.s.new	Sun May 12 22:20:59 2002
@@ -63,8 +63,9 @@
 	 */
 	testl	$PSL_VM,TF_EFLAGS(%esp)	/* are we in vm86 mode? */
 	jz	doreti_notvm86
-	cmpl	$1,in_vm86call		/* are we in a vm86 call? */
-	jne	doreti_ast		/* can handle ASTs now if not */
+	movl	PCPU(CURPCB),%ecx
+	testl	$PCB_VM86CALL,PCB_FLAGS(%ecx)
+	jz	doreti_ast		/* can handle ASTs now if not */
   	jmp	doreti_exit
 
 doreti_notvm86:


--- /usr/src/sys/i386/include/pcb.h	Wed Mar 27 13:39:19 2002
+++ /usr/src/sys/i386/include/pcb.h.new	Sun May 12 22:22:33 2002
@@ -66,6 +66,7 @@
 #define	FP_SOFTFP	0x01	/* process using software fltng pnt emulator */
 #define	PCB_DBREGS	0x02	/* process using debug registers */
 #define	PCB_NPXTRAP	0x04	/* npx trap pending */
+#define PCB_VM86CALL	0x08
 	caddr_t	pcb_onfault;	/* copyin/out fault recovery */
 	int	pcb_gs;
 	struct	pcb_ext	*pcb_ext;	/* optional pcb extension */


--- /usr/src/sys/i386/i386/trap.c	Sun Apr 28 01:07:15 2002
+++ /usr/src/sys/i386/i386/trap.c.new	Sun May 12 22:14:51 2002
@@ -252,7 +252,7 @@
 #endif	/* DEVICE_POLLING */
 
         if ((ISPL(frame.tf_cs) == SEL_UPL) ||
-	    ((frame.tf_eflags & PSL_VM) && !in_vm86call)) {
+	    ((frame.tf_eflags & PSL_VM) && !(PCPU_GET(curpcb)->pcb_flags & PCB_VM86CALL))) {
 		/* user trap */
 
 		sticks = td->td_kse->ke_sticks;
@@ -462,7 +462,7 @@
 			/* FALL THROUGH */
 
 		case T_SEGNPFLT:	/* segment not present fault */
-			if (in_vm86call)
+			if (PCPU_GET(curpcb)->pcb_flags & PCB_VM86CALL)
 				break;
 
 			if (td->td_intr_nesting_level != 0)
@@ -564,7 +564,7 @@
 			 * debugging the kernel.
 			 */
 			/* XXX Giant */
-			if (user_dbreg_trap() && !in_vm86call) {
+			if (user_dbreg_trap() && !(PCPU_GET(curpcb)->pcb_flags & PCB_VM86CALL)) {
 				/*
 				 * Reset breakpoint bits because the
 				 * processor doesn't


--- /usr/src/sys/i386/include/vm86.h	Wed Mar 20 13:48:58 2002
+++ /usr/src/sys/i386/include/vm86.h.new	Sun May 12 22:23:05 2002
@@ -145,7 +145,6 @@
 };
 
 #ifdef _KERNEL
-extern	int in_vm86call;
 extern 	int vm86paddr;
 
 struct thread;

--- /usr/src/sys/i386/i386/vm86.c	Fri Apr  5 05:03:19 2002
+++ /usr/src/sys/i386/i386/vm86.c.new	Mon May 13 07:22:58 2002
@@ -55,6 +55,7 @@
 extern void vm86_biosret(struct vm86frame *);
 
 void vm86_prepcall(struct vm86frame);
+static void vm86_setargs(void);
 
 struct system_map {
 	int		type;
@@ -434,7 +435,7 @@
 	pcb->pcb_ext = ext;
 
 	bzero(ext, sizeof(struct pcb_ext)); 
-	ext->ext_tss.tss_esp0 = vm86paddr;
+	ext->ext_tss.tss_esp0 = vm86paddr - sizeof(struct vm86frame);
 	ext->ext_tss.tss_ss0 = GSEL(GDATA_SEL, SEL_KPL);
 	ext->ext_tss.tss_ioopt = 
 		((u_int)vml->vml_iomap - (u_int)&ext->ext_tss) << 16;
@@ -465,6 +466,13 @@
 #endif
 }
 
+static void vm86_setargs(void)
+{
+	vm86pcb->new_ptd = vm86pa | PG_V | PG_RW | PG_U;
+	vm86pcb->vm86_frame = vm86paddr - sizeof(struct vm86frame);
+	vm86pcb->pgtable_va = vm86paddr;
+}
+
 vm_offset_t
 vm86_getpage(struct vm86context *vmc, int pagenum)
 {
@@ -576,6 +584,7 @@
 
 	vmf->vmf_trapno = intnum;
 	mtx_lock(&vm86_lock);
+	vm86_setargs();
 	retval = vm86_bioscall(vmf);
 	mtx_unlock(&vm86_lock);
 	return (retval);
@@ -598,6 +607,7 @@
 	int i, entry, retval;
 
 	mtx_lock(&vm86_lock);
+	vm86_setargs();
 	for (i = 0; i < vmc->npages; i++) {
 		page = vtophys(vmc->pmap[i].kva & PG_FRAME);
 		entry = vmc->pmap[i].pte_num; 



--- /usr/src/sys/i386/i386/vm86bios.s	Sat May 18 14:44:00 2002
+++ /usr/src/sys/i386/i386/vm86bios.s.new	Sat May 18 14:43:52 2002
@@ -35,19 +35,22 @@
 
 #define SCR_NEWPTD	PCB_ESI		/* readability macros */ 
 #define SCR_VMFRAME	PCB_EBP		/* see vm86.c for explanation */
-#define SCR_STACK	PCB_ESP
 #define SCR_PGTABLE	PCB_EBX
-#define SCR_ARGFRAME	PCB_EIP
 #define SCR_TSS0	PCB_SPARE
 #define SCR_TSS1	(PCB_SPARE+4)
 
+#define ARG_SRCFRAME	 8(%ebp)
+#define ARG_NEWPTD	-4(%ebp)
+#define ARG_VMFRAME	-8(%ebp)
+#define ARG_PGTABLE	-12(%ebp)
+
 	.data
 	ALIGN_DATA
 
-	.globl	in_vm86call, vm86pcb
+	.globl	vm86pcb
 
-in_vm86call:		.long	0
 vm86pcb:		.long	0
+oldstack:		.long	0
 
 	.text
 
@@ -55,11 +58,18 @@
  * vm86_bioscall(struct trapframe_vm86 *vm86)
  */
 ENTRY(vm86_bioscall)
+	pushl	%ebp
+	movl	%esp,%ebp
 	movl	vm86pcb,%edx		/* scratch data area */
-	movl	4(%esp),%eax
-	movl	%eax,SCR_ARGFRAME(%edx)	/* save argument pointer */
+	/*
+	   vm86pcb could be preempted and contents will be modified,
+	   save arguments on stack
+	*/
+	pushl	SCR_NEWPTD(%edx)
+	pushl	SCR_VMFRAME(%edx)
+	pushl	SCR_PGTABLE(%edx)
+
 	pushl	%ebx
-	pushl	%ebp
 	pushl	%esi
 	pushl	%edi
 	pushl	%gs
@@ -83,17 +93,26 @@
 	popfl
 #endif
 
-	movl	SCR_VMFRAME(%edx),%ebx	/* target frame location */
-	movl	%ebx,%edi		/* destination */
-	movl    SCR_ARGFRAME(%edx),%esi	/* source (set on entry) */
+	movl	ARG_VMFRAME,%edi	/* target frame location */
+	movl	ARG_SRCFRAME,%esi	/* source */
 	movl	$VM86_FRAMESIZE/4,%ecx	/* sizeof(struct vm86frame)/4 */
 	cld
 	rep
 	movsl				/* copy frame to new stack */
 
-	movl	PCPU(CURPCB),%eax
-	pushl	%eax			/* save curpcb */
-	movl	%edx,PCPU(CURPCB)	/* set curpcb to vm86pcb */
+	movl	$PCB_VM86CALL,PCB_FLAGS(%edx)	/* set vm86 call flag */
+	movl	%cr3,%eax
+	movl	%eax,PCB_CR3(%edx)	/* set address space */
+
+	xorl	%eax,%eax
+	movl	PCPU(CPUID),%ecx
+	btsl	%ecx,private_tss	/* set private_tss flag */
+
+	pushl	PCPU(CURPCB)		/* save curpcb */
+	movl	PCPU(CURTHREAD),%ecx
+	pushl	TD_PCB(%ecx)  		/* save thread pcb */
+	movl	%edx,TD_PCB(%ecx)	/* set thread pcb */
+	movl	%edx,PCPU(CURPCB)	/* set curpcb */
 
 	movl	PCPU(TSS_GDT),%ebx	/* entry in GDT */
 	movl	0(%ebx),%eax
@@ -110,26 +129,19 @@
 	movl	$GPROC0_SEL*8,%esi	/* GSEL(entry, SEL_KPL) */
 	ltr	%si
 
-	movl	%cr3,%eax
-	pushl	%eax			/* save address space */
-	movl	IdlePTD,%ecx
-	movl	%ecx,%ebx
-	addl	$KERNBASE,%ebx		/* va of Idle PTD */
+	leal	PTD,%ebx
 	movl	0(%ebx),%eax
 	pushl	%eax			/* old ptde != 0 when booting */
 	pushl	%ebx			/* keep for reuse */
-
-	movl	%esp,SCR_STACK(%edx)	/* save current stack location */
-
-	movl	SCR_NEWPTD(%edx),%eax	/* mapping for vm86 page table */
+	movl	ARG_NEWPTD,%eax		/* mapping for vm86 page table */
 	movl	%eax,0(%ebx)		/* ... install as PTD entry 0 */
 
-	movl	%ecx,%cr3		/* new page tables */
-	movl	SCR_VMFRAME(%edx),%esp	/* switch to new stack */
-	
-	call	vm86_prepcall		/* finish setup */
+	pushl	%ebp			/* easy for later recover */
+	movl	PCB_EXT(%edx),%edi
+	movl	%esp,oldstack		/* save current stack location */
 
-	movl	$1,in_vm86call		/* set flag for trap() */
+	movl	ARG_VMFRAME,%esp	/* switch to new stack */
+	call	vm86_prepcall		/* finish setup */
 
 	/*
 	 * Return via doreti
@@ -142,39 +154,53 @@
  * vm86_biosret(struct trapframe_vm86 *vm86)
  */
 ENTRY(vm86_biosret)
-	movl	vm86pcb,%edx		/* data area */
-
 	movl	4(%esp),%esi		/* source */
-	movl	SCR_ARGFRAME(%edx),%edi	/* destination */
+	movl	oldstack,%esp		/* back to old stack */
+	popl	%ebp			/* old argument location */
+
+	movl	ARG_SRCFRAME,%edi	/* destination */
 	movl	$VM86_FRAMESIZE/4,%ecx	/* size */
 	cld
 	rep
 	movsl				/* copy frame to original frame */
 
-	movl	SCR_STACK(%edx),%esp	/* back to old stack */
-	popl	%ebx			/* saved va of Idle PTD */
+	popl	%ebx			/* PTD */
 	popl	%eax
 	movl	%eax,0(%ebx)		/* restore old pte */
-	popl	%eax
-	movl	%eax,%cr3		/* install old page table */
-
-	movl	$0,in_vm86call		/* reset trapflag */
 
-	movl	PCPU(TSS_GDT),%ebx		/* entry in GDT */
-	movl	SCR_TSS0(%edx),%eax
-	movl	%eax,0(%ebx)		/* restore first word */
-	movl	SCR_TSS1(%edx),%eax
-	movl	%eax,4(%ebx)		/* restore second word */
+	popl	%edx			/* original thread PCB */
+	movl	PCPU(CPUID),%esi
+	cmpl	$0, PCB_EXT(%edx)
+	je	1f
+	btsl	%esi,private_tss
+	movl	PCB_EXT(%edx),%edi
+	jmp	2f
+1:	
+	leal	-16(%edx),%ebx
+	movl	%ebx,PCPU(COMMON_TSS) + TSS_ESP0
+	btrl	%esi,private_tss
+	PCPU_ADDR(COMMON_TSSD,%edi)
+2:	
+	movl	PCPU(TSS_GDT),%ebx	/* entry in GDT */
+	movl	0(%edi), %eax
+	movl	%eax, 0(%ebx)
+	movl	4(%edi), %eax
+	movl	%eax, 4(%ebx)
 	movl	$GPROC0_SEL*8,%esi	/* GSEL(entry, SEL_KPL) */
 	ltr	%si
-	
+
+	movl	PCPU(CURTHREAD),%ecx
+	movl	%edx,TD_PCB(%ecx)
 	popl	PCPU(CURPCB)		/* restore curpcb/curproc */
-	movl	SCR_ARGFRAME(%edx),%edx	/* original stack frame */
+
+	movl	ARG_SRCFRAME,%edx	/* original stack frame */
 	movl	TF_TRAPNO(%edx),%eax	/* return (trapno) */
 
 	popl	%gs
 	popl	%edi
 	popl	%esi
-	popl	%ebp
 	popl	%ebx
+	movl	%ebp, %esp
+	popl	%ebp
 	ret				/* back to our normal program */
+
      
following is patched vm86bios.s, because the patch is big, I provide
a full source file here:

/*-
 * Copyright (c) 1998 Jonathan Lemon
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 *
 * $FreeBSD: src/sys/i386/i386/vm86bios.s,v 1.28 2001/12/11 23:33:40 jhb Exp $
 */

#include "opt_npx.h"

#include <machine/asmacros.h>		/* miscellaneous asm macros */
#include <machine/trap.h>

#include "assym.s"

#define SCR_NEWPTD	PCB_ESI		/* readability macros */ 
#define SCR_VMFRAME	PCB_EBP		/* see vm86.c for explanation */
#define SCR_PGTABLE	PCB_EBX
#define SCR_TSS0	PCB_SPARE
#define SCR_TSS1	(PCB_SPARE+4)

#define ARG_SRCFRAME	 8(%ebp)
#define ARG_NEWPTD	-4(%ebp)
#define ARG_VMFRAME	-8(%ebp)
#define ARG_PGTABLE	-12(%ebp)

	.data
	ALIGN_DATA

	.globl	vm86pcb

vm86pcb:		.long	0
oldstack:		.long	0

	.text

/*
 * vm86_bioscall(struct trapframe_vm86 *vm86)
 */
ENTRY(vm86_bioscall)
	pushl	%ebp
	movl	%esp,%ebp
	movl	vm86pcb,%edx		/* scratch data area */
	/*
	   vm86pcb could be preempted and contents will be modified,
	   save arguments on stack
	*/
	pushl	SCR_NEWPTD(%edx)
	pushl	SCR_VMFRAME(%edx)
	pushl	SCR_PGTABLE(%edx)

	pushl	%ebx
	pushl	%esi
	pushl	%edi
	pushl	%gs

#ifdef DEV_NPX
	pushfl
	cli
	movl	PCPU(CURTHREAD),%ecx
	cmpl	%ecx,PCPU(FPCURTHREAD)	/* do we need to save fp? */
	jne	1f
	testl	%ecx,%ecx
	je 	1f			/* no curproc/npxproc */
	pushl	%edx
	movl	TD_PCB(%ecx),%ecx
	addl	$PCB_SAVEFPU,%ecx
	pushl	%ecx
	call	npxsave
	popl	%ecx
	popl	%edx			/* recover our pcb */
1:
	popfl
#endif

	movl	ARG_VMFRAME,%edi	/* target frame location */
	movl	ARG_SRCFRAME,%esi	/* source */
	movl	$VM86_FRAMESIZE/4,%ecx	/* sizeof(struct vm86frame)/4 */
	cld
	rep
	movsl				/* copy frame to new stack */

	movl	$PCB_VM86CALL,PCB_FLAGS(%edx)	/* set vm86 call flag */
	movl	%cr3,%eax
	movl	%eax,PCB_CR3(%edx)	/* set address space */

	xorl	%eax,%eax
	movl	PCPU(CPUID),%ecx
	btsl	%ecx,private_tss	/* set private_tss flag */

	pushl	PCPU(CURPCB)		/* save curpcb */
	movl	PCPU(CURTHREAD),%ecx
	pushl	TD_PCB(%ecx)  		/* save thread pcb */
	movl	%edx,TD_PCB(%ecx)	/* set thread pcb */
	movl	%edx,PCPU(CURPCB)	/* set curpcb */

	movl	PCPU(TSS_GDT),%ebx	/* entry in GDT */
	movl	0(%ebx),%eax
	movl	%eax,SCR_TSS0(%edx)	/* save first word */
	movl	4(%ebx),%eax
	andl    $~0x200, %eax		/* flip 386BSY -> 386TSS */
	movl	%eax,SCR_TSS1(%edx)	/* save second word */

	movl	PCB_EXT(%edx),%edi	/* vm86 tssd entry */
	movl	0(%edi),%eax
	movl	%eax,0(%ebx)
	movl	4(%edi),%eax
	movl	%eax,4(%ebx)
	movl	$GPROC0_SEL*8,%esi	/* GSEL(entry, SEL_KPL) */
	ltr	%si

	leal	PTD,%ebx
	movl	0(%ebx),%eax
	pushl	%eax			/* old ptde != 0 when booting */
	pushl	%ebx			/* keep for reuse */
	movl	ARG_NEWPTD,%eax		/* mapping for vm86 page table */
	movl	%eax,0(%ebx)		/* ... install as PTD entry 0 */

	pushl	%ebp			/* easy for later recover */
	movl	PCB_EXT(%edx),%edi
	movl	%esp,oldstack		/* save current stack location */

	movl	ARG_VMFRAME,%esp	/* switch to new stack */
	call	vm86_prepcall		/* finish setup */

	/*
	 * Return via doreti
	 */
	MEXITCOUNT
	jmp	doreti


/*
 * vm86_biosret(struct trapframe_vm86 *vm86)
 */
ENTRY(vm86_biosret)
	movl	4(%esp),%esi		/* source */
	movl	oldstack,%esp		/* back to old stack */
	popl	%ebp			/* old argument location */

	movl	ARG_SRCFRAME,%edi	/* destination */
	movl	$VM86_FRAMESIZE/4,%ecx	/* size */
	cld
	rep
	movsl				/* copy frame to original frame */

	popl	%ebx			/* PTD */
	popl	%eax
	movl	%eax,0(%ebx)		/* restore old pte */

	popl	%edx			/* original thread PCB */
	movl	PCPU(CPUID),%esi
	cmpl	$0, PCB_EXT(%edx)
	je	1f
	btsl	%esi,private_tss
	movl	PCB_EXT(%edx),%edi
	jmp	2f
1:	
	leal	-16(%edx),%ebx
	movl	%ebx,PCPU(COMMON_TSS) + TSS_ESP0
	btrl	%esi,private_tss
	PCPU_ADDR(COMMON_TSSD,%edi)
2:	
	movl	PCPU(TSS_GDT),%ebx	/* entry in GDT */
	movl	0(%edi), %eax
	movl	%eax, 0(%ebx)
	movl	4(%edi), %eax
	movl	%eax, 4(%ebx)
	movl	$GPROC0_SEL*8,%esi	/* GSEL(entry, SEL_KPL) */
	ltr	%si

	movl	PCPU(CURTHREAD),%ecx
	movl	%edx,TD_PCB(%ecx)
	popl	PCPU(CURPCB)		/* restore curpcb/curproc */

	movl	ARG_SRCFRAME,%edx	/* original stack frame */
	movl	TF_TRAPNO(%edx),%eax	/* return (trapno) */

	popl	%gs
	popl	%edi
	popl	%esi
	popl	%ebx
	movl	%ebp, %esp
	popl	%ebp
	ret				/* back to our normal program */


>Release-Note:
>Audit-Trail:
>Unformatted:

To Unsubscribe: send mail to majordomo@FreeBSD.org
with "unsubscribe freebsd-bugs" in the body of the message




Want to link to this message? Use this URL: <https://mail-archive.FreeBSD.org/cgi/mid.cgi?200205180700.g4I70goR075276>