This revision was automatically updated to reflect the committed changes.
Closed by commit rG102b4105e3fd: [CMSE] Clear padding bits of 
struct/unions/fp16 passed by value (authored by chill).
Herald added a project: clang.
Herald added a subscriber: cfe-commits.

Repository:
  rG LLVM Github Monorepo

CHANGES SINCE LAST ACTION
  https://reviews.llvm.org/D76369/new/

https://reviews.llvm.org/D76369

Files:
  clang/include/clang/AST/Decl.h
  clang/include/clang/Basic/DiagnosticSemaKinds.td
  clang/lib/AST/Decl.cpp
  clang/lib/CodeGen/CGCall.cpp
  clang/lib/CodeGen/CodeGenFunction.h
  clang/lib/Sema/SemaDeclAttr.cpp
  clang/lib/Sema/SemaExpr.cpp
  clang/lib/Sema/SemaStmt.cpp
  clang/test/CodeGen/cmse-clear-arg.c
  clang/test/CodeGen/cmse-clear-fp16.c
  clang/test/CodeGen/cmse-clear-return.c
  clang/test/Sema/arm-cmse-no-diag.c
  clang/test/Sema/arm-cmse.c

Index: clang/test/Sema/arm-cmse.c
===================================================================
--- clang/test/Sema/arm-cmse.c
+++ clang/test/Sema/arm-cmse.c
@@ -28,3 +28,30 @@
 void fn1() __attribute__((cmse_nonsecure_entry(1)));  // expected-error {{'cmse_nonsecure_entry' attribute takes no arguments}}
 
 typedef void (*fn2_t)() __attribute__((cmse_nonsecure_call("abc"))); // expected-error {{'cmse_nonsecure_call' attribute takes no argument}}
+
+union U { unsigned n; char b[4]; } u;
+
+union U xyzzy() __attribute__((cmse_nonsecure_entry)) {
+  return u; // expected-warning {{passing union across security boundary via return value may leak information}}
+}
+
+void (*fn2)(int, union U) __attribute__((cmse_nonsecure_call));
+void (*fn3)() __attribute__ ((cmse_nonsecure_call));
+
+struct S {
+  int t;
+  union {
+    char b[4];
+    unsigned w;
+  };
+} s;
+
+void qux() {
+  fn2(1,
+      u); // expected-warning {{passing union across security boundary via parameter 1 may leak information}}
+
+  fn3(
+       u, // expected-warning {{passing union across security boundary via parameter 0 may leak information}}
+       1,
+       s); // expected-warning {{passing union across security boundary via parameter 2 may leak information}}
+}
Index: clang/test/Sema/arm-cmse-no-diag.c
===================================================================
--- /dev/null
+++ clang/test/Sema/arm-cmse-no-diag.c
@@ -0,0 +1,11 @@
+// RUN: %clang_cc1 -triple thumbv8m.base-none-eabi -mcmse -verify -Wno-cmse-union-leak %s
+// expected-no-diagnostics
+
+union U { unsigned n; char b[4]; } u;
+
+void (*fn2)(int, union U) __attribute__((cmse_nonsecure_call));
+
+union U xyzzy() __attribute__((cmse_nonsecure_entry)) {
+  fn2(0, u);
+  return u;
+}
Index: clang/test/CodeGen/cmse-clear-return.c
===================================================================
--- /dev/null
+++ clang/test/CodeGen/cmse-clear-return.c
@@ -0,0 +1,265 @@
+// RUN: %clang_cc1 -triple thumbv8m.main   -O0 -mcmse -S -emit-llvm %s -o - | \
+// RUN:    FileCheck %s --check-prefixes=CHECK,CHECK-LE,CHECK-LE-NOPT,CHECK-SOFT
+// RUN: %clang_cc1 -triple thumbebv8m.main -O0 -mcmse -S -emit-llvm %s -o - | \
+// RUN:    FileCheck %s --check-prefixes=CHECK,CHECK-BE,CHECK-BE-NOPT,CHECK-SOFT
+// RUN: %clang_cc1 -triple thumbv8m.main   -O2 -mcmse -S -emit-llvm %s -o - | \
+// RUN:    FileCheck %s --check-prefixes=CHECK,CHECK-LE,CHECK-LE-OPT,CHECK-SOFT
+// RUN: %clang_cc1 -triple thumbebv8m.main -O2 -mcmse -S -emit-llvm %s -o - | \
+// RUN:    FileCheck %s --check-prefixes=CHECK,CHECK-BE,CHECK-BE-OPT,CHECK-SOFT
+// RUN: %clang_cc1 -triple thumbv8m.main   -O0 -mcmse -S -emit-llvm %s -o - \
+// RUN:            -mfloat-abi hard | \
+// RUN:    FileCheck %s --check-prefixes=CHECK,CHECK-LE,CHECK-LE-NOPT,CHECK-HARD
+
+
+//   :        Memory layout                | Mask
+// LE: .......1 ........ ........ ........ | 0x00000001/1
+// BE: 1....... ........ ........ ........ | 0x80000000/-2147483648
+typedef struct T0 {
+  int a : 1, : 31;
+} T0;
+
+T0 t0;
+__attribute__((cmse_nonsecure_entry)) T0 f0() { return t0; }
+// CHECK:    define {{.*}} @f0()
+// CHECK-LE: %[[R:.*]] = and i32 %{{.*}}, 1
+// CHECK-BE: %[[R:.*]] = and i32 %{{.*}}, -2147483648
+// CHECK:    ret i32 %[[R]]
+
+// LE: ......1. ........ ........ ........ 0x00000002/2
+// BE: .1...... ........ ........ ........ 0x40000000/1073741824
+typedef struct T1 {
+  int : 1, a : 1, : 30;
+} T1;
+
+T1 t1;
+__attribute__((cmse_nonsecure_entry)) T1 f1() { return t1; }
+// CHECK:    define {{.*}} @f1()
+// CHECK-LE: %[[R:.*]] = and i32 %{{.*}}, 2
+// CHECK-BE: %[[R:.*]] = and i32 %{{.*}}, 1073741824
+// CHECK:    ret i32 %[[R]]
+
+// LE: ........ .......1 ........ ........ 0x00000100/256
+// BE: ........ 1....... ........ ........ 0x00800000/8388608
+typedef struct T2 {
+  int : 8, a : 1, : 23;
+} T2;
+
+T2 t2;
+__attribute__((cmse_nonsecure_entry)) T2 f2() { return t2; }
+// CHECK:    define {{.*}} @f2()
+// CHECK-LE: %[[R:.*]] = and i32 %{{.*}}, 256
+// CHECK-BE: %[[R:.*]] = and i32 %{{.*}}, 8388608
+// CHECK:    ret i32 %[[R]]
+
+// LE: ........ .....1.. ........ ........ 0x00000400/1024
+// BE: ........ ..1..... ........ ........ 0x00200000/2097152
+typedef struct T3 {
+  int : 10, a : 1;
+} T3;
+
+T3 t3;
+__attribute__((cmse_nonsecure_entry)) T3 f3() { return t3; }
+// CHECK:    define {{.*}} @f3()
+// CHECK-LE: %[[R:.*]] = and i32 %{{.*}}, 1024
+// CHECK-BE: %[[R:.*]] = and i32 %{{.*}}, 2097152
+// CHECK:    ret i32 %[[R]]
+
+// LE: 11111111 ........ ........ ........ 0x000000ff/255
+// BE: 11111111 ........ ........ ........ 0xff000000/-16777216
+typedef struct T4 {
+  int a : 8, : 24;
+} T4;
+
+T4 t4;
+__attribute__((cmse_nonsecure_entry)) T4 f4() { return t4; }
+// CHECK: define {{.*}} @f4()
+// CHECK-LE: %[[R:.*]] = and i32 %{{.*}}, 255
+// CHECK-BE: %[[R:.*]] = and i32 %{{.*}}, -16777216
+// CHECK: ret i32 %[[R]]
+
+// LE: 1111111. .......1 ........ ........ 0x000001fe/510
+// BE: .1111111 1....... ........ ........ 0x7f800000/2139095040
+typedef struct T5 {
+  int : 1, a : 8, : 23;
+} T5;
+
+T5 t5;
+__attribute__((cmse_nonsecure_entry)) T5 f5() { return t5; }
+// CHECK:    define {{.*}} @f5()
+// CHECK-LE: %[[R:.*]] = and i32 %{{.*}}, 510
+// CHECK-BE: %[[R:.*]] = and i32 %{{.*}}, 2139095040
+// CHECK:    ret i32 %[[R]]
+
+// LE: 1111111. 11111111 ........ ........ 0x0000fffe/65534
+// BE: .1111111 11111111 ........ ........ 0x7fff0000/2147418112
+typedef struct T6 {
+  int : 1, a : 15, : 16;
+} T6;
+
+T6 t6;
+__attribute__((cmse_nonsecure_entry)) T6 f6() { return t6; }
+// CHECK:    define {{.*}} @f6()
+// CHECK-LE: %[[R:.*]] = and i32 %{{.*}}, 65534
+// CHECK-BE: %[[R:.*]] = and i32 %{{.*}}, 2147418112
+// CHECK:    ret i32 %[[R]]
+
+// LE: 1111111. 11111111 .......1 ........ 0x0001fffe/131070
+// BE: .1111111 11111111 1....... ........ 0x7fff8000/2147450880
+typedef struct T7 {
+  int : 1, a : 16, : 15;
+} T7;
+
+T7 t7;
+__attribute__((cmse_nonsecure_entry)) T7 f7() { return t7; }
+// CHECK:    define {{.*}} @f7()
+// CHECK-LE: %[[R:.*]] = and i32 %{{.*}}, 131070
+// CHECK-BE: %[[R:.*]] = and i32 %{{.*}}, 2147450880
+// CHECK:    ret i32 %[[R]]
+
+// LE: 11111111 111111.. 11111111 11111111 0xfffffcff/-769
+// BE: 11111111 ..111111 11111111 11111111 0xff3fffff/-12582913
+typedef struct T8 {
+  struct T80 {
+    char a;
+    char : 2, b : 6;
+  } a;
+  short b;
+} T8;
+
+T8 t8;
+__attribute__((cmse_nonsecure_entry)) T8 f8() { return t8; }
+// CHECK:    define {{.*}} @f8()
+// CHECK-LE: %[[R:.*]] = and i32 %{{.*}}, -769
+// CHECK-BE: %[[R:.*]] = and i32 %{{.*}}, -12582913
+// CHECK:    ret i32 %[[R]]
+
+// LE: ......11 ..111111 ...11111 ........ 0x001f3f03/2047747
+// BE: 11...... 111111.. 11111... ........ 0xc0fcf800/-1057163264
+typedef struct T9 {
+  struct T90 {
+    char a : 2;
+    char : 0;
+    short b : 6;
+  } a;
+  int b : 5;
+} T9;
+
+T9 t9;
+__attribute__((cmse_nonsecure_entry)) T9 f9() { return t9; }
+// CHECK:    define {{.*}} @f9()
+// CHECK-LE: %[[R:.*]] = and i32 %{{.*}}, 2047747
+// CHECK-BE: %[[R:.*]] = and i32 %{{.*}}, -1057163264
+// CHECK:    ret i32 %[[R]]
+
+T9 f91() { return t9; }
+// CHECK:  define {{.*}} @f91()
+// CHECK: %[[R:.*]] = load i32
+// CHECK: ret i32 %[[R]]
+
+// LE: 11111111 ........ 11111111 11111111 0xffff00ff/-65281
+// BE: 11111111 ........ 11111111 11111111 0xff00ffff/16711681
+typedef struct T10 {
+  char a;
+  short b;
+} T10;
+
+T10 t10;
+__attribute__((cmse_nonsecure_entry)) T10 f10() { return t10; }
+// CHECK: define {{.*}} @f10()
+// CHECK-LE: %[[R:.*]] = and i32 %{{.*}}, -65281
+// CHECK-BE: %[[R:.*]] = and i32 %{{.*}}, -16711681
+// CHECK: ret i32 %[[R]]
+
+// LE: 11111111 11111111 11111111 ........ 0x00ffffff/16777215
+// BE: 11111111 11111111 11111111 ........ 0xffffff00/-256
+typedef struct T11 {
+  short a;
+  char b;
+} T11;
+
+T11 t11;
+__attribute__((cmse_nonsecure_entry)) T11 f11() { return t11; }
+// CHECK: define {{.*}} @f11()
+// CHECK-LE: %[[R:.*]] = and i32 %{{.*}}, 16777215
+// CHECK-BE: %[[R:.*]] = and i32 %{{.*}}, -256
+// CHECK: ret i32 %[[R]]
+
+// LE: 11111111 11111111 11111111 ........ 0x00ffffff/16777215
+// BE: 11111111 11111111 11111111 ........ 0xffffff00/-256
+typedef struct T12 {
+  char a[3];
+} T12;
+
+T12 t12;
+__attribute__((cmse_nonsecure_entry)) T12 f12() { return t12; }
+// CHECK:    define {{.*}} @f12()
+// CHECK-LE-OPT:  %[[V0:.*]] = load i24, i24* bitcast (%struct.T12* @t12
+// CHECK-LE-OPT:  %[[R:.*]] = zext i24 %[[V0]] to i32
+// CHECK-LE-NOPT: %[[R:.*]] = and i32 %{{.*}}, 16777215
+
+// CHECK-BE-OPT:  %[[V0:.*]] = load i24, i24* bitcast (%struct.T12* @t12
+// CHECK-BE-OPT:  %[[V1:.*]] = zext i24 %[[V0]] to i32
+// CHECK-BE-OPT:  %[[R:.*]] = shl nuw i32 %[[V1]], 8
+// CHECK:         ret i32 %[[R]]
+
+// LE: 11111111 11111111 11111111 ........ 0x00ffffff/16777215
+// BE: 11111111 11111111 11111111 ........ 0xffffff00/-256
+typedef struct __attribute__((packed)) T13 {
+  char a;
+  short b;
+} T13;
+
+T13 t13;
+__attribute__((cmse_nonsecure_entry)) T13 f13() { return t13; }
+// CHECK:         define {{.*}} @f13()
+// CHECK-LE-OPT:  %[[V0:.*]] = load i24, i24* bitcast (%struct.T13* @t13
+// CHECK-LE-OPT:  %[[R:.*]] = zext i24 %[[V0]] to i32
+// CHECK-LE-NOPT: %[[R:.*]] = and i32 %{{.*}}, 16777215
+
+// CHECK-BE-OPT:  %[[V0:.*]] = load i24, i24* bitcast (%struct.T13* @t13
+// CHECK-BE-OPT:  %[[V1:.*]] = zext i24 %[[V0]] to i32
+// CHECK-BE-OPT:  %[[R:.*]] = shl nuw i32 %[[V1]], 8
+// CHECK:         ret i32 %[[R]]
+
+typedef struct __attribute__((packed)) T14 {
+  short a;
+  short b;
+} T14;
+
+T14 t14;
+__attribute__((cmse_nonsecure_entry)) T14 f14() { return t14; }
+// CHECK: define {{.*}} @f14()
+// CHECK: %[[R:.*]] = load
+// CHECK: ret i32 %[[R]]
+
+// LE: 1111..11 1111..11 11111111 11111111 0xfffff3f3/-3085
+// BE: 11..1111 11..1111 11111111 11111111 0xcfcfffff/-808452097
+typedef struct T17 {
+  struct T170 {
+    char a : 2;
+    char   : 2, b : 4;
+  } a[2];
+  char b[2];
+  char c[];
+} T17;
+
+T17 t17;
+__attribute__((cmse_nonsecure_entry)) T17 f17() { return t17; }
+// CHECK:    define {{.*}} @f17()
+// CHECK-LE: %[[R:.*]] = and i32 {{.*}}, -3085
+// CHECK-BE: %[[R:.*]] = and i32 {{.*}}, -808452097
+// CHECK: ret i32 %[[R]]
+
+typedef struct T21 {
+  float a;
+} T21;
+
+T21 t21;
+__attribute__((cmse_nonsecure_entry)) T21 f21() { return t21; }
+// CHECK:      define {{.*}} @f21()
+// CHECK-SOFT: ret i32
+// CHECK-HARD: ret %struct.T21
+
+__attribute__((cmse_nonsecure_entry)) float f22() { return 1.0f; }
+// CHECK: define {{.*}} @f22()
+// CHECK: ret float
Index: clang/test/CodeGen/cmse-clear-fp16.c
===================================================================
--- /dev/null
+++ clang/test/CodeGen/cmse-clear-fp16.c
@@ -0,0 +1,59 @@
+// RUN: %clang_cc1 -triple thumbv8m.main -O0 -mcmse  -S -emit-llvm \
+// RUN:            -fallow-half-arguments-and-returns %s -o - | \
+// RUN:    FileCheck %s --check-prefixes=CHECK,CHECK-NOPT-SOFT
+// RUN: %clang_cc1 -triple thumbv8m.main -O2 -mcmse  -S -emit-llvm \
+// RUN:            -fallow-half-arguments-and-returns %s -o - | \
+// RUN:    FileCheck %s --check-prefixes=CHECK,CHECK-OPT-SOFT
+// RUN: %clang_cc1 -triple thumbv8m.main -O0 -mcmse  -S -emit-llvm \
+// RUN:            -fallow-half-arguments-and-returns -mfloat-abi hard %s -o - | \
+// RUN:    FileCheck %s --check-prefixes=CHECK,CHECK-NOPT-HARD
+// RUN: %clang_cc1 -triple thumbv8m.main -O2 -mcmse  -S -emit-llvm \
+// RUN:            -fallow-half-arguments-and-returns -mfloat-abi hard %s -o - | \
+// RUN:    FileCheck %s --check-prefixes=CHECK,CHECK-OPT-HARD
+
+__fp16 g0();
+__attribute__((cmse_nonsecure_entry)) __fp16 f0() {
+  return g0();
+}
+// CHECK:           define {{.*}}@f0()
+
+// CHECK-NOPT-SOFT: %[[V0:.*]] = load i32
+// CHECK-NOPT-SOFT: %[[V1:.*]] = and i32 %[[V0]], 65535
+// CHECK-NOPT-SOFT: ret i32 %[[V1]]
+
+// CHECK-OPT-SOFT: %[[V0:.*]] = tail call {{.*}} @g0
+// CHECK-OPT-SOFT: %[[V1:.*]] = and i32 %[[V0]], 65535
+// CHECK-OPT-SOFT: ret i32 %[[V1]]
+
+// CHECK-NOPT-HARD: %[[V0:.*]] = bitcast float {{.*}} to i32
+// CHECK-NOPT-HARD: %[[V1:.*]] = and i32 %[[V0]], 65535
+// CHECK-NOPT-HARD: %[[V2:.*]] = bitcast i32 %[[V1]] to float
+// CHECK-NOPT-HARD: ret float %[[V2]]
+
+// CHECK-OPT-HARD: %[[V0:.*]] = bitcast float {{.*}} to i32
+// CHECK-OPT-HARD: %[[V1:.*]] = and i32 %[[V0]], 65535
+// CHECK-OPT-HARD: %[[V2:.*]] = bitcast i32 %[[V1]] to float
+// CHECK-OPT-HARD: ret float %[[V2]]
+
+void __attribute__((cmse_nonsecure_call)) (*g1)(__fp16);
+__fp16 x;
+void f1() {
+  g1(x);
+}
+// CHECK: define {{.*}}@f1()
+
+// CHECK-NOPT-SOFT: %[[V0:.*]] = load i32
+// CHECK-NOPT-SOFT: %[[V1:.*]] = and i32 %[[V0]], 65535
+// CHECK-NOPT-SOFT: call {{.*}} void {{.*}}(i32 %[[V1]])
+
+// CHECK-OPT-SOFT: %[[V1:.*]] = zext i16 {{.*}} to i32
+// CHECK-OPT-SOFT: call {{.*}} void {{.*}}(i32 %[[V1]])
+
+// CHECK-NOPT-HARD: %[[V0:.*]] = bitcast float {{.*}} to i32
+// CHECK-NOPT-HARD: %[[V1:.*]] = and i32 %[[V0]], 65535
+// CHECK-NOPT-HARD: %[[V2:.*]] = bitcast i32 %[[V1]] to float
+// CHECK-NOPT-HARD: call {{.*}}(float %[[V2]])
+
+// CHECK-OPT-HARD: %[[V0:.*]] = zext i16 {{.*}} to i32
+// CHECK-OPT-HARD: %[[V1:.*]] = bitcast i32 %[[V0]] to float
+// CHECK-OPT-HARD: call {{.*}}(float %[[V1]])
Index: clang/test/CodeGen/cmse-clear-arg.c
===================================================================
--- /dev/null
+++ clang/test/CodeGen/cmse-clear-arg.c
@@ -0,0 +1,189 @@
+// RUN: %clang_cc1 -triple thumbv8m.main   -O0 -mcmse -S -emit-llvm %s -o - | \
+// RUN:    FileCheck %s --check-prefixes=CHECK,CHECK-LE,CHECK-SOFTFP
+// RUN: %clang_cc1 -triple thumbebv8m.main -O0 -mcmse -S -emit-llvm %s -o - | \
+// RUN:    FileCheck %s --check-prefixes=CHECK,CHECK-BE,CHECK-SOFTFP
+// RUN: %clang_cc1 -triple thumbv8m.main   -O2 -mcmse -S -emit-llvm %s -o - | \
+// RUN:    FileCheck %s --check-prefixes=CHECK,CHECK-LE,CHECK-SOFTFP
+// RUN: %clang_cc1 -triple thumbebv8m.main -O2 -mcmse -S -emit-llvm %s -o - | \
+// RUN:    FileCheck %s --check-prefixes=CHECK,CHECK-BE,CHECK-SOFTFP
+// RUN: %clang_cc1 -triple thumbv8m.main   -O0 -mcmse -mfloat-abi hard  \
+// RUN:            -S -emit-llvm %s -o - | \
+// RUN:    FileCheck %s --check-prefixes=CHECK,CHECK-LE,CHECK-HARDFP
+
+// We don't really need to repeat *all* the test cases from cmse-clear-return.c
+// as it won't increase test coverage.
+
+//   :        Memory layout                | Mask
+// LE: .......1 ........ ........ ........ | 0x00000001/1
+// BE: 1....... ........ ........ ........ | 0x80000000/-2147483648
+typedef struct T0 {
+  int a : 1, : 31;
+} T0;
+
+void __attribute__((cmse_nonsecure_call)) (*g0)(T0);
+
+T0 t0;
+void f0() { g0(t0); }
+// CHECK:    define {{.*}} @f0()
+// CHECK-LE: %[[V0:.*]] = and i32 {{.*}}, 1
+// CHECK-BE: %[[V0:.*]] = and i32 {{.*}}, -2147483648
+// CHECK:    %[[V1:.*]] = insertvalue [1 x i32] undef, i32 %[[V0]], 0
+// CHECK:    call {{.*}} void %0([1 x i32] %[[V1]])
+
+// LE: 11111111 111111.. 11111111 11111111 0xfffffcff/-769
+// BE: 11111111 ..111111 11111111 11111111 0xff3fffff/-12582913
+typedef struct T8 {
+  struct T80 {
+    char a;
+    char : 2, b : 6;
+  } a;
+  short b;
+} T8;
+
+T8 t8;
+void __attribute__((cmse_nonsecure_call)) (*g8)(T8);
+void f8() { g8(t8); }
+// CHECK:    define {{.*}} @f8()
+// CHECK-LE: %[[V0:.*]] = and i32 {{.*}}, -769
+// CHECK-BE: %[[V0:.*]] = and i32 {{.*}}, -12582913
+// CHECK:    %[[V1:.*]] = insertvalue [1 x i32] undef, i32 %[[V0]], 0
+// CHECK:    call {{.*}} void %0([1 x i32] %[[V1]])
+
+// LE(0): 11111111 ........ 11111111 11111111 0xffff00ff/-65281
+// LE(4): ...111.. 11111... 11111111 .....111 0x7fff81c/134215708
+// BE(0): 11111111 ........ 11111111 11111111 0xff00ffff/-16711681
+// BE(4): ..111... ...11111 11111111 111..... 0x381fffe0/941621216
+typedef struct T15 {
+  char a;
+  short b;
+  int : 2, c : 3, : 6, d : 16;
+} T15;
+
+T15 t15;
+
+void __attribute__((cmse_nonsecure_call)) (*g15_0)(T15);
+void f15_0() {
+  g15_0(t15);
+}
+// CHECK: define {{.*}}@f15_0()
+// CHECK: %[[FN:.*]] = load {{.*}} @g15_0
+// CHECK-LE:  %cmse.clear = and i32 {{.*}}, -65281
+// CHECK-BE:  %cmse.clear = and i32 {{.*}}, -16711681
+// CHECK: %[[R0:.*]] = insertvalue [2 x i32] undef, i32 %cmse.clear, 0
+// CHECK-LE: %cmse.clear1 = and i32 {{.*}}, 134215708
+// CHECK-BE: %cmse.clear1 = and i32 {{.*}}, 941621216
+// CHECK: %[[R1:.*]] = insertvalue [2 x i32] %[[R0]], i32 %cmse.clear1, 1
+// CHECK: call {{.*}} void %[[FN]]([2 x i32] %[[R1]])
+
+void __attribute__((cmse_nonsecure_call)) (*g15_1)(int, int, int, T15);
+void f15_1() {
+  g15_1(0, 1, 2, t15);
+}
+// CHECK: define {{.*}}@f15_1()
+// CHECK: %[[FN:.*]] = load {{.*}} @g15_1
+// CHECK-LE:  %cmse.clear = and i32 {{.*}}, -65281
+// CHECK-BE:  %cmse.clear = and i32 {{.*}}, -16711681
+// CHECK: %[[R0:.*]] = insertvalue [2 x i32] undef, i32 %cmse.clear, 0
+// CHECK-LE: %cmse.clear1 = and i32 {{.*}}, 134215708
+// CHECK-BE: %cmse.clear1 = and i32 {{.*}}, 941621216
+// CHECK: %[[R1:.*]] = insertvalue [2 x i32] %[[R0]], i32 %cmse.clear1, 1
+// CHECK: call {{.*}} void %[[FN]](i32 0, i32 1, i32 2, [2 x i32] %[[R1]])
+
+// LE: 11111111 ........ 11111111 11111111 1111.... ...11111 ........ .111111.
+// LE: 0xff00fffff01f007e/9079291968726434047
+// BE: 11111111 ........ 11111111 11111111 ....1111 11111... ........ .111111.
+// BE: 0xff00ffff0ff8007e/-71776123088273282
+
+typedef struct T16 {
+  char a;
+  short b;
+  long long : 4, c : 9, : 12, d : 6;
+} T16;
+
+T16 t16;
+
+void __attribute__((cmse_nonsecure_call)) (*g16_0)(T16);
+void f16_0() {
+  g16_0(t16);
+}
+// CHECK: define {{.*}} @f16_0()
+// CHECK: %[[FN:.*]] = load {{.*}} @g16_0
+// CHECK-LE: %cmse.clear = and i64 {{.*}}, 9079291968726434047
+// CHECK-BE: %cmse.clear = and i64 {{.*}}, -71776123088273282
+// CHECK: %[[R:.*]] = insertvalue [1 x i64] undef, i64 %cmse.clear, 0
+// CHECK: call {{.*}} void %0([1 x i64] %[[R]])
+
+
+// LE0: 1111..11 .......1 1111..11 .......1 1111..11 .......1 1111..11 .......1
+// LE4: 1111..11 .......1 1111..11 .......1 11111111 11111111 11111111 ........
+// LE : 0x01f301f3/32702963 * 3 + 0x00ffffff/16777215
+// BE0: 11..1111 1....... 11..1111 1....... 11..1111 1....... 11..1111 1.......
+// BE4: 11..1111 1....... 11..1111 1....... 11111111 11111111 11111111 ........
+// BE : 0xcf80cf80/-813641856 * 3 + 0xffffff00/-256
+
+typedef struct T18 {
+  struct T180 {
+    short a : 2;
+    short   : 2, b : 5;
+  } a[2][3];
+  char b[3];
+  char c[];
+} T18;
+
+T18 t18;
+
+void __attribute__((cmse_nonsecure_call)) (*g18)(T18);
+void f18() {
+  g18(t18);
+}
+// CHECK:    define {{.*}} @f18()
+// CHECK:    %[[FN:.*]] = load {{.*}} @g18
+// CHECK-LE: %cmse.clear = and i32 {{.*}}, 32702963
+// CHECK-BE: %cmse.clear = and i32 {{.*}}, -813641856
+// CHECK:    %[[R0:.*]] = insertvalue [4 x i32] undef, i32 %cmse.clear, 0
+// CHECK-LE: %cmse.clear1 = and i32 {{.*}}, 32702963
+// CHECK-BE: %cmse.clear1 = and i32 {{.*}}, -813641856
+// CHECK:    %[[R1:.*]] = insertvalue [4 x i32] %[[R0]], i32 %cmse.clear1, 1
+// CHECK-LE: %cmse.clear2 = and i32 {{.*}}, 32702963
+// CHECK-BE: %cmse.clear2 = and i32 {{.*}}, -813641856
+// CHECK:    %[[R2:.*]] = insertvalue [4 x i32] %[[R1]], i32 %cmse.clear2, 2
+// CHECK-LE: %cmse.clear3 = and i32 {{.*}}, 16777215
+// CHECK-BE: %cmse.clear3 = and i32 {{.*}}, -256
+// CHECK:    %[[R3:.*]] = insertvalue [4 x i32] %[[R2]], i32 %cmse.clear3, 3
+// CHECK:    call {{.*}} void %[[FN]]([4 x i32] %[[R3]])
+
+// LE: 11111111 11111111 ..111... ..111... 0x3838ffff/943259647
+// BE: 11111111 11111111 ...111.. ...111.. 0xffff1c1c/-58340
+typedef union T19 {
+  short a;
+  struct T190 {
+    char : 3, a : 3;
+  } b[4];
+} T19;
+
+T19 t19;
+void __attribute__((cmse_nonsecure_call)) (*g19)(T19);
+void f19() {
+  g19(t19);
+}
+// CHECK:    define {{.*}} @f19()
+// CHECK:    %[[FN:.*]] = load {{.*}} @g19
+// CHECK-LE: %cmse.clear = and i32 {{.*}}, 943259647
+// CHECK-BE: %cmse.clear = and i32 {{.*}}, -58340
+// CHECK:    %[[R:.*]] = insertvalue [1 x i32] undef, i32 %cmse.clear, 0
+// CHECK:    call {{.*}} void %[[FN]]([1 x i32] %[[R]])
+
+
+typedef struct T20 {
+  float a[2];
+} T20;
+
+T20 t20;
+void __attribute__((cmse_nonsecure_call)) (*g20)(T20);
+void f20() {
+  g20(t20);
+}
+// CHECK: define {{.*}} @f20()
+// CHECK:    %[[FN:.*]] = load {{.*}} @g20
+// CHECK-SOFTFP: call arm_aapcscc void %[[FN]]([2 x i32]
+// CHECK-HARDFP: call arm_aapcs_vfpcc void %[[FN]](%struct.T20
Index: clang/lib/Sema/SemaStmt.cpp
===================================================================
--- clang/lib/Sema/SemaStmt.cpp
+++ clang/lib/Sema/SemaStmt.cpp
@@ -3631,6 +3631,12 @@
       if (isa<CXXBoolLiteralExpr>(RetValExp))
         Diag(ReturnLoc, diag::warn_main_returns_bool_literal)
           << RetValExp->getSourceRange();
+    if (FD->hasAttr<CmseNSEntryAttr>() && RetValExp) {
+      if (const auto *RT = dyn_cast<RecordType>(FnRetType.getCanonicalType())) {
+        if (RT->getDecl()->isOrContainsUnion())
+          Diag(RetValExp->getBeginLoc(), diag::warn_cmse_nonsecure_union) << 1;
+      }
+    }
   } else if (ObjCMethodDecl *MD = getCurMethodDecl()) {
     FnRetType = MD->getReturnType();
     isObjCMethod = true;
Index: clang/lib/Sema/SemaExpr.cpp
===================================================================
--- clang/lib/Sema/SemaExpr.cpp
+++ clang/lib/Sema/SemaExpr.cpp
@@ -6583,6 +6583,18 @@
   if (NDecl)
     DiagnoseSentinelCalls(NDecl, LParenLoc, Args);
 
+  // Warn for unions passing across security boundary (CMSE).
+  if (FuncT != nullptr && FuncT->getCmseNSCallAttr()) {
+    for (unsigned i = 0, e = Args.size(); i != e; i++) {
+      if (const auto *RT =
+              dyn_cast<RecordType>(Args[i]->getType().getCanonicalType())) {
+        if (RT->getDecl()->isOrContainsUnion())
+          Diag(Args[i]->getBeginLoc(), diag::warn_cmse_nonsecure_union)
+              << 0 << i;
+      }
+    }
+  }
+
   // Do special checking on direct calls to functions.
   if (FDecl) {
     if (CheckFunctionCall(FDecl, TheCall, Proto))
Index: clang/lib/Sema/SemaDeclAttr.cpp
===================================================================
--- clang/lib/Sema/SemaDeclAttr.cpp
+++ clang/lib/Sema/SemaDeclAttr.cpp
@@ -1998,7 +1998,8 @@
     return;
   }
 
-  if (cast<FunctionDecl>(D)->getStorageClass() == SC_Static) {
+  const auto *FD = cast<FunctionDecl>(D);
+  if (!FD->isExternallyVisible()) {
     S.Diag(AL.getLoc(), diag::warn_attribute_cmse_entry_static);
     return;
   }
Index: clang/lib/CodeGen/CodeGenFunction.h
===================================================================
--- clang/lib/CodeGen/CodeGenFunction.h
+++ clang/lib/CodeGen/CodeGenFunction.h
@@ -3877,6 +3877,11 @@
   llvm::Value *EmitARMCDEBuiltinExpr(unsigned BuiltinID, const CallExpr *E,
                                      ReturnValueSlot ReturnValue,
                                      llvm::Triple::ArchType Arch);
+  llvm::Value *EmitCMSEClearRecord(llvm::Value *V, llvm::IntegerType *ITy,
+                                   QualType RTy);
+  llvm::Value *EmitCMSEClearRecord(llvm::Value *V, llvm::ArrayType *ATy,
+                                   QualType RTy);
+  llvm::Value *EmitCMSEClearFP16(llvm::Value *V);
 
   llvm::Value *EmitCommonNeonBuiltinExpr(unsigned BuiltinID,
                                          unsigned LLVMIntrinsic,
Index: clang/lib/CodeGen/CGCall.cpp
===================================================================
--- clang/lib/CodeGen/CGCall.cpp
+++ clang/lib/CodeGen/CGCall.cpp
@@ -16,6 +16,7 @@
 #include "CGBlocks.h"
 #include "CGCXXABI.h"
 #include "CGCleanup.h"
+#include "CGRecordLayout.h"
 #include "CodeGenFunction.h"
 #include "CodeGenModule.h"
 #include "TargetInfo.h"
@@ -2871,6 +2872,213 @@
   return store;
 }
 
+// Helper functions for EmitCMSEClearRecord
+
+// Set the bits corresponding to a field having width `BitWidth` and located at
+// offset `BitOffset` (from the least significant bit) within a storage unit of
+// `Bits.size()` bytes. Each element of `Bits` corresponds to one target byte.
+// Use little-endian layout, i.e.`Bits[0]` is the LSB.
+static void setBitRange(SmallVectorImpl<uint64_t> &Bits, int BitOffset,
+                        int BitWidth, int CharWidth) {
+  assert(CharWidth <= 64);
+  assert(static_cast<unsigned>(BitWidth) <= Bits.size() * CharWidth);
+
+  int Pos = 0;
+  if (BitOffset >= CharWidth) {
+    Pos += BitOffset / CharWidth;
+    BitOffset = BitOffset % CharWidth;
+  }
+
+  const uint64_t Used = (uint64_t(1) << CharWidth) - 1;
+  if (BitOffset + BitWidth >= CharWidth) {
+    Bits[Pos++] |= (Used << BitOffset) & Used;
+    BitWidth -= CharWidth - BitOffset;
+    BitOffset = 0;
+  }
+
+  while (BitWidth >= CharWidth) {
+    Bits[Pos++] = Used;
+    BitWidth -= CharWidth;
+  }
+
+  if (BitWidth > 0)
+    Bits[Pos++] |= (Used >> (CharWidth - BitWidth)) << BitOffset;
+}
+
+// Set the bits corresponding to a field having width `BitWidth` and located at
+// offset `BitOffset` (from the least significant bit) within a storage unit of
+// `StorageSize` bytes, located at `StorageOffset` in `Bits`. Each element of
+// `Bits` corresponds to one target byte. Use target endian layout.
+static void setBitRange(SmallVectorImpl<uint64_t> &Bits, int StorageOffset,
+                        int StorageSize, int BitOffset, int BitWidth,
+                        int CharWidth, bool BigEndian) {
+
+  SmallVector<uint64_t, 8> TmpBits(StorageSize);
+  setBitRange(TmpBits, BitOffset, BitWidth, CharWidth);
+
+  if (BigEndian)
+    std::reverse(TmpBits.begin(), TmpBits.end());
+
+  for (uint64_t V : TmpBits)
+    Bits[StorageOffset++] |= V;
+}
+
+static void setUsedBits(CodeGenModule &, QualType, int,
+                        SmallVectorImpl<uint64_t> &);
+
+// Set the bits in `Bits`, which correspond to the value representations of
+// the actual members of the record type `RTy`. Note that this function does
+// not handle base classes, virtual tables, etc, since they cannot happen in
+// CMSE function arguments or return. The bit mask corresponds to the target
+// memory layout, i.e. it's endian dependent.
+static void setUsedBits(CodeGenModule &CGM, const RecordType *RTy, int Offset,
+                        SmallVectorImpl<uint64_t> &Bits) {
+  ASTContext &Context = CGM.getContext();
+  int CharWidth = Context.getCharWidth();
+  const RecordDecl *RD = RTy->getDecl()->getDefinition();
+  const ASTRecordLayout &ASTLayout = Context.getASTRecordLayout(RD);
+  const CGRecordLayout &Layout = CGM.getTypes().getCGRecordLayout(RD);
+
+  int Idx = 0;
+  for (auto I = RD->field_begin(), E = RD->field_end(); I != E; ++I, ++Idx) {
+    const FieldDecl *F = *I;
+
+    if (F->isUnnamedBitfield() || F->isZeroLengthBitField(Context) ||
+        F->getType()->isIncompleteArrayType())
+      continue;
+
+    if (F->isBitField()) {
+      const CGBitFieldInfo &BFI = Layout.getBitFieldInfo(F);
+      setBitRange(Bits, Offset + BFI.StorageOffset.getQuantity(),
+                  BFI.StorageSize / CharWidth, BFI.Offset,
+                  BFI.Size, CharWidth,
+                  CGM.getDataLayout().isBigEndian());
+      continue;
+    }
+
+    setUsedBits(CGM, F->getType(),
+                Offset + ASTLayout.getFieldOffset(Idx) / CharWidth, Bits);
+  }
+}
+
+// Set the bits in `Bits`, which correspond to the value representations of
+// the elements of an array type `ATy`.
+static void setUsedBits(CodeGenModule &CGM, const ConstantArrayType *ATy,
+                        int Offset, SmallVectorImpl<uint64_t> &Bits) {
+  const ASTContext &Context = CGM.getContext();
+
+  QualType ETy = Context.getBaseElementType(ATy);
+  int Size = Context.getTypeSizeInChars(ETy).getQuantity();
+  SmallVector<uint64_t, 4> TmpBits(Size);
+  setUsedBits(CGM, ETy, 0, TmpBits);
+
+  for (int I = 0, N = Context.getConstantArrayElementCount(ATy); I < N; ++I) {
+    auto Src = TmpBits.begin();
+    auto Dst = Bits.begin() + Offset + I * Size;
+    for (int J = 0; J < Size; ++J)
+      *Dst++ |= *Src++;
+  }
+}
+
+// Set the bits in `Bits`, which correspond to the value representations of
+// the type `QTy`.
+static void setUsedBits(CodeGenModule &CGM, QualType QTy, int Offset,
+                        SmallVectorImpl<uint64_t> &Bits) {
+  if (const auto *RTy = QTy->getAs<RecordType>())
+    return setUsedBits(CGM, RTy, Offset, Bits);
+
+  ASTContext &Context = CGM.getContext();
+  if (const auto *ATy = Context.getAsConstantArrayType(QTy))
+    return setUsedBits(CGM, ATy, Offset, Bits);
+
+  int Size = Context.getTypeSizeInChars(QTy).getQuantity();
+  if (Size <= 0)
+    return;
+
+  std::fill_n(Bits.begin() + Offset, Size,
+              (uint64_t(1) << Context.getCharWidth()) - 1);
+}
+
+static uint64_t buildMultiCharMask(const SmallVectorImpl<uint64_t> &Bits,
+                                   int Pos, int Size, int CharWidth,
+                                   bool BigEndian) {
+  assert(Size > 0);
+  uint64_t Mask = 0;
+  if (BigEndian) {
+    for (auto P = Bits.begin() + Pos, E = Bits.begin() + Pos + Size; P != E;
+         ++P)
+      Mask = (Mask << CharWidth) | *P;
+  } else {
+    auto P = Bits.begin() + Pos + Size, End = Bits.begin() + Pos;
+    do
+      Mask = (Mask << CharWidth) | *--P;
+    while (P != End);
+  }
+  return Mask;
+}
+
+// Emit code to clear the bits in a record, which aren't a part of any user
+// declared member, when the record is a function return.
+llvm::Value *CodeGenFunction::EmitCMSEClearRecord(llvm::Value *Src,
+                                                  llvm::IntegerType *ITy,
+                                                  QualType QTy) {
+  assert(Src->getType() == ITy);
+  assert(ITy->getScalarSizeInBits() <= 64);
+
+  const llvm::DataLayout &DataLayout = CGM.getDataLayout();
+  int Size = DataLayout.getTypeStoreSize(ITy);
+  SmallVector<uint64_t, 4> Bits(Size);
+  setUsedBits(CGM, QTy->getAs<RecordType>(), 0, Bits);
+
+  int CharWidth = CGM.getContext().getCharWidth();
+  uint64_t Mask =
+      buildMultiCharMask(Bits, 0, Size, CharWidth, DataLayout.isBigEndian());
+
+  return Builder.CreateAnd(Src, Mask, "cmse.clear");
+}
+
+// Emit code to clear the bits in a record, which aren't a part of any user
+// declared member, when the record is a function argument.
+llvm::Value *CodeGenFunction::EmitCMSEClearRecord(llvm::Value *Src,
+                                                  llvm::ArrayType *ATy,
+                                                  QualType QTy) {
+  const llvm::DataLayout &DataLayout = CGM.getDataLayout();
+  int Size = DataLayout.getTypeStoreSize(ATy);
+  SmallVector<uint64_t, 16> Bits(Size);
+  setUsedBits(CGM, QTy->getAs<RecordType>(), 0, Bits);
+
+  // Clear each element of the LLVM array.
+  int CharWidth = CGM.getContext().getCharWidth();
+  int CharsPerElt =
+      ATy->getArrayElementType()->getScalarSizeInBits() / CharWidth;
+  int MaskIndex = 0;
+  llvm::Value *R = llvm::UndefValue::get(ATy);
+  for (int I = 0, N = ATy->getArrayNumElements(); I != N; ++I) {
+    uint64_t Mask = buildMultiCharMask(Bits, MaskIndex, CharsPerElt, CharWidth,
+                                       DataLayout.isBigEndian());
+    MaskIndex += CharsPerElt;
+    llvm::Value *T0 = Builder.CreateExtractValue(Src, I);
+    llvm::Value *T1 = Builder.CreateAnd(T0, Mask, "cmse.clear");
+    R = Builder.CreateInsertValue(R, T1, I);
+  }
+
+  return R;
+}
+
+// Emit code to clear the padding bits when returning or passing as an argument
+// a 16-bit floating-point value.
+llvm::Value *CodeGenFunction::EmitCMSEClearFP16(llvm::Value *Src) {
+  llvm::Type *RetTy = Src->getType();
+  assert(RetTy->isFloatTy() ||
+         RetTy->isIntegerTy() && RetTy->getIntegerBitWidth() == 32);
+  if (RetTy->isFloatTy()) {
+    llvm::Value *T0 = Builder.CreateBitCast(Src, Builder.getIntNTy(32));
+    llvm::Value *T1 = Builder.CreateAnd(T0, 0xffff, "cmse.clear");
+    return Builder.CreateBitCast(T1, RetTy);
+  }
+  return Builder.CreateAnd(Src, 0xffff, "cmse.clear");
+}
+
 void CodeGenFunction::EmitFunctionEpilog(const CGFunctionInfo &FI,
                                          bool EmitRetDbgLoc,
                                          SourceLocation EndLoc) {
@@ -3037,6 +3245,21 @@
 
   llvm::Instruction *Ret;
   if (RV) {
+    if (CurFuncDecl && CurFuncDecl->hasAttr<CmseNSEntryAttr>()) {
+      // For certain return types, clear padding bits, as they may reveal
+      // sensitive information.
+      const Type *RTy = RetTy.getCanonicalType().getTypePtr();
+      if (RTy->isFloat16Type() || RTy->isHalfType()) {
+        // 16-bit floating-point types are passed in a 32-bit integer or float,
+        // with unspecified upper bits.
+        RV = EmitCMSEClearFP16(RV);
+      } else {
+        // Small struct/union types are passed as integers.
+        auto *ITy = dyn_cast<llvm::IntegerType>(RV->getType());
+        if (ITy != nullptr && isa<RecordType>(RetTy.getCanonicalType()))
+          RV = EmitCMSEClearRecord(RV, ITy, RetTy);
+      }
+    }
     EmitReturnValueCheck(RV);
     Ret = Builder.CreateRet(RV);
   } else {
@@ -4332,8 +4555,25 @@
       } else {
         // In the simple case, just pass the coerced loaded value.
         assert(NumIRArgs == 1);
-        IRCallArgs[FirstIRArg] =
-          CreateCoercedLoad(Src, ArgInfo.getCoerceToType(), *this);
+        llvm::Value *Load =
+            CreateCoercedLoad(Src, ArgInfo.getCoerceToType(), *this);
+
+        if (CallInfo.isCmseNSCall()) {
+          // For certain parameter types, clear padding bits, as they may reveal
+          // sensitive information.
+          const Type *PTy = I->Ty.getCanonicalType().getTypePtr();
+          // 16-bit floating-point types are passed in a 32-bit integer or
+          // float, with unspecified upper bits.
+          if (PTy->isFloat16Type() || PTy->isHalfType()) {
+            Load = EmitCMSEClearFP16(Load);
+          } else {
+            // Small struct/union types are passed as integer arrays.
+            auto *ATy = dyn_cast<llvm::ArrayType>(Load->getType());
+            if (ATy != nullptr && isa<RecordType>(I->Ty.getCanonicalType()))
+              Load = EmitCMSEClearRecord(Load, ATy, I->Ty);
+          }
+        }
+        IRCallArgs[FirstIRArg] = Load;
       }
 
       break;
Index: clang/lib/AST/Decl.cpp
===================================================================
--- clang/lib/AST/Decl.cpp
+++ clang/lib/AST/Decl.cpp
@@ -4410,6 +4410,21 @@
   addAttr(CapturedRecordAttr::CreateImplicit(getASTContext()));
 }
 
+bool RecordDecl::isOrContainsUnion() const {
+  if (isUnion())
+    return true;
+
+  if (const RecordDecl *Def = getDefinition()) {
+    for (const FieldDecl *FD : Def->fields()) {
+      const RecordType *RT = FD->getType()->getAs<RecordType>();
+      if (RT && RT->getDecl()->isOrContainsUnion())
+        return true;
+    }
+  }
+
+  return false;
+}
+
 RecordDecl::field_iterator RecordDecl::field_begin() const {
   if (hasExternalLexicalStorage() && !hasLoadedFieldsFromExternalStorage())
     LoadFieldsFromExternalStorage();
Index: clang/include/clang/Basic/DiagnosticSemaKinds.td
===================================================================
--- clang/include/clang/Basic/DiagnosticSemaKinds.td
+++ clang/include/clang/Basic/DiagnosticSemaKinds.td
@@ -3123,6 +3123,10 @@
 def warn_attribute_cmse_entry_static : Warning<
   "'cmse_nonsecure_entry' cannot be applied to functions with internal linkage">,
   InGroup<IgnoredAttributes>;
+def warn_cmse_nonsecure_union : Warning<
+  "passing union across security boundary via %select{parameter %1|return value}0 "
+  "may leak information">,
+  InGroup<DiagGroup<"cmse-union-leak">>;
 def err_attribute_weak_static : Error<
   "weak declaration cannot have internal linkage">;
 def err_attribute_selectany_non_extern_data : Error<
Index: clang/include/clang/AST/Decl.h
===================================================================
--- clang/include/clang/AST/Decl.h
+++ clang/include/clang/AST/Decl.h
@@ -3961,6 +3961,11 @@
     return cast_or_null<RecordDecl>(TagDecl::getDefinition());
   }
 
+  /// Returns whether this record is a union, or contains (at any nesting level)
+  /// a union member. This is used by CMSE to warn about possible information
+  /// leaks.
+  bool isOrContainsUnion() const;
+
   // Iterator access to field members. The field iterator only visits
   // the non-static data members of this class, ignoring any static
   // data members, functions, constructors, destructors, etc.
_______________________________________________
cfe-commits mailing list
cfe-commits@lists.llvm.org
https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits

Reply via email to