Index: lib/CodeGen/CGClass.cpp =================================================================== --- lib/CodeGen/CGClass.cpp +++ lib/CodeGen/CGClass.cpp @@ -2489,7 +2489,7 @@ void CodeGenFunction::EmitVTablePtrCheckForCall(const CXXMethodDecl *MD, llvm::Value *VTable, - CFITypeCheckKind TCK, + CFICheckKind TCK, SourceLocation Loc) { const CXXRecordDecl *ClassDecl = MD->getParent(); if (!SanOpts.has(SanitizerKind::CFICastStrict)) @@ -2501,7 +2501,7 @@ void CodeGenFunction::EmitVTablePtrCheckForCast(QualType T, llvm::Value *Derived, bool MayBeNull, - CFITypeCheckKind TCK, + CFICheckKind TCK, SourceLocation Loc) { if (!getLangOpts().CPlusPlus) return; @@ -2545,7 +2545,7 @@ void CodeGenFunction::EmitVTablePtrCheck(const CXXRecordDecl *RD, llvm::Value *VTable, - CFITypeCheckKind TCK, + CFICheckKind TCK, SourceLocation Loc) { if (CGM.IsCFIBlacklistedRecord(RD)) return; @@ -2561,13 +2561,6 @@ Builder.CreateCall(CGM.getIntrinsic(llvm::Intrinsic::bitset_test), {CastedVTable, BitSetName}); - if (CGM.getCodeGenOpts().SanitizeCfiCrossDso) { - if (auto TypeId = CGM.CreateCfiIdForTypeMetadata(MD)) { - EmitCfiSlowPathCheck(BitSetTest, TypeId, CastedVTable); - return; - } - } - SanitizerMask M; switch (TCK) { case CFITCK_VCall: @@ -2582,15 +2575,23 @@ case CFITCK_UnrelatedCast: M = SanitizerKind::CFIUnrelatedCast; break; + case CFITCK_ICall: + llvm_unreachable("not expecting CFITCK_ICall"); } llvm::Constant *StaticData[] = { + llvm::ConstantInt::get(Int8Ty, TCK), EmitCheckSourceLocation(Loc), EmitCheckTypeDescriptor(QualType(RD->getTypeForDecl(), 0)), - llvm::ConstantInt::get(Int8Ty, TCK), }; - EmitCheck(std::make_pair(BitSetTest, M), "cfi_bad_type", StaticData, - CastedVTable); + + auto TypeId = CGM.CreateCfiIdForTypeMetadata(MD); + if (CGM.getCodeGenOpts().SanitizeCfiCrossDso && TypeId) { + EmitCfiSlowPathCheck(M, BitSetTest, TypeId, CastedVTable, StaticData); + } else { + EmitCheck(std::make_pair(BitSetTest, M), "cfi_check_fail", StaticData, + CastedVTable); + } } // FIXME: Ideally Expr::IgnoreParenNoopCasts should do this, but it doesn't do Index: lib/CodeGen/CGExpr.cpp =================================================================== --- lib/CodeGen/CGExpr.cpp +++ lib/CodeGen/CGExpr.cpp @@ -2532,10 +2532,9 @@ EmitBlock(Cont); } -void CodeGenFunction::EmitCfiSlowPathCheck(llvm::Value *Cond, - llvm::ConstantInt *TypeId, - llvm::Value *Ptr) { - auto &Ctx = getLLVMContext(); +void CodeGenFunction::EmitCfiSlowPathCheck( + SanitizerMask Kind, llvm::Value *Cond, llvm::ConstantInt *TypeId, + llvm::Value *Ptr, ArrayRef StaticArgs) { llvm::BasicBlock *Cont = createBasicBlock("cfi.cont"); llvm::BasicBlock *CheckBB = createBasicBlock("cfi.slowpath"); @@ -2547,19 +2546,145 @@ EmitBlock(CheckBB); - llvm::Constant *SlowPathFn = CGM.getModule().getOrInsertFunction( - "__cfi_slowpath", - llvm::FunctionType::get( - llvm::Type::getVoidTy(Ctx), - {llvm::Type::getInt64Ty(Ctx), - llvm::PointerType::getUnqual(llvm::Type::getInt8Ty(Ctx))}, - false)); - llvm::CallInst *CheckCall = Builder.CreateCall(SlowPathFn, {TypeId, Ptr}); + bool WithDiag = !CGM.getCodeGenOpts().SanitizeTrap.has(Kind); + + llvm::CallInst *CheckCall; + if (WithDiag) { + llvm::Constant *Info = llvm::ConstantStruct::getAnon(StaticArgs); + auto *InfoPtr = + new llvm::GlobalVariable(CGM.getModule(), Info->getType(), false, + llvm::GlobalVariable::PrivateLinkage, Info); + InfoPtr->setUnnamedAddr(true); + CGM.getSanitizerMetadata()->disableSanitizerForGlobal(InfoPtr); + + llvm::Constant *SlowPathDiagFn = CGM.getModule().getOrInsertFunction( + "__cfi_slowpath_diag", + llvm::FunctionType::get(VoidTy, {Int64Ty, Int8PtrTy, Int8PtrTy}, + false)); + CheckCall = Builder.CreateCall( + SlowPathDiagFn, + {TypeId, Ptr, Builder.CreateBitCast(InfoPtr, Int8PtrTy)}); + } else { + llvm::Constant *SlowPathFn = CGM.getModule().getOrInsertFunction( + "__cfi_slowpath", + llvm::FunctionType::get(VoidTy, {Int64Ty, Int8PtrTy}, false)); + CheckCall = Builder.CreateCall(SlowPathFn, {TypeId, Ptr}); + } + CheckCall->setDoesNotThrow(); EmitBlock(Cont); } +// This function is basically a switch over the CFI failure kind, which is +// extracted from CFICheckFailData (1st function argument). Each case is either +// llvm.trap or a call to one of the two runtime handlers, based on +// -fsanitize-trap and -fsanitize-recover settings. Default case (invalid +// failure kind) traps, but this should really never happen. CFICheckFailData +// can be nullptr if the calling module has -fsanitize-trap behavior for this +// check kind; in this case __cfi_check_fail traps as well. +void CodeGenFunction::EmitCfiCheckFail() { + FunctionArgList Args; + ImplicitParamDecl ArgData(getContext(), nullptr, SourceLocation(), nullptr, + getContext().VoidPtrTy); + ImplicitParamDecl ArgAddr(getContext(), nullptr, SourceLocation(), nullptr, + getContext().VoidPtrTy); + Args.push_back(&ArgData); + Args.push_back(&ArgAddr); + + const CGFunctionInfo &FI = CGM.getTypes().arrangeFreeFunctionDeclaration( + getContext().VoidTy, Args, FunctionType::ExtInfo(), /*variadic=*/false); + + llvm::Function *F = llvm::Function::Create( + llvm::FunctionType::get(VoidTy, {VoidPtrTy, VoidPtrTy}, false), + llvm::GlobalValue::WeakODRLinkage, "__cfi_check_fail", &CGM.getModule()); + F->setVisibility(llvm::GlobalValue::HiddenVisibility); + + StartFunction(GlobalDecl(), CGM.getContext().VoidTy, F, FI, Args, + SourceLocation()); + + llvm::Value *Data = + EmitLoadOfScalar(GetAddrOfLocalVar(&ArgData), /*Volatile=*/false, + CGM.getContext().VoidPtrTy, ArgData.getLocation()); + llvm::Value *Addr = + EmitLoadOfScalar(GetAddrOfLocalVar(&ArgAddr), /*Volatile=*/false, + CGM.getContext().VoidPtrTy, ArgAddr.getLocation()); + + auto ExitBB = createBasicBlock("exit"); + auto TrapBB = createBasicBlock("trap"); + auto ContBB = createBasicBlock("cont"); + + // Data == nullptr means the calling module has trap behaviour for this check. + llvm::Value *DataIsNullPtr = + Builder.CreateICmpEQ(Data, llvm::ConstantPointerNull::get(Int8PtrTy)); + Builder.CreateCondBr(DataIsNullPtr, TrapBB, ContBB); + + EmitBlock(ContBB); + llvm::StructType *SourceLocationTy = + llvm::StructType::get(VoidPtrTy, Int32Ty, Int32Ty, nullptr); + llvm::StructType *CfiCheckFailDataTy = + llvm::StructType::get(Int8Ty, SourceLocationTy, VoidPtrTy, nullptr); + + llvm::Value *V = Builder.CreateConstGEP2_32( + CfiCheckFailDataTy, + Builder.CreatePointerCast(Data, CfiCheckFailDataTy->getPointerTo(0)), 0, + 0); + Address CheckKindAddr(V, getIntAlign()); + llvm::Value *CheckKind = Builder.CreateLoad(CheckKindAddr); + llvm::SwitchInst *SI = + Builder.CreateSwitch(CheckKind, TrapBB, /*NumCases*/ 5); + + EmitBlock(TrapBB); + llvm::CallInst *TrapCall = EmitTrapCall(llvm::Intrinsic::trap); + TrapCall->setDoesNotReturn(); + TrapCall->setDoesNotThrow(); + Builder.CreateUnreachable(); + + llvm::BasicBlock *RecoverBB = nullptr; + llvm::BasicBlock *FatalBB = nullptr; + + constexpr std::pair CheckKinds[] = { + {CFITCK_VCall, SanitizerKind::CFIVCall}, + {CFITCK_NVCall, SanitizerKind::CFINVCall}, + {CFITCK_DerivedCast, SanitizerKind::CFIDerivedCast}, + {CFITCK_UnrelatedCast, SanitizerKind::CFIUnrelatedCast}, + {CFITCK_ICall, SanitizerKind::CFIICall}}; + + for (auto CheckKindMaskPair : CheckKinds) { + int Kind = CheckKindMaskPair.first; + SanitizerMask Mask = CheckKindMaskPair.second; + // All CFI checks are recoverable. + assert(getRecoverableKind(Mask) == CheckRecoverableKind::Recoverable); + if (CGM.getCodeGenOpts().SanitizeTrap.has(Mask)) { + SI->addCase(llvm::ConstantInt::get(Int8Ty, Kind), TrapBB); + } else if (CGM.getCodeGenOpts().SanitizeRecover.has(Mask)) { + if (!RecoverBB) { + RecoverBB = createBasicBlock("non_fatal"); + EmitBlock(RecoverBB); + emitCheckHandlerCall(*this, F->getFunctionType(), {Data, Addr}, + "cfi_check_fail", + CheckRecoverableKind::Recoverable, false, ExitBB); + } + SI->addCase(llvm::ConstantInt::get(Int8Ty, Kind), RecoverBB); + } else { + if (!FatalBB) { + FatalBB = createBasicBlock("fatal"); + EmitBlock(FatalBB); + emitCheckHandlerCall(*this, F->getFunctionType(), {Data, Addr}, + "cfi_check_fail", + CheckRecoverableKind::Recoverable, true, ExitBB); + } + SI->addCase(llvm::ConstantInt::get(Int8Ty, Kind), FatalBB); + } + } + + EmitBlock(ExitBB); + FinishFunction(); + // The only reference to this function will be created during LTO link. + // Make sure it survives until then. + CGM.addUsedGlobal(F); +} + void CodeGenFunction::EmitTrapCheck(llvm::Value *Checked) { llvm::BasicBlock *Cont = createBasicBlock("cont"); @@ -3860,15 +3985,17 @@ {CastedCallee, BitSetName}); auto TypeId = CGM.CreateCfiIdForTypeMetadata(MD); + llvm::Constant *StaticData[] = { + llvm::ConstantInt::get(Int8Ty, CFITCK_ICall), + EmitCheckSourceLocation(E->getLocStart()), + EmitCheckTypeDescriptor(QualType(FnType, 0)), + }; if (CGM.getCodeGenOpts().SanitizeCfiCrossDso && TypeId) { - EmitCfiSlowPathCheck(BitSetTest, TypeId, CastedCallee); + EmitCfiSlowPathCheck(SanitizerKind::CFIICall, BitSetTest, TypeId, + CastedCallee, StaticData); } else { - llvm::Constant *StaticData[] = { - EmitCheckSourceLocation(E->getLocStart()), - EmitCheckTypeDescriptor(QualType(FnType, 0)), - }; EmitCheck(std::make_pair(BitSetTest, SanitizerKind::CFIICall), - "cfi_bad_icall", StaticData, CastedCallee); + "cfi_check_fail", StaticData, CastedCallee); } } Index: lib/CodeGen/CodeGenFunction.h =================================================================== --- lib/CodeGen/CodeGenFunction.h +++ lib/CodeGen/CodeGenFunction.h @@ -1383,29 +1383,30 @@ llvm::Value *GetVTablePtr(Address This, llvm::Type *VTableTy, const CXXRecordDecl *VTableClass); - enum CFITypeCheckKind { + enum CFICheckKind { CFITCK_VCall, CFITCK_NVCall, CFITCK_DerivedCast, CFITCK_UnrelatedCast, + CFITCK_ICall, }; /// \brief Derived is the presumed address of an object of type T after a /// cast. If T is a polymorphic class type, emit a check that the virtual /// table for Derived belongs to a class derived from T. void EmitVTablePtrCheckForCast(QualType T, llvm::Value *Derived, - bool MayBeNull, CFITypeCheckKind TCK, + bool MayBeNull, CFICheckKind TCK, SourceLocation Loc); /// EmitVTablePtrCheckForCall - Virtual method MD is being called via VTable. /// If vptr CFI is enabled, emit a check that VTable is valid. void EmitVTablePtrCheckForCall(const CXXMethodDecl *MD, llvm::Value *VTable, - CFITypeCheckKind TCK, SourceLocation Loc); + CFICheckKind TCK, SourceLocation Loc); /// EmitVTablePtrCheck - Emit a check that VTable is a valid virtual table for /// RD using llvm.bitset.test. void EmitVTablePtrCheck(const CXXRecordDecl *RD, llvm::Value *VTable, - CFITypeCheckKind TCK, SourceLocation Loc); + CFICheckKind TCK, SourceLocation Loc); /// CanDevirtualizeMemberFunctionCalls - Checks whether virtual calls on given /// expr can be devirtualized. @@ -3011,8 +3012,9 @@ /// \brief Emit a slow path cross-DSO CFI check which calls __cfi_slowpath /// if Cond if false. - void EmitCfiSlowPathCheck(llvm::Value *Cond, llvm::ConstantInt *TypeId, - llvm::Value *Ptr); + void EmitCfiSlowPathCheck(SanitizerMask Kind, llvm::Value *Cond, + llvm::ConstantInt *TypeId, llvm::Value *Ptr, + ArrayRef StaticArgs); /// \brief Create a basic block that will call the trap intrinsic, and emit a /// conditional branch to it, for the -ftrapv checks. @@ -3022,6 +3024,9 @@ /// "trap-func-name" if specified. llvm::CallInst *EmitTrapCall(llvm::Intrinsic::ID IntrID); + /// \brief Emit a cross-DSO CFI failure handling function. + void EmitCfiCheckFail(); + /// \brief Create a check for a function parameter that may potentially be /// declared as non-null. void EmitNonNullArgCheck(RValue RV, QualType ArgType, SourceLocation ArgLoc, Index: lib/CodeGen/CodeGenModule.cpp =================================================================== --- lib/CodeGen/CodeGenModule.cpp +++ lib/CodeGen/CodeGenModule.cpp @@ -387,6 +387,8 @@ EmitDeferredUnusedCoverageMappings(); if (CoverageMapping) CoverageMapping->emit(); + if (CodeGenOpts.SanitizeCfiCrossDso) + CodeGenFunction(*this).EmitCfiCheckFail(); emitLLVMUsed(); if (CodeGenOpts.Autolink && Index: lib/Driver/Tools.cpp =================================================================== --- lib/Driver/Tools.cpp +++ lib/Driver/Tools.cpp @@ -2784,10 +2784,14 @@ } if (SanArgs.needsSafeStackRt()) StaticRuntimes.push_back("safestack"); - if (SanArgs.needsCfiRt()) + if (SanArgs.needsCfiRt()) { StaticRuntimes.push_back("cfi"); - if (SanArgs.needsCfiDiagRt()) + } + if (SanArgs.needsCfiDiagRt()) { StaticRuntimes.push_back("cfi_diag"); + if (SanArgs.linkCXXRuntimes()) + StaticRuntimes.push_back("ubsan_standalone_cxx"); + } } // Should be called before we add system libraries (C++ ABI, libstdc++/libc++, Index: test/CodeGen/cfi-check-fail.c =================================================================== --- /dev/null +++ test/CodeGen/cfi-check-fail.c @@ -0,0 +1,42 @@ +// RUN: %clang_cc1 -triple x86_64-unknown-linux -O0 -fsanitize=cfi-icall -fsanitize-cfi-cross-dso \ +// RUN: -fsanitize-trap=cfi-icall,cfi-nvcall -fsanitize-recover=cfi-vcall,cfi-unrelated-cast \ +// RUN: -emit-llvm -o - %s | FileCheck %s + +void caller(void (*f)()) { + f(); +} + +// CHECK: define weak_odr hidden void @__cfi_check_fail(i8*, i8*) { +// CHECK: store i8* %0, i8** %[[ALLOCA0:.*]], align 8 +// CHECK: store i8* %1, i8** %[[ALLOCA1:.*]], align 8 +// CHECK: %[[DATA:.*]] = load i8*, i8** %[[ALLOCA0]], align 8 +// CHECK: %[[ADDR:.*]] = load i8*, i8** %[[ALLOCA1]], align 8 +// CHECK: %[[ICMP_NULL:.*]] = icmp eq i8* %[[DATA]], null +// CHECK: br i1 %[[ICMP_NULL]], label %[[TRAP:.*]], label %[[CONT:.*]] + +// CHECK: [[CONT]]: +// CHECK: %[[A:.*]] = bitcast i8* %[[DATA]] to { i8, { i8*, i32, i32 }, i8* }* +// CHECK: %[[KINDPTR:.*]] = getelementptr {{.*}} %[[A]], i32 0, i32 0 +// CHECK: %[[KIND:.*]] = load i8, i8* %[[KINDPTR]], align 4 +// CHECK: switch i8 %[[KIND]], label %[[TRAP]] [ +// CHECK-NEXT: i8 0, label %[[NON_FATAL:.*]] +// CHECK-NEXT: i8 1, label %[[TRAP]] +// CHECK-NEXT: i8 2, label %[[FATAL:.*]] +// CHECK-NEXT: i8 3, label %[[NON_FATAL]] +// CHECK-NEXT: i8 4, label %[[TRAP]] +// CHECK-NEXT: ] + +// CHECK: [[TRAP]]: +// CHECK: call void @llvm.trap() +// CHECK: unreachable + +// CHECK: [[NON_FATAL]]: +// CHECK: call void @__ubsan_handle_cfi_check_fail(i8* %[[DATA]], i8* %[[ADDR]]) +// CHECK: br label %[[EXIT:.*]] + +// CHECK: [[FATAL]]: +// CHECK: call void @__ubsan_handle_cfi_check_fail_abort(i8* %[[DATA]], i8* %[[ADDR]]) +// CHECK: unreachable + +// CHECK: [[EXIT]]: +// CHECK: ret void Index: test/CodeGen/cfi-icall-cross-dso.c =================================================================== --- test/CodeGen/cfi-icall-cross-dso.c +++ test/CodeGen/cfi-icall-cross-dso.c @@ -19,11 +19,16 @@ inline void foo() {} void bar() { foo(); } +// CHECK: @[[SRC:.*]] = private unnamed_addr constant {{.*}}cfi-icall-cross-dso.c\00 +// CHECK: @[[TYPE:.*]] = private unnamed_addr constant { i16, i16, [{{.*}} x i8] } { i16 -1, i16 0, [{{.*}} x i8] c"'void ()'\00" +// CHECK: @[[DATA:.*]] = private unnamed_addr global {{.*}}@[[SRC]]{{.*}}@[[TYPE]] + + // ITANIUM: call i1 @llvm.bitset.test(i8* %{{.*}}, metadata !"_ZTSFvE"), !nosanitize -// ITANIUM: call void @__cfi_slowpath(i64 6588678392271548388, i8* %{{.*}}) {{.*}}, !nosanitize +// ITANIUM: call void @__cfi_slowpath_diag(i64 6588678392271548388, i8* %{{.*}}, {{.*}}@[[DATA]]{{.*}}, !nosanitize // MS: call i1 @llvm.bitset.test(i8* %{{.*}}, metadata !"?6AX@Z"), !nosanitize -// MS: call void @__cfi_slowpath(i64 4195979634929632483, i8* %{{.*}}) {{.*}}, !nosanitize +// MS: call void @__cfi_slowpath_diag(i64 4195979634929632483, i8* %{{.*}}, {{.*}}@[[DATA]]{{.*}}, !nosanitize // ITANIUM: define available_externally void @foo() // MS: define linkonce_odr void @foo() Index: test/CodeGenCXX/cfi-cross-dso.cpp =================================================================== --- test/CodeGenCXX/cfi-cross-dso.cpp +++ test/CodeGenCXX/cfi-cross-dso.cpp @@ -34,8 +34,8 @@ // MS: %[[TEST:.*]] = call i1 @llvm.bitset.test(i8* %[[VT2]], metadata !"?AUA@@"), !nosanitize // CHECK: br i1 %[[TEST]], label %[[CONT:.*]], label %[[SLOW:.*]], {{.*}} !nosanitize // CHECK: [[SLOW]] -// ITANIUM: call void @__cfi_slowpath(i64 7004155349499253778, i8* %[[VT2]]) {{.*}} !nosanitize -// MS: call void @__cfi_slowpath(i64 -8005289897957287421, i8* %[[VT2]]) {{.*}} !nosanitize +// ITANIUM: call void @__cfi_slowpath_diag(i64 7004155349499253778, i8* %[[VT2]], {{.*}}) {{.*}} !nosanitize +// MS: call void @__cfi_slowpath_diag(i64 -8005289897957287421, i8* %[[VT2]], {{.*}}) {{.*}} !nosanitize // CHECK: br label %[[CONT]], !nosanitize // CHECK: [[CONT]] // CHECK: call void %{{.*}}(%struct.A* %{{.*}}) Index: test/CodeGenCXX/cfi-vcall.cpp =================================================================== --- test/CodeGenCXX/cfi-vcall.cpp +++ test/CodeGenCXX/cfi-vcall.cpp @@ -55,7 +55,7 @@ // DIAG: @[[SRC:.*]] = private unnamed_addr constant [{{.*}} x i8] c"{{.*}}cfi-vcall.cpp\00", align 1 // DIAG: @[[TYPE:.*]] = private unnamed_addr constant { i16, i16, [4 x i8] } { i16 -1, i16 0, [4 x i8] c"'A'\00" } -// DIAG: @[[BADTYPESTATIC:.*]] = private unnamed_addr global { { [{{.*}} x i8]*, i32, i32 }, { i16, i16, [4 x i8] }*, i8 } { { [{{.*}} x i8]*, i32, i32 } { [{{.*}} x i8]* @[[SRC]], i32 [[@LINE+21]], i32 3 }, { i16, i16, [4 x i8] }* @[[TYPE]], i8 0 } +// DIAG: @[[BADTYPESTATIC:.*]] = private unnamed_addr global { i8, { [{{.*}} x i8]*, i32, i32 }, { i16, i16, [4 x i8] }* } { i8 0, { [{{.*}} x i8]*, i32, i32 } { [{{.*}} x i8]* @[[SRC]], i32 [[@LINE+21]], i32 3 }, { i16, i16, [4 x i8] }* @[[TYPE]] } // ITANIUM: define void @_Z2afP1A // MS: define void @"\01?af@@YAXPEAUA@@@Z" @@ -69,9 +69,9 @@ // NDIAG-NEXT: call void @llvm.trap() // NDIAG-NEXT: unreachable // DIAG-NEXT: [[VTINT:%[^ ]*]] = ptrtoint i8* [[VT]] to i64 - // DIAG-ABORT-NEXT: call void @__ubsan_handle_cfi_bad_type_abort(i8* bitcast ({{.*}} @[[BADTYPESTATIC]] to i8*), i64 [[VTINT]]) + // DIAG-ABORT-NEXT: call void @__ubsan_handle_cfi_check_fail_abort(i8* getelementptr inbounds ({{.*}} @[[BADTYPESTATIC]], i32 0, i32 0), i64 [[VTINT]]) // DIAG-ABORT-NEXT: unreachable - // DIAG-RECOVER-NEXT: call void @__ubsan_handle_cfi_bad_type(i8* bitcast ({{.*}} @[[BADTYPESTATIC]] to i8*), i64 [[VTINT]]) + // DIAG-RECOVER-NEXT: call void @__ubsan_handle_cfi_check_fail(i8* getelementptr inbounds ({{.*}} @[[BADTYPESTATIC]], i32 0, i32 0), i64 [[VTINT]]) // DIAG-RECOVER-NEXT: br label %[[CONTBB]] // CHECK: [[CONTBB]]