From mboxrd@z Thu Jan 1 00:00:00 1970 Received: from mout02.posteo.de (mout02.posteo.de [185.67.36.66]) by mx.groups.io with SMTP id smtpd.web09.10585.1621007070565872582 for ; Fri, 14 May 2021 08:44:31 -0700 Authentication-Results: mx.groups.io; dkim=fail reason="body hash did not verify" header.i=@posteo.de header.s=2017 header.b=HMrMYR1n; spf=pass (domain: posteo.de, ip: 185.67.36.66, mailfrom: mhaeuser@posteo.de) Received: from submission (posteo.de [89.146.220.130]) by mout02.posteo.de (Postfix) with ESMTPS id B94CA2400FD for ; Fri, 14 May 2021 17:44:27 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=posteo.de; s=2017; t=1621007067; bh=moqwk1eFSxpHOyA2jVMX4cn5xOofPBn1PfefmHoAXNM=; h=Subject:To:Cc:From:Date:From; b=HMrMYR1n/B4000dD4Ua2+qT8PSznKI5vWoDoIaN082rNb6/npZoRAubXFe7GYGQ4y pWlHxGiLZOoxLl9ngeHiOH3ZT+im67e7A7L+vlfJud+i5UzNOlI2g66bJzhsWqm3Q3 WzpHTbp+K7TYaUM18c5EbEF2Y+7lfajUAoOi/TAocMcchA/q+XAYwv3WE2zLYG5kz2 Q5O773mV0IuGvm5s0EF9CPJGJYmBVkB02yJE9Rv1uVf/28L1qpO5p7oZnJESbWXti0 2jiy56mF/7q8LX/teYgQ/a5EUoGxfQo/UkVnYCUB0l5q2Ne7tWyIm9NMoixtmmvyQr qE3qMx7stWLag== Received: from customer (localhost [127.0.0.1]) by submission (posteo.de) with ESMTPSA id 4FhXrn5YT8z6tmQ; Fri, 14 May 2021 17:44:25 +0200 (CEST) Subject: Re: [edk2-devel] [PATCH] UefiCpuPkg/MpInitLib: Allocate a separate SEV-ES AP reset stack area To: devel@edk2.groups.io, thomas.lendacky@amd.com Cc: Brijesh Singh , Eric Dong , Ray Ni , Laszlo Ersek , Rahul Kumar References: <1162d1f4fe09048aaafba6f6ea3046bebd34fc2b.1620766224.git.thomas.lendacky@amd.com> From: =?UTF-8?B?TWFydmluIEjDpHVzZXI=?= Message-ID: Date: Fri, 14 May 2021 15:44:25 +0000 MIME-Version: 1.0 In-Reply-To: Content-Type: text/plain; charset=utf-8; format=flowed Content-Language: en-GB Content-Transfer-Encoding: quoted-printable On 14.05.21 17:23, Lendacky, Thomas wrote: > On 5/14/21 10:04 AM, Marvin H=C3=A4user wrote: >> Good day Thomas, > Hi Marvin, > >> Thank you very much for the quick patch. Comments inline. >> >> Best regards, >> Marvin >> >> On 11.05.21 22:50, Lendacky, Thomas wrote: >>> BZ: >>> https://nam11.safelinks.protection.outlook.com/?url=3Dhttps%3A%2F%2Fbu= gzilla.tianocore.org%2Fshow_bug.cgi%3Fid%3D3324&data=3D04%7C01%7Cthomas= .lendacky%40amd.com%7C25d0b0bdc5d14a8bc4ff08d916e99c10%7C3dd8961fe4884e608e= 11a82d994e183d%7C0%7C0%7C637566014883375137%7CUnknown%7CTWFpbGZsb3d8eyJWIjo= iMC4wLjAwMDAiLCJQIjoiV2luMzIiLCJBTiI6Ik1haWwiLCJXVCI6Mn0%3D%7C1000&sdat= a=3D8%2BywcjFoZ00AymlBdxt%2BMCP0JClc64soY6pwcfz08zo%3D&reserved=3D0 >>> >>> >>> The SEV-ES stacks currently share a page with the reset code and data. >>> Separate the SEV-ES stacks from the reset vector code and data to avoi= d >>> possible stack overflows from overwriting the code and/or data. >>> >>> When SEV-ES is enabled, invoke the GetWakeupBuffer() routine a second = time >>> to allocate a new area, below the reset vector and data. >>> >>> Both the PEI and DXE versions of GetWakeupBuffer() are changed to trac= k >>> the previous reset buffer allocation in order to ensure that the new >>> buffer allocation is below the previous allocation. >>> >>> Fixes: 7b7508ad784d16a5208c8d12dff43aef6df0835b >>> Cc: Eric Dong >>> Cc: Ray Ni >>> Cc: Laszlo Ersek >>> Cc: Rahul Kumar >>> Signed-off-by: Tom Lendacky >>> --- >>> =C2=A0 UefiCpuPkg/Library/MpInitLib/DxeMpLib.c | 12 ++++- >>> =C2=A0 UefiCpuPkg/Library/MpInitLib/MpLib.c=C2=A0=C2=A0=C2=A0 | 48 ++= +++++++++++------- >>> =C2=A0 UefiCpuPkg/Library/MpInitLib/PeiMpLib.c | 14 ++++-- >>> =C2=A0 3 files changed, 54 insertions(+), 20 deletions(-) >>> >>> diff --git a/UefiCpuPkg/Library/MpInitLib/DxeMpLib.c >>> b/UefiCpuPkg/Library/MpInitLib/DxeMpLib.c >>> index 7839c249760e..fdfa0755d37a 100644 >>> --- a/UefiCpuPkg/Library/MpInitLib/DxeMpLib.c >>> +++ b/UefiCpuPkg/Library/MpInitLib/DxeMpLib.c >>> @@ -29,6 +29,11 @@ VOID=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0=C2=A0=C2=A0=C2=A0 *mReservedApLoopFunc =3D NULL; >>> =C2=A0 UINTN=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= = =A0=C2=A0 mReservedTopOfApStack; >>> =C2=A0 volatile UINT32=C2=A0 mNumberToFinish =3D 0; >>> =C2=A0 +// >>> +// Begin wakeup buffer allocation below 0x88000 >>> +// >> This definitely is not an issue of your patch at all, but I find the >> comments of the behaviour very lacking. Might this be a good opportunit= y >> to briefly document it? It took me quite a bit of git blame to fully >> figure it out, especially due to the non-descriptive commit message. Th= e >> constant is explained very well in the description: >> https://nam11.safelinks.protection.outlook.com/?url=3Dhttps%3A%2F%2Fgit= hub.com%2Ftianocore%2Fedk2%2Fcommit%2Fe4ff6349bf9ee4f3f392141374901ea4994e0= 43e&data=3D04%7C01%7Cthomas.lendacky%40amd.com%7C25d0b0bdc5d14a8bc4ff08= d916e99c10%7C3dd8961fe4884e608e11a82d994e183d%7C0%7C0%7C637566014883375137%= 7CUnknown%7CTWFpbGZsb3d8eyJWIjoiMC4wLjAwMDAiLCJQIjoiV2luMzIiLCJBTiI6Ik1haWw= iLCJXVCI6Mn0%3D%7C1000&sdata=3DNHR7dQjJbXQK5j4zc0ni0Q%2FZtOQp7szwDEzpHY= 6V9Dc%3D&reserved=3D0 >> > I think a separate patch would be best... and since you understand it so > well now, maybe something you could submit :) :) >>> +STATIC EFI_PHYSICAL_ADDRESS mWakeupBuffer =3D 0x88000; >> Hmm, I wonder whether a global variable is the best solution here. With= an >> explanation from the commit above, a top-down allocator makes good sens= e >> for this scenario. However, a "GetWakeupBuffer(Size, MaxAddress)" funct= ion >> might be more self-descriptive. What do you think? > Given the previous comments to not introduce any regressions in the > non-SEV-ES path, it is probably not a good idea to change this as part o= f > this patch. A separate distinct patch would be best. > > But understand that GetWakeupBuffer() has a different implementation in > PEI and DXE. The only common thing is that GetWakeupBuffer() knows to be > under 1MB. PEI doesn't have the 0x88000 limitation that DXE does, so I > don't think the MaxAddress parameter helps here. For now you are right, yes. There currently is an open ticket which=20 would likely be resolved by aligning the PEI behaviour with DXE, which=20 would resolve this issue as well. But also better to push to a separate=20 thread, sorry. >>> + >>> =C2=A0 /** >>> =C2=A0=C2=A0=C2=A0 Enable Debug Agent to support source debugging on = AP function. >>> =C2=A0 @@ -102,7 +107,7 @@ GetWakeupBuffer ( >>> =C2=A0=C2=A0=C2=A0 // LagacyBios driver depends on CPU Arch protocol = which guarantees >>> below >>> =C2=A0=C2=A0=C2=A0 // allocation runs earlier than LegacyBios driver. >>> =C2=A0=C2=A0=C2=A0 // >>> -=C2=A0 StartAddress =3D 0x88000; >>> +=C2=A0 StartAddress =3D mWakeupBuffer; >>> =C2=A0=C2=A0=C2=A0 Status =3D gBS->AllocatePages ( >>> =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= = =A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 AllocateMaxAddress, >>> =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= = =A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 MemoryType, >>> @@ -112,6 +117,11 @@ GetWakeupBuffer ( >>> =C2=A0=C2=A0=C2=A0 ASSERT_EFI_ERROR (Status); >>> =C2=A0=C2=A0=C2=A0 if (EFI_ERROR (Status)) { >>> =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 StartAddress =3D (EFI_PHYSICAL_ADDRESS= ) -1; >>> +=C2=A0 } else { >>> +=C2=A0=C2=A0=C2=A0 // >>> +=C2=A0=C2=A0=C2=A0 // Next wakeup buffer allocation must be below thi= s allocation >>> +=C2=A0=C2=A0=C2=A0 // >>> +=C2=A0=C2=A0=C2=A0 mWakeupBuffer =3D StartAddress; >>> =C2=A0=C2=A0=C2=A0 } >>> =C2=A0 =C2=A0=C2=A0=C2=A0 DEBUG ((DEBUG_INFO, "WakeupBufferStart =3D = %x, WakeupBufferSize =3D >>> %x\n", >>> diff --git a/UefiCpuPkg/Library/MpInitLib/MpLib.c >>> b/UefiCpuPkg/Library/MpInitLib/MpLib.c >>> index dc2a54aa31e8..a76dae437606 100644 >>> --- a/UefiCpuPkg/Library/MpInitLib/MpLib.c >>> +++ b/UefiCpuPkg/Library/MpInitLib/MpLib.c >>> @@ -1164,20 +1164,6 @@ GetApResetVectorSize ( >>> =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= = =A0 AddressMap->SwitchToRealSize + >>> =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= = =A0 sizeof (MP_CPU_EXCHANGE_INFO); >>> =C2=A0 -=C2=A0 // >>> -=C2=A0 // The AP reset stack is only used by SEV-ES guests. Do not ad= d to the >>> -=C2=A0 // allocation if SEV-ES is not enabled. >>> -=C2=A0 // >>> -=C2=A0 if (PcdGetBool (PcdSevEsIsEnabled)) { >>> -=C2=A0=C2=A0=C2=A0 // >>> -=C2=A0=C2=A0=C2=A0 // Stack location is based on APIC ID, so use the = total number of >>> -=C2=A0=C2=A0=C2=A0 // processors for calculating the total stack area= . >>> -=C2=A0=C2=A0=C2=A0 // >>> -=C2=A0=C2=A0=C2=A0 Size +=3D AP_RESET_STACK_SIZE * PcdGet32 >>> (PcdCpuMaxLogicalProcessorNumber); >>> - >>> -=C2=A0=C2=A0=C2=A0 Size =3D ALIGN_VALUE (Size, CPU_STACK_ALIGNMENT); >>> -=C2=A0 } >>> - >>> =C2=A0=C2=A0=C2=A0 return Size; >>> =C2=A0 } >>> =C2=A0 @@ -1207,9 +1193,39 @@ AllocateResetVector ( >>> =20 >>> CpuMpData->AddressMap.ModeTransitionOffset >>> =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= = =A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0 ); >>> =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 // >>> -=C2=A0=C2=A0=C2=A0 // The reset stack starts at the end of the buffer= . >>> +=C2=A0=C2=A0=C2=A0 // The AP reset stack is only used by SEV-ES guest= s. Do not >>> allocate it >>> +=C2=A0=C2=A0=C2=A0 // if SEV-ES is not enabled. >>> =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 // >>> -=C2=A0=C2=A0=C2=A0 CpuMpData->SevEsAPResetStackStart =3D CpuMpData->W= akeupBuffer + >>> ApResetVectorSize; >>> +=C2=A0=C2=A0=C2=A0 if (PcdGetBool (PcdSevEsIsEnabled)) { >>> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 UINTN=C2=A0 ApResetStackSize; >> Personally, I do not mind this at all, but I think the code style >> prohibits declaring variables in inner scopes. Though preferably I woul= d >> like to see this, nowadays, arbitrary restriction lifted rather than th= e >> patch be changed. Do the package maintainers have comments on this? > Yup, noted in other comments. So the variable will be moved. Thx >>> + >>> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 // >>> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 // Stack location is based on Processo= rNumber, so use the total >>> number >>> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 // of processors for calculating the t= otal stack area. >>> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 // >>> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 ApResetStackSize =3D AP_RESET_STACK_SI= ZE * >>> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= = =A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0=C2=A0 PcdGet32 (PcdCpuMaxLogicalProcessorNumber); >>> + >>> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 // >>> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 // Invoke GetWakeupBuffer a second tim= e to allocate the stack area >>> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 // below 1MB. The returned buffer will= be page aligned and sized and >>> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 // below the previously allocated buff= er. >>> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 // >>> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 CpuMpData->SevEsAPResetStackStart =3D = GetWakeupBuffer >>> (ApResetStackSize); >>> + >>> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 // >>> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 // Check to be sure that the "allocate= below" behavior hasn't >>> changed. >>> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 // This will also catch a failed alloc= ation, as "-1" is returned on >>> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 // failure. >>> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 // >>> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 if (CpuMpData->SevEsAPResetStackStart = >=3D CpuMpData->WakeupBuffer) { >>> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 DEBUG ((DEBUG_ERROR, >>> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 "SEV-ES AP res= et stack is not below wakeup buffer\n")); >>> + >>> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 ASSERT (FALSE); >> Should the ASSERT not only catch the broken "allocate below" behaviour, >> i.e. not trigger on failed allocation? > I think it's best to trigger on a failed allocation as well rather than > continuing and allowing a page fault or some other problem to occur. Well, it should handle the error in a safe way, i.e. the deadloop below.= =20 To not ASSERT on plausible conditions is a common design guideline in=20 most low-level projects including Linux kernel. Best regards, Marvin > Thanks, > Tom > >>> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 CpuDeadLoop (); >>> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 } >>> +=C2=A0=C2=A0=C2=A0 } >>> =C2=A0=C2=A0=C2=A0 } >>> =C2=A0=C2=A0=C2=A0 BackupAndPrepareWakeupBuffer (CpuMpData); >>> =C2=A0 } >>> diff --git a/UefiCpuPkg/Library/MpInitLib/PeiMpLib.c >>> b/UefiCpuPkg/Library/MpInitLib/PeiMpLib.c >>> index 3989bd6a7a9f..4d09e89b4128 100644 >>> --- a/UefiCpuPkg/Library/MpInitLib/PeiMpLib.c >>> +++ b/UefiCpuPkg/Library/MpInitLib/PeiMpLib.c >>> @@ -11,6 +11,8 @@ >>> =C2=A0 #include >>> =C2=A0 #include >>> =C2=A0 +STATIC UINT64 mWakeupBuffer =3D BASE_1MB; >>> + >>> =C2=A0 /** >>> =C2=A0=C2=A0=C2=A0 S3 SMM Init Done notification function. >>> =C2=A0 @@ -220,11 +222,11 @@ GetWakeupBuffer ( >>> =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 // Need memory= under 1MB to be collected here >>> =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 // >>> =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 WakeupBufferEn= d =3D Hob.ResourceDescriptor->PhysicalStart + >>> Hob.ResourceDescriptor->ResourceLength; >>> -=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 if (WakeupBufferEnd > BASE= _1MB) { >>> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 if (WakeupBufferEnd > mWak= eupBuffer) { >>> =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 // >>> -=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 // Wakeup buff= er should be under 1MB >>> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 // Wakeup buff= er should be under 1MB and under the previous one >>> =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 // >>> -=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 WakeupBufferEn= d =3D BASE_1MB; >>> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 WakeupBufferEn= d =3D mWakeupBuffer; >>> =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 } >>> =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 while (WakeupB= ufferEnd > WakeupBufferSize) { >>> =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 // >>> @@ -244,6 +246,12 @@ GetWakeupBuffer ( >>> =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 } >>> =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 DE= BUG ((DEBUG_INFO, "WakeupBufferStart =3D %x, >>> WakeupBufferSize =3D %x\n", >>> =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= = =A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2= =A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 WakeupBufferStart, Wak= eupBufferSize)); >>> + >>> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 // >>> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 // Next wakeup= buffer allocation must be below this allocation >>> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 // >>> +=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 mWakeupBuffer = = =3D WakeupBufferStart; >>> + >>> =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 re= turn (UINTN)WakeupBufferStart; >>> =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 } >>> =C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0=C2=A0 } > >=20 > >