Skip to content

Commit

Permalink
[PowerPC][ISelLowering] Support -mstack-protector-guard=tls (#110928)
Browse files Browse the repository at this point in the history
Add support for using a thread-local variable with a specified offset
for holding the stack guard canary value. This supports both 32- and 64-
bit PowerPC targets.

This mirrors changes from #108942 but targeting PowerPC instead of
RISCV. Because both of these PRs modify the same driver functions, this
series is stack on top of the RISC-V one.

---------

Signed-off-by: Keith Packard <keithp@keithp.com>
  • Loading branch information
keith-packard authored Oct 18, 2024
1 parent 70865c4 commit 44b020a
Show file tree
Hide file tree
Showing 20 changed files with 354 additions and 36 deletions.
18 changes: 14 additions & 4 deletions clang/lib/Driver/ToolChains/Clang.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3595,7 +3595,7 @@ static void RenderSSPOptions(const Driver &D, const ToolChain &TC,
StringRef Value = A->getValue();
if (!EffectiveTriple.isX86() && !EffectiveTriple.isAArch64() &&
!EffectiveTriple.isARM() && !EffectiveTriple.isThumb() &&
!EffectiveTriple.isRISCV())
!EffectiveTriple.isRISCV() && !EffectiveTriple.isPPC())
D.Diag(diag::err_drv_unsupported_opt_for_target)
<< A->getAsString(Args) << TripleStr;
if ((EffectiveTriple.isX86() || EffectiveTriple.isARM() ||
Expand Down Expand Up @@ -3635,7 +3635,7 @@ static void RenderSSPOptions(const Driver &D, const ToolChain &TC,
<< A->getOption().getName() << Value << "sysreg global";
return;
}
if (EffectiveTriple.isRISCV()) {
if (EffectiveTriple.isRISCV() || EffectiveTriple.isPPC()) {
if (Value != "tls" && Value != "global") {
D.Diag(diag::err_drv_invalid_value_with_suggestion)
<< A->getOption().getName() << Value << "tls global";
Expand All @@ -3656,7 +3656,7 @@ static void RenderSSPOptions(const Driver &D, const ToolChain &TC,
StringRef Value = A->getValue();
if (!EffectiveTriple.isX86() && !EffectiveTriple.isAArch64() &&
!EffectiveTriple.isARM() && !EffectiveTriple.isThumb() &&
!EffectiveTriple.isRISCV())
!EffectiveTriple.isRISCV() && !EffectiveTriple.isPPC())
D.Diag(diag::err_drv_unsupported_opt_for_target)
<< A->getAsString(Args) << TripleStr;
int Offset;
Expand All @@ -3676,7 +3676,7 @@ static void RenderSSPOptions(const Driver &D, const ToolChain &TC,
if (Arg *A = Args.getLastArg(options::OPT_mstack_protector_guard_reg_EQ)) {
StringRef Value = A->getValue();
if (!EffectiveTriple.isX86() && !EffectiveTriple.isAArch64() &&
!EffectiveTriple.isRISCV())
!EffectiveTriple.isRISCV() && !EffectiveTriple.isPPC())
D.Diag(diag::err_drv_unsupported_opt_for_target)
<< A->getAsString(Args) << TripleStr;
if (EffectiveTriple.isX86() && (Value != "fs" && Value != "gs")) {
Expand All @@ -3693,6 +3693,16 @@ static void RenderSSPOptions(const Driver &D, const ToolChain &TC,
<< A->getOption().getName() << Value << "tp";
return;
}
if (EffectiveTriple.isPPC64() && Value != "r13") {
D.Diag(diag::err_drv_invalid_value_with_suggestion)
<< A->getOption().getName() << Value << "r13";
return;
}
if (EffectiveTriple.isPPC32() && Value != "r2") {
D.Diag(diag::err_drv_invalid_value_with_suggestion)
<< A->getOption().getName() << Value << "r2";
return;
}
A->render(Args, CmdArgs);
}

Expand Down
16 changes: 16 additions & 0 deletions clang/test/CodeGen/stack-protector-guard.c
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@
// RUN: %clang_cc1 -mstack-protector-guard=tls -triple riscv64-unknown-elf \
// RUN: -mstack-protector-guard-offset=44 -mstack-protector-guard-reg=tp \
// RUN: -emit-llvm %s -o - | FileCheck %s --check-prefix=RISCV
// RUN: %clang_cc1 -mstack-protector-guard=tls -triple powerpc64-unknown-elf \
// RUN: -mstack-protector-guard-offset=52 -mstack-protector-guard-reg=r13 \
// RUN: -emit-llvm %s -o - | FileCheck %s --check-prefix=POWERPC64
// RUN: %clang_cc1 -mstack-protector-guard=tls -triple ppc32-unknown-elf \
// RUN: -mstack-protector-guard-offset=16 -mstack-protector-guard-reg=r2 \
// RUN: -emit-llvm %s -o - | FileCheck %s --check-prefix=POWERPC32
void foo(int*);
void bar(int x) {
int baz[x];
Expand All @@ -31,3 +37,13 @@ void bar(int x) {
// RISCV: [[ATTR1]] = !{i32 1, !"stack-protector-guard", !"tls"}
// RISCV: [[ATTR2]] = !{i32 1, !"stack-protector-guard-reg", !"tp"}
// RISCV: [[ATTR3]] = !{i32 1, !"stack-protector-guard-offset", i32 44}

// POWERPC64: !llvm.module.flags = !{{{.*}}[[ATTR1:![0-9]+]], [[ATTR2:![0-9]+]], [[ATTR3:![0-9]+]], [[ATTR4:![0-9]+]]}
// POWERPC64: [[ATTR2]] = !{i32 1, !"stack-protector-guard", !"tls"}
// POWERPC64: [[ATTR3]] = !{i32 1, !"stack-protector-guard-reg", !"r13"}
// POWERPC64: [[ATTR4]] = !{i32 1, !"stack-protector-guard-offset", i32 52}

// POWERPC32: !llvm.module.flags = !{{{.*}}[[ATTR1:![0-9]+]], [[ATTR2:![0-9]+]], [[ATTR3:![0-9]+]], [[ATTR4:![0-9]+]]}
// POWERPC32: [[ATTR2]] = !{i32 1, !"stack-protector-guard", !"tls"}
// POWERPC32: [[ATTR3]] = !{i32 1, !"stack-protector-guard-reg", !"r2"}
// POWERPC32: [[ATTR4]] = !{i32 1, !"stack-protector-guard-offset", i32 16}
57 changes: 54 additions & 3 deletions clang/test/Driver/stack-protector-guard.c
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,15 @@
// RUN: FileCheck -check-prefix=CHECK-SYM %s

// Invalid arch
// RUN: not %clang -target powerpc64le-linux-gnu -mstack-protector-guard=tls %s 2>&1 | \
// RUN: not %clang -target mipsel-linux-gnu -mstack-protector-guard=tls %s 2>&1 | \
// RUN: FileCheck -check-prefix=INVALID-ARCH %s
// INVALID-ARCH: unsupported option '-mstack-protector-guard=tls' for target

// RUN: not %clang -target powerpc64le-linux-gnu -mstack-protector-guard-reg=fs %s 2>&1 | \
// RUN: not %clang -target mipsel-linux-gnu -mstack-protector-guard-reg=fs %s 2>&1 | \
// RUN: FileCheck -check-prefix=INVALID-ARCH2 %s
// INVALID-ARCH2: unsupported option '-mstack-protector-guard-reg=fs' for target

// RUN: not %clang -target powerpc64le-linux-gnu -mstack-protector-guard-offset=10 %s 2>&1 | \
// RUN: not %clang -target mipsel-linux-gnu -mstack-protector-guard-offset=10 %s 2>&1 | \
// RUN: FileCheck -check-prefix=INVALID-ARCH3 %s
// INVALID-ARCH3: unsupported option '-mstack-protector-guard-offset=10' for target

Expand Down Expand Up @@ -104,3 +104,54 @@
// RUN: FileCheck -check-prefix=INVALID-REG-RISCV %s

// INVALID-REG-RISCV: error: invalid value 'sp' in 'mstack-protector-guard-reg=', expected one of: tp

// RUN: %clang -### -target powerpc64-unknown-elf -mstack-protector-guard=tls -mstack-protector-guard-offset=24 -mstack-protector-guard-reg=r13 %s 2>&1 | \
// RUN: FileCheck -v -check-prefix=CHECK-TLS-POWERPC64 %s
// RUN: %clang -### -target powerpc64-unknown-linux-gnu -mstack-protector-guard=global %s 2>&1 | \
// RUN: FileCheck -check-prefix=CHECK-GLOBAL %s

// RUN: not %clang -target powerpc64-unknown-linux-gnu -mstack-protector-guard=tls %s 2>&1 | \
// RUN: FileCheck -check-prefix=MISSING-OFFSET %s

// RUN: not %clang -target powerpc64-unknown-elf -mstack-protector-guard=sysreg %s 2>&1 | \
// RUN: FileCheck -check-prefix=INVALID-VALUE2 %s

// RUN: not %clang -target powerpc64-unknown-elf -mstack-protector-guard=tls \
// RUN: -mstack-protector-guard-offset=20 -mstack-protector-guard-reg=r12 %s 2>&1 | \
// RUN: FileCheck -check-prefix=INVALID-REG-POWERPC64 %s

// CHECK-TLS-POWERPC64: "-cc1" {{.*}}"-mstack-protector-guard=tls" "-mstack-protector-guard-offset=24" "-mstack-protector-guard-reg=r13"
// INVALID-REG-POWERPC64: error: invalid value 'r12' in 'mstack-protector-guard-reg=', expected one of: r13

// RUN: %clang -### -target powerpc64le-unknown-elf -mstack-protector-guard=tls -mstack-protector-guard-offset=24 -mstack-protector-guard-reg=r13 %s 2>&1 | \
// RUN: FileCheck -v -check-prefix=CHECK-TLS-POWERPC64 %s
// RUN: %clang -### -target powerpc64le-unknown-elf -mstack-protector-guard=global %s 2>&1 | \
// RUN: FileCheck -check-prefix=CHECK-GLOBAL %s

// RUN: not %clang -target powerpc64le-unknown-elf -mstack-protector-guard=tls %s 2>&1 | \
// RUN: FileCheck -check-prefix=MISSING-OFFSET %s

// RUN: not %clang -target powerpc64le-unknown-elf -mstack-protector-guard=sysreg %s 2>&1 | \
// RUN: FileCheck -check-prefix=INVALID-VALUE2 %s

// RUN: not %clang -target powerpc64le-unknown-elf -mstack-protector-guard=tls \
// RUN: -mstack-protector-guard-offset=20 -mstack-protector-guard-reg=r12 %s 2>&1 | \
// RUN: FileCheck -check-prefix=INVALID-REG-POWERPC64 %s

// RUN: %clang -### -target ppc32-unknown-elf -mstack-protector-guard=tls -mstack-protector-guard-offset=24 -mstack-protector-guard-reg=r2 %s 2>&1 | \
// RUN: FileCheck -v -check-prefix=CHECK-TLS-POWERPC32 %s
// RUN: %clang -### -target ppc32-unknown-elf -mstack-protector-guard=global %s 2>&1 | \
// RUN: FileCheck -check-prefix=CHECK-GLOBAL %s

// RUN: not %clang -target ppc32-unknown-elf -mstack-protector-guard=tls %s 2>&1 | \
// RUN: FileCheck -check-prefix=MISSING-OFFSET %s

// RUN: not %clang -target ppc32-unknown-elf -mstack-protector-guard=sysreg %s 2>&1 | \
// RUN: FileCheck -check-prefix=INVALID-VALUE2 %s

// RUN: not %clang -target ppc32-unknown-elf -mstack-protector-guard=tls \
// RUN: -mstack-protector-guard-offset=20 -mstack-protector-guard-reg=r3 %s 2>&1 | \
// RUN: FileCheck -check-prefix=INVALID-REG-POWERPC32 %s

// CHECK-TLS-POWERPC32: "-cc1" {{.*}}"-mstack-protector-guard=tls" "-mstack-protector-guard-offset=24" "-mstack-protector-guard-reg=r2"
// INVALID-REG-POWERPC32: error: invalid value 'r3' in 'mstack-protector-guard-reg=', expected one of: r2
4 changes: 1 addition & 3 deletions llvm/include/llvm/CodeGen/TargetLowering.h
Original file line number Diff line number Diff line change
Expand Up @@ -5567,9 +5567,7 @@ class TargetLowering : public TargetLoweringBase {

/// If this function returns true, SelectionDAGBuilder emits a
/// LOAD_STACK_GUARD node when it is lowering Intrinsic::stackprotector.
virtual bool useLoadStackGuardNode() const {
return false;
}
virtual bool useLoadStackGuardNode(const Module &M) const { return false; }

virtual SDValue emitStackGuardXorFP(SelectionDAG &DAG, SDValue Val,
const SDLoc &DL) const {
Expand Down
4 changes: 2 additions & 2 deletions llvm/lib/CodeGen/GlobalISel/IRTranslator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2378,7 +2378,7 @@ bool IRTranslator::translateKnownIntrinsic(const CallInst &CI, Intrinsic::ID ID,
case Intrinsic::stackprotector: {
LLT PtrTy = getLLTForType(*CI.getArgOperand(0)->getType(), *DL);
Register GuardVal;
if (TLI->useLoadStackGuardNode()) {
if (TLI->useLoadStackGuardNode(*CI.getModule())) {
GuardVal = MRI->createGenericVirtualRegister(PtrTy);
getStackGuard(GuardVal, MIRBuilder);
} else
Expand Down Expand Up @@ -3869,7 +3869,7 @@ bool IRTranslator::emitSPDescriptorParent(StackProtectorDescriptor &SPD,

// If useLoadStackGuardNode returns true, generate LOAD_STACK_GUARD.
// Otherwise, emit a volatile load to retrieve the stack guard value.
if (TLI->useLoadStackGuardNode()) {
if (TLI->useLoadStackGuardNode(*ParentBB->getBasicBlock()->getModule())) {
Guard =
MRI->createGenericVirtualRegister(LLT::scalar(PtrTy.getSizeInBits()));
getStackGuard(Guard, *CurBuilder);
Expand Down
7 changes: 4 additions & 3 deletions llvm/lib/CodeGen/SelectionDAG/SelectionDAGBuilder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3138,7 +3138,7 @@ void SelectionDAGBuilder::visitSPDescriptorParent(StackProtectorDescriptor &SPD,
// If useLoadStackGuardNode returns true, generate LOAD_STACK_GUARD.
// Otherwise, emit a volatile load to retrieve the stack guard value.
SDValue Chain = DAG.getEntryNode();
if (TLI.useLoadStackGuardNode()) {
if (TLI.useLoadStackGuardNode(M)) {
Guard = getLoadStackGuard(DAG, dl, Chain);
} else {
const Value *IRGuard = TLI.getSDagStackGuard(M);
Expand Down Expand Up @@ -7349,7 +7349,7 @@ void SelectionDAGBuilder::visitIntrinsicCall(const CallInst &I,
const Module &M = *MF.getFunction().getParent();
EVT PtrTy = TLI.getValueType(DAG.getDataLayout(), I.getType());
SDValue Chain = getRoot();
if (TLI.useLoadStackGuardNode()) {
if (TLI.useLoadStackGuardNode(M)) {
Res = getLoadStackGuard(DAG, sdl, Chain);
Res = DAG.getPtrExtOrTrunc(Res, sdl, PtrTy);
} else {
Expand All @@ -7369,9 +7369,10 @@ void SelectionDAGBuilder::visitIntrinsicCall(const CallInst &I,
// Emit code into the DAG to store the stack guard onto the stack.
MachineFunction &MF = DAG.getMachineFunction();
MachineFrameInfo &MFI = MF.getFrameInfo();
const Module &M = *MF.getFunction().getParent();
SDValue Src, Chain = getRoot();

if (TLI.useLoadStackGuardNode())
if (TLI.useLoadStackGuardNode(M))
Src = getLoadStackGuard(DAG, sdl, Chain);
else
Src = getValue(I.getArgOperand(0)); // The guard's value.
Expand Down
4 changes: 2 additions & 2 deletions llvm/lib/Target/AArch64/AArch64ISelLowering.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -26982,9 +26982,9 @@ void AArch64TargetLowering::ReplaceNodeResults(
}
}

bool AArch64TargetLowering::useLoadStackGuardNode() const {
bool AArch64TargetLowering::useLoadStackGuardNode(const Module &M) const {
if (Subtarget->isTargetAndroid() || Subtarget->isTargetFuchsia())
return TargetLowering::useLoadStackGuardNode();
return TargetLowering::useLoadStackGuardNode(M);
return true;
}

Expand Down
2 changes: 1 addition & 1 deletion llvm/lib/Target/AArch64/AArch64ISelLowering.h
Original file line number Diff line number Diff line change
Expand Up @@ -811,7 +811,7 @@ class AArch64TargetLowering : public TargetLowering {
TargetLoweringBase::AtomicExpansionKind
shouldExpandAtomicCmpXchgInIR(AtomicCmpXchgInst *AI) const override;

bool useLoadStackGuardNode() const override;
bool useLoadStackGuardNode(const Module &M) const override;
TargetLoweringBase::LegalizeTypeAction
getPreferredVectorAction(MVT VT) const override;

Expand Down
2 changes: 1 addition & 1 deletion llvm/lib/Target/ARM/ARMISelLowering.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21305,7 +21305,7 @@ bool ARMTargetLowering::shouldInsertFencesForAtomic(
return InsertFencesForAtomic;
}

bool ARMTargetLowering::useLoadStackGuardNode() const {
bool ARMTargetLowering::useLoadStackGuardNode(const Module &M) const {
// ROPI/RWPI are not supported currently.
return !Subtarget->isROPI() && !Subtarget->isRWPI();
}
Expand Down
2 changes: 1 addition & 1 deletion llvm/lib/Target/ARM/ARMISelLowering.h
Original file line number Diff line number Diff line change
Expand Up @@ -675,7 +675,7 @@ class VectorType;
TargetLoweringBase::AtomicExpansionKind
shouldExpandAtomicCmpXchgInIR(AtomicCmpXchgInst *AI) const override;

bool useLoadStackGuardNode() const override;
bool useLoadStackGuardNode(const Module &M) const override;

void insertSSPDeclarations(Module &M) const override;
Value *getSDagStackGuard(const Module &M) const override;
Expand Down
8 changes: 4 additions & 4 deletions llvm/lib/Target/PowerPC/PPCISelLowering.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17884,10 +17884,10 @@ SDValue PPCTargetLowering::getNegatedExpression(SDValue Op, SelectionDAG &DAG,
}

// Override to enable LOAD_STACK_GUARD lowering on Linux.
bool PPCTargetLowering::useLoadStackGuardNode() const {
if (!Subtarget.isTargetLinux())
return TargetLowering::useLoadStackGuardNode();
return true;
bool PPCTargetLowering::useLoadStackGuardNode(const Module &M) const {
if (M.getStackProtectorGuard() == "tls" || Subtarget.isTargetLinux())
return true;
return TargetLowering::useLoadStackGuardNode(M);
}

// Override to disable global variable loading on Linux and insert AIX canary
Expand Down
2 changes: 1 addition & 1 deletion llvm/lib/Target/PowerPC/PPCISelLowering.h
Original file line number Diff line number Diff line change
Expand Up @@ -1137,7 +1137,7 @@ namespace llvm {
getExceptionSelectorRegister(const Constant *PersonalityFn) const override;

/// Override to support customized stack guard loading.
bool useLoadStackGuardNode() const override;
bool useLoadStackGuardNode(const Module &M) const override;
void insertSSPDeclarations(Module &M) const override;
Value *getSDagStackGuard(const Module &M) const override;

Expand Down
14 changes: 11 additions & 3 deletions llvm/lib/Target/PowerPC/PPCInstrInfo.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
#include "llvm/CodeGen/ScheduleDAG.h"
#include "llvm/CodeGen/SlotIndexes.h"
#include "llvm/CodeGen/StackMaps.h"
#include "llvm/IR/Module.h"
#include "llvm/MC/MCAsmInfo.h"
#include "llvm/MC/MCInst.h"
#include "llvm/MC/TargetRegistry.h"
Expand Down Expand Up @@ -3107,9 +3108,16 @@ bool PPCInstrInfo::expandPostRAPseudo(MachineInstr &MI) const {
return true;
}
case TargetOpcode::LOAD_STACK_GUARD: {
assert(Subtarget.isTargetLinux() &&
"Only Linux target is expected to contain LOAD_STACK_GUARD");
const int64_t Offset = Subtarget.isPPC64() ? -0x7010 : -0x7008;
auto M = MBB.getParent()->getFunction().getParent();
assert(
(Subtarget.isTargetLinux() || M->getStackProtectorGuard() == "tls") &&
"Only Linux target or tls mode are expected to contain "
"LOAD_STACK_GUARD");
int64_t Offset;
if (M->getStackProtectorGuard() == "tls")
Offset = M->getStackProtectorGuardOffset();
else
Offset = Subtarget.isPPC64() ? -0x7010 : -0x7008;
const unsigned Reg = Subtarget.isPPC64() ? PPC::X13 : PPC::R2;
MI.setDesc(get(Subtarget.isPPC64() ? PPC::LD : PPC::LWZ));
MachineInstrBuilder(*MI.getParent()->getParent(), MI)
Expand Down
4 changes: 2 additions & 2 deletions llvm/lib/Target/Sparc/SparcISelLowering.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3548,9 +3548,9 @@ void SparcTargetLowering::ReplaceNodeResults(SDNode *N,
}

// Override to enable LOAD_STACK_GUARD lowering on Linux.
bool SparcTargetLowering::useLoadStackGuardNode() const {
bool SparcTargetLowering::useLoadStackGuardNode(const Module &M) const {
if (!Subtarget->isTargetLinux())
return TargetLowering::useLoadStackGuardNode();
return TargetLowering::useLoadStackGuardNode(M);
return true;
}

Expand Down
2 changes: 1 addition & 1 deletion llvm/lib/Target/Sparc/SparcISelLowering.h
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ namespace llvm {
}

/// Override to support customized stack guard loading.
bool useLoadStackGuardNode() const override;
bool useLoadStackGuardNode(const Module &M) const override;
void insertSSPDeclarations(Module &M) const override;

/// getSetCCResultType - Return the ISD::SETCC ValueType
Expand Down
4 changes: 1 addition & 3 deletions llvm/lib/Target/SystemZ/SystemZISelLowering.h
Original file line number Diff line number Diff line change
Expand Up @@ -570,9 +570,7 @@ class SystemZTargetLowering : public TargetLowering {
getExceptionSelectorRegister(const Constant *PersonalityFn) const override;

/// Override to support customized stack guard loading.
bool useLoadStackGuardNode() const override {
return true;
}
bool useLoadStackGuardNode(const Module &M) const override { return true; }
void insertSSPDeclarations(Module &M) const override {
}

Expand Down
2 changes: 1 addition & 1 deletion llvm/lib/Target/X86/X86ISelLowering.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2682,7 +2682,7 @@ X86TargetLowering::X86TargetLowering(const X86TargetMachine &TM,
}

// This has so far only been implemented for 64-bit MachO.
bool X86TargetLowering::useLoadStackGuardNode() const {
bool X86TargetLowering::useLoadStackGuardNode(const Module &M) const {
return Subtarget.isTargetMachO() && Subtarget.is64Bit();
}

Expand Down
2 changes: 1 addition & 1 deletion llvm/lib/Target/X86/X86ISelLowering.h
Original file line number Diff line number Diff line change
Expand Up @@ -1568,7 +1568,7 @@ namespace llvm {
/// returns the address of that location. Otherwise, returns nullptr.
Value *getIRStackGuard(IRBuilderBase &IRB) const override;

bool useLoadStackGuardNode() const override;
bool useLoadStackGuardNode(const Module &M) const override;
bool useStackGuardXorFP() const override;
void insertSSPDeclarations(Module &M) const override;
Value *getSDagStackGuard(const Module &M) const override;
Expand Down
Loading

0 comments on commit 44b020a

Please sign in to comment.