1 /* 2 * Copyright (C) 2017 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 import dalvik.system.VMRuntime; 18 import java.lang.invoke.MethodHandles; 19 import java.lang.invoke.MethodType; 20 import java.util.function.Consumer; 21 22 public class ChildClass { 23 enum PrimitiveType { 24 TInteger('I', Integer.TYPE, Integer.valueOf(0)), 25 TLong('J', Long.TYPE, Long.valueOf(0)), 26 TFloat('F', Float.TYPE, Float.valueOf(0)), 27 TDouble('D', Double.TYPE, Double.valueOf(0)), 28 TBoolean('Z', Boolean.TYPE, Boolean.valueOf(false)), 29 TByte('B', Byte.TYPE, Byte.valueOf((byte) 0)), 30 TShort('S', Short.TYPE, Short.valueOf((short) 0)), 31 TCharacter('C', Character.TYPE, Character.valueOf('0')); 32 PrimitiveType(char shorty, Class klass, Object value)33 PrimitiveType(char shorty, Class klass, Object value) { 34 mShorty = shorty; 35 mClass = klass; 36 mDefaultValue = value; 37 } 38 39 public char mShorty; 40 public Class mClass; 41 public Object mDefaultValue; 42 } 43 44 enum Hiddenness { 45 Sdk(PrimitiveType.TShort), 46 Unsupported(PrimitiveType.TBoolean), 47 ConditionallyBlocked(PrimitiveType.TByte), 48 Blocklist(PrimitiveType.TCharacter), 49 BlocklistAndCorePlatformApi(PrimitiveType.TInteger); 50 Hiddenness(PrimitiveType type)51 Hiddenness(PrimitiveType type) { mAssociatedType = type; } 52 public PrimitiveType mAssociatedType; 53 } 54 55 enum Visibility { 56 Public(PrimitiveType.TInteger), 57 Package(PrimitiveType.TFloat), 58 Protected(PrimitiveType.TLong), 59 Private(PrimitiveType.TDouble); 60 Visibility(PrimitiveType type)61 Visibility(PrimitiveType type) { mAssociatedType = type; } 62 public PrimitiveType mAssociatedType; 63 } 64 65 enum Behaviour { 66 Granted, 67 Warning, 68 Denied, 69 } 70 71 // This needs to be kept in sync with DexDomain in Main. 72 enum DexDomain { 73 CorePlatform, 74 Platform, 75 Application 76 } 77 78 private static final boolean booleanValues[] = new boolean[] { false, true }; 79 runTest(String libFileName, int parentDomainOrdinal, int childDomainOrdinal, boolean everythingSdked)80 public static void runTest(String libFileName, int parentDomainOrdinal, 81 int childDomainOrdinal, boolean everythingSdked) throws Exception { 82 System.load(libFileName); 83 84 parentDomain = DexDomain.values()[parentDomainOrdinal]; 85 childDomain = DexDomain.values()[childDomainOrdinal]; 86 87 configMessage = "parentDomain=" + parentDomain.name() + ", childDomain=" + childDomain.name() 88 + ", everythingSdked=" + everythingSdked; 89 90 // Check expectations about loading into boot class path. 91 boolean isParentInBoot = (ParentClass.class.getClassLoader().getParent() == null); 92 boolean expectedParentInBoot = (parentDomain != DexDomain.Application); 93 if (isParentInBoot != expectedParentInBoot) { 94 throw new RuntimeException("Expected ParentClass " + 95 (expectedParentInBoot ? "" : "not ") + "in boot class path"); 96 } 97 boolean isChildInBoot = (ChildClass.class.getClassLoader().getParent() == null); 98 boolean expectedChildInBoot = (childDomain != DexDomain.Application); 99 if (isChildInBoot != expectedChildInBoot) { 100 throw new RuntimeException("Expected ChildClass " + (expectedChildInBoot ? "" : "not ") + 101 "in boot class path"); 102 } 103 ChildClass.everythingSdked = everythingSdked; 104 105 boolean isSameBoot = (isParentInBoot == isChildInBoot); 106 107 // For compat reasons, meta-reflection should still be usable by apps if hidden api check 108 // hardening is disabled (i.e. target SDK is Q or earlier). The only configuration where this 109 // workaround used to work is for ChildClass in the Application domain and ParentClass in the 110 // Platform domain, so only test that configuration with hidden api check hardening disabled. 111 boolean testHiddenApiCheckHardeningDisabled = 112 (childDomain == DexDomain.Application) && (parentDomain == DexDomain.Platform); 113 114 // Run meaningful combinations of access flags. 115 for (Hiddenness hiddenness : Hiddenness.values()) { 116 final Behaviour expected; 117 final boolean invokesMemberCallback; 118 // Warnings are now disabled whenever access is granted, even for 119 // greylisted APIs. This is the behaviour for release builds. 120 if (everythingSdked || hiddenness == Hiddenness.Sdk) { 121 expected = Behaviour.Granted; 122 invokesMemberCallback = false; 123 } else if (parentDomain == DexDomain.CorePlatform && childDomain == DexDomain.Platform) { 124 expected = (hiddenness == Hiddenness.Unsupported 125 || hiddenness == Hiddenness.BlocklistAndCorePlatformApi) 126 ? Behaviour.Granted 127 : Behaviour.Denied; 128 invokesMemberCallback = false; 129 } else if (isSameBoot) { 130 expected = Behaviour.Granted; 131 invokesMemberCallback = false; 132 } else if (hiddenness == Hiddenness.Blocklist || 133 hiddenness == Hiddenness.BlocklistAndCorePlatformApi) { 134 expected = Behaviour.Denied; 135 invokesMemberCallback = true; 136 } else { 137 expected = Behaviour.Warning; 138 invokesMemberCallback = true; 139 } 140 141 for (boolean isStatic : booleanValues) { 142 String suffix = (isStatic ? "Static" : "") + hiddenness.name(); 143 144 for (Visibility visibility : Visibility.values()) { 145 // Test reflection and JNI on methods and fields 146 for (Class klass : new Class<?>[] { ParentClass.class, ParentInterface.class }) { 147 String baseName = visibility.name() + suffix; 148 checkField(klass, "field" + baseName, isStatic, visibility, expected, 149 invokesMemberCallback, testHiddenApiCheckHardeningDisabled); 150 checkMethod(klass, "method" + baseName, isStatic, visibility, expected, 151 invokesMemberCallback, testHiddenApiCheckHardeningDisabled); 152 } 153 154 // Check whether one can use a class constructor. 155 checkConstructor(ParentClass.class, visibility, hiddenness, expected, 156 testHiddenApiCheckHardeningDisabled); 157 158 // Check whether one can use an interface default method. 159 String name = "method" + visibility.name() + "Default" + hiddenness.name(); 160 checkMethod(ParentInterface.class, name, /*isStatic*/ false, visibility, expected, 161 invokesMemberCallback, testHiddenApiCheckHardeningDisabled); 162 } 163 164 // Test whether static linking succeeds. 165 checkLinking("LinkFieldGet" + suffix, /*takesParameter*/ false, expected); 166 checkLinking("LinkFieldSet" + suffix, /*takesParameter*/ true, expected); 167 checkLinking("LinkMethod" + suffix, /*takesParameter*/ false, expected); 168 checkLinking("LinkMethodInterface" + suffix, /*takesParameter*/ false, expected); 169 } 170 171 // Check whether Class.newInstance succeeds. 172 checkNullaryConstructor(Class.forName("NullaryConstructor" + hiddenness.name()), expected); 173 } 174 } 175 176 static final class RecordingConsumer implements Consumer<String> { 177 public String recordedValue = null; 178 179 @Override accept(String value)180 public void accept(String value) { 181 recordedValue = value; 182 } 183 } 184 checkMemberCallback(Class<?> klass, String name, boolean isPublic, boolean isField, boolean expectedCallback)185 private static void checkMemberCallback(Class<?> klass, String name, 186 boolean isPublic, boolean isField, boolean expectedCallback) { 187 try { 188 RecordingConsumer consumer = new RecordingConsumer(); 189 VMRuntime.setNonSdkApiUsageConsumer(consumer); 190 try { 191 if (isPublic) { 192 if (isField) { 193 klass.getField(name); 194 } else { 195 klass.getMethod(name); 196 } 197 } else { 198 if (isField) { 199 klass.getDeclaredField(name); 200 } else { 201 klass.getDeclaredMethod(name); 202 } 203 } 204 } catch (NoSuchFieldException|NoSuchMethodException ignored) { 205 // We're not concerned whether an exception is thrown or not - we're 206 // only interested in whether the callback is invoked. 207 } 208 209 boolean actualCallback = consumer.recordedValue != null && 210 consumer.recordedValue.contains(name); 211 if (expectedCallback != actualCallback) { 212 if (expectedCallback) { 213 throw new RuntimeException("Expected callback for member: " + name); 214 } else { 215 throw new RuntimeException("Did not expect callback for member: " + name); 216 } 217 } 218 } finally { 219 VMRuntime.setNonSdkApiUsageConsumer(null); 220 } 221 } 222 checkField(Class<?> klass, String name, boolean isStatic, Visibility visibility, Behaviour behaviour, boolean invokesMemberCallback, boolean testHiddenApiCheckHardeningDisabled)223 private static void checkField(Class<?> klass, String name, boolean isStatic, 224 Visibility visibility, Behaviour behaviour, boolean invokesMemberCallback, 225 boolean testHiddenApiCheckHardeningDisabled) throws Exception { 226 227 boolean isPublic = (visibility == Visibility.Public); 228 boolean canDiscover = (behaviour != Behaviour.Denied); 229 230 if (klass.isInterface() && (!isStatic || !isPublic)) { 231 // Interfaces only have public static fields. 232 return; 233 } 234 235 // Test discovery with reflection. 236 237 if (Reflection.canDiscoverWithGetDeclaredField(klass, name) != canDiscover) { 238 throwDiscoveryException(klass, name, true, "getDeclaredField()", canDiscover); 239 } 240 241 if (Reflection.canDiscoverWithGetDeclaredFields(klass, name) != canDiscover) { 242 throwDiscoveryException(klass, name, true, "getDeclaredFields()", canDiscover); 243 } 244 245 if (Reflection.canDiscoverWithGetField(klass, name) != (canDiscover && isPublic)) { 246 throwDiscoveryException(klass, name, true, "getField()", (canDiscover && isPublic)); 247 } 248 249 if (Reflection.canDiscoverWithGetFields(klass, name) != (canDiscover && isPublic)) { 250 throwDiscoveryException(klass, name, true, "getFields()", (canDiscover && isPublic)); 251 } 252 253 // Test discovery with JNI. 254 255 if (JNI.canDiscoverField(klass, name, isStatic) != canDiscover) { 256 throwDiscoveryException(klass, name, true, "JNI", canDiscover); 257 } 258 259 // Test discovery with MethodHandles.lookup() which is caller 260 // context sensitive. 261 262 final MethodHandles.Lookup lookup = MethodHandles.lookup(); 263 if (JLI.canDiscoverWithLookupFindGetter(lookup, klass, name, int.class) 264 != canDiscover) { 265 throwDiscoveryException(klass, name, true, "MethodHandles.lookup().findGetter()", 266 canDiscover); 267 } 268 if (JLI.canDiscoverWithLookupFindStaticGetter(lookup, klass, name, int.class) 269 != canDiscover) { 270 throwDiscoveryException(klass, name, true, "MethodHandles.lookup().findStaticGetter()", 271 canDiscover); 272 } 273 274 // Test discovery with MethodHandles.publicLookup() which can only 275 // see public fields. Looking up setters here and fields in 276 // interfaces are implicitly final. 277 278 final MethodHandles.Lookup publicLookup = MethodHandles.publicLookup(); 279 if (JLI.canDiscoverWithLookupFindSetter(publicLookup, klass, name, int.class) 280 != canDiscover) { 281 throwDiscoveryException(klass, name, true, "MethodHandles.publicLookup().findSetter()", 282 canDiscover); 283 } 284 if (JLI.canDiscoverWithLookupFindStaticSetter(publicLookup, klass, name, int.class) 285 != canDiscover) { 286 throwDiscoveryException(klass, name, true, "MethodHandles.publicLookup().findStaticSetter()", 287 canDiscover); 288 } 289 290 // Check for meta reflection. 291 292 // With hidden api check hardening enabled, only white and light greylisted fields should be 293 // discoverable. 294 if (Reflection.canDiscoverFieldWithMetaReflection(klass, name, true) != canDiscover) { 295 throwDiscoveryException(klass, name, false, 296 "Meta reflection with hidden api hardening enabled", canDiscover); 297 } 298 299 if (testHiddenApiCheckHardeningDisabled) { 300 // With hidden api check hardening disabled, all fields should be discoverable. 301 if (Reflection.canDiscoverFieldWithMetaReflection(klass, name, false) != true) { 302 throwDiscoveryException(klass, name, false, 303 "Meta reflection with hidden api hardening enabled", canDiscover); 304 } 305 } 306 307 if (canDiscover) { 308 // Test that modifiers are unaffected. 309 310 if (Reflection.canObserveFieldHiddenAccessFlags(klass, name)) { 311 throwModifiersException(klass, name, true); 312 } 313 314 // Test getters and setters when meaningful. 315 316 if (!Reflection.canGetField(klass, name)) { 317 throwAccessException(klass, name, true, "Field.getInt()"); 318 } 319 if (!Reflection.canSetField(klass, name)) { 320 throwAccessException(klass, name, true, "Field.setInt()"); 321 } 322 if (!JNI.canGetField(klass, name, isStatic)) { 323 throwAccessException(klass, name, true, "getIntField"); 324 } 325 if (!JNI.canSetField(klass, name, isStatic)) { 326 throwAccessException(klass, name, true, "setIntField"); 327 } 328 } 329 330 // Test that callbacks are invoked correctly. 331 checkMemberCallback(klass, name, isPublic, true /* isField */, invokesMemberCallback); 332 } 333 checkMethod(Class<?> klass, String name, boolean isStatic, Visibility visibility, Behaviour behaviour, boolean invokesMemberCallback, boolean testHiddenApiCheckHardeningDisabled)334 private static void checkMethod(Class<?> klass, String name, boolean isStatic, 335 Visibility visibility, Behaviour behaviour, boolean invokesMemberCallback, 336 boolean testHiddenApiCheckHardeningDisabled) throws Exception { 337 338 boolean isPublic = (visibility == Visibility.Public); 339 if (klass.isInterface() && !isPublic) { 340 // All interface members are public. 341 return; 342 } 343 344 boolean canDiscover = (behaviour != Behaviour.Denied); 345 346 // Test discovery with reflection. 347 348 if (Reflection.canDiscoverWithGetDeclaredMethod(klass, name) != canDiscover) { 349 throwDiscoveryException(klass, name, false, "getDeclaredMethod()", canDiscover); 350 } 351 352 if (Reflection.canDiscoverWithGetDeclaredMethods(klass, name) != canDiscover) { 353 throwDiscoveryException(klass, name, false, "getDeclaredMethods()", canDiscover); 354 } 355 356 if (Reflection.canDiscoverWithGetMethod(klass, name) != (canDiscover && isPublic)) { 357 throwDiscoveryException(klass, name, false, "getMethod()", (canDiscover && isPublic)); 358 } 359 360 if (Reflection.canDiscoverWithGetMethods(klass, name) != (canDiscover && isPublic)) { 361 throwDiscoveryException(klass, name, false, "getMethods()", (canDiscover && isPublic)); 362 } 363 364 // Test discovery with JNI. 365 366 if (JNI.canDiscoverMethod(klass, name, isStatic) != canDiscover) { 367 throwDiscoveryException(klass, name, false, "JNI", canDiscover); 368 } 369 370 // Test discovery with MethodHandles.lookup(). 371 372 final MethodHandles.Lookup lookup = MethodHandles.lookup(); 373 final MethodType methodType = MethodType.methodType(int.class); 374 if (JLI.canDiscoverWithLookupFindVirtual(lookup, klass, name, methodType) != canDiscover) { 375 throwDiscoveryException(klass, name, false, "MethodHandles.lookup().findVirtual()", 376 canDiscover); 377 } 378 379 if (JLI.canDiscoverWithLookupFindStatic(lookup, klass, name, methodType) != canDiscover) { 380 throwDiscoveryException(klass, name, false, "MethodHandles.lookup().findStatic()", 381 canDiscover); 382 } 383 384 // Check for meta reflection. 385 386 // With hidden api check hardening enabled, only white and light greylisted methods should be 387 // discoverable. 388 if (Reflection.canDiscoverMethodWithMetaReflection(klass, name, true) != canDiscover) { 389 throwDiscoveryException(klass, name, false, 390 "Meta reflection with hidden api hardening enabled", canDiscover); 391 } 392 393 if (testHiddenApiCheckHardeningDisabled) { 394 // With hidden api check hardening disabled, all methods should be discoverable. 395 if (Reflection.canDiscoverMethodWithMetaReflection(klass, name, false) != true) { 396 throwDiscoveryException(klass, name, false, 397 "Meta reflection with hidden api hardening enabled", canDiscover); 398 } 399 } 400 401 // Finish here if we could not discover the method. 402 403 if (canDiscover) { 404 // Test that modifiers are unaffected. 405 406 if (Reflection.canObserveMethodHiddenAccessFlags(klass, name)) { 407 throwModifiersException(klass, name, false); 408 } 409 410 // Test whether we can invoke the method. This skips non-static interface methods. 411 if (!klass.isInterface() || isStatic) { 412 if (!Reflection.canInvokeMethod(klass, name)) { 413 throwAccessException(klass, name, false, "invoke()"); 414 } 415 if (!JNI.canInvokeMethodA(klass, name, isStatic)) { 416 throwAccessException(klass, name, false, "CallMethodA"); 417 } 418 if (!JNI.canInvokeMethodV(klass, name, isStatic)) { 419 throwAccessException(klass, name, false, "CallMethodV"); 420 } 421 } 422 } 423 424 // Test that callbacks are invoked correctly. 425 checkMemberCallback(klass, name, isPublic, false /* isField */, invokesMemberCallback); 426 } 427 checkConstructor(Class<?> klass, Visibility visibility, Hiddenness hiddenness, Behaviour behaviour, boolean testHiddenApiCheckHardeningDisabled)428 private static void checkConstructor(Class<?> klass, Visibility visibility, Hiddenness hiddenness, 429 Behaviour behaviour, boolean testHiddenApiCheckHardeningDisabled) throws Exception { 430 431 boolean isPublic = (visibility == Visibility.Public); 432 String signature = "(" + visibility.mAssociatedType.mShorty + 433 hiddenness.mAssociatedType.mShorty + ")V"; 434 String fullName = "<init>" + signature; 435 Class<?> args[] = new Class[] { visibility.mAssociatedType.mClass, 436 hiddenness.mAssociatedType.mClass }; 437 Object initargs[] = new Object[] { visibility.mAssociatedType.mDefaultValue, 438 hiddenness.mAssociatedType.mDefaultValue }; 439 MethodType methodType = MethodType.methodType(void.class, args); 440 441 boolean canDiscover = (behaviour != Behaviour.Denied); 442 443 // Test discovery with reflection. 444 445 if (Reflection.canDiscoverWithGetDeclaredConstructor(klass, args) != canDiscover) { 446 throwDiscoveryException(klass, fullName, false, "getDeclaredConstructor()", canDiscover); 447 } 448 449 if (Reflection.canDiscoverWithGetDeclaredConstructors(klass, args) != canDiscover) { 450 throwDiscoveryException(klass, fullName, false, "getDeclaredConstructors()", canDiscover); 451 } 452 453 if (Reflection.canDiscoverWithGetConstructor(klass, args) != (canDiscover && isPublic)) { 454 throwDiscoveryException( 455 klass, fullName, false, "getConstructor()", (canDiscover && isPublic)); 456 } 457 458 if (Reflection.canDiscoverWithGetConstructors(klass, args) != (canDiscover && isPublic)) { 459 throwDiscoveryException( 460 klass, fullName, false, "getConstructors()", (canDiscover && isPublic)); 461 } 462 463 // Test discovery with JNI. 464 465 if (JNI.canDiscoverConstructor(klass, signature) != canDiscover) { 466 throwDiscoveryException(klass, fullName, false, "JNI", canDiscover); 467 } 468 469 // Test discovery with MethodHandles.lookup() 470 471 final MethodHandles.Lookup lookup = MethodHandles.lookup(); 472 if (JLI.canDiscoverWithLookupFindConstructor(lookup, klass, methodType) != canDiscover) { 473 throwDiscoveryException(klass, fullName, false, "MethodHandles.lookup().findConstructor", 474 canDiscover); 475 } 476 477 final MethodHandles.Lookup publicLookup = MethodHandles.publicLookup(); 478 if (JLI.canDiscoverWithLookupFindConstructor(publicLookup, klass, methodType) != canDiscover) { 479 throwDiscoveryException(klass, fullName, false, 480 "MethodHandles.publicLookup().findConstructor", 481 canDiscover); 482 } 483 484 // Check for meta reflection. 485 486 // With hidden api check hardening enabled, only white and light greylisted constructors should 487 // be discoverable. 488 if (Reflection.canDiscoverConstructorWithMetaReflection(klass, args, true) != canDiscover) { 489 throwDiscoveryException(klass, fullName, false, 490 "Meta reflection with hidden api hardening enabled", canDiscover); 491 } 492 493 if (testHiddenApiCheckHardeningDisabled) { 494 // With hidden api check hardening disabled, all constructors should be discoverable. 495 if (Reflection.canDiscoverConstructorWithMetaReflection(klass, args, false) != true) { 496 throwDiscoveryException(klass, fullName, false, 497 "Meta reflection with hidden api hardening enabled", canDiscover); 498 } 499 } 500 501 if (canDiscover) { 502 // Test whether we can invoke the constructor. 503 504 if (!Reflection.canInvokeConstructor(klass, args, initargs)) { 505 throwAccessException(klass, fullName, false, "invoke()"); 506 } 507 if (!JNI.canInvokeConstructorA(klass, signature)) { 508 throwAccessException(klass, fullName, false, "NewObjectA"); 509 } 510 if (!JNI.canInvokeConstructorV(klass, signature)) { 511 throwAccessException(klass, fullName, false, "NewObjectV"); 512 } 513 } 514 } 515 checkNullaryConstructor(Class<?> klass, Behaviour behaviour)516 private static void checkNullaryConstructor(Class<?> klass, Behaviour behaviour) 517 throws Exception { 518 boolean canAccess = (behaviour != Behaviour.Denied); 519 520 if (Reflection.canUseNewInstance(klass) != canAccess) { 521 throw new RuntimeException("Expected to " + (canAccess ? "" : "not ") + 522 "be able to construct " + klass.getName() + ". " + configMessage); 523 } 524 } 525 checkLinking(String className, boolean takesParameter, Behaviour behaviour)526 private static void checkLinking(String className, boolean takesParameter, Behaviour behaviour) 527 throws Exception { 528 boolean canAccess = (behaviour != Behaviour.Denied); 529 530 if (Linking.canAccess(className, takesParameter) != canAccess) { 531 throw new RuntimeException("Expected to " + (canAccess ? "" : "not ") + 532 "be able to verify " + className + "." + configMessage); 533 } 534 } 535 throwDiscoveryException(Class<?> klass, String name, boolean isField, String fn, boolean canAccess)536 private static void throwDiscoveryException(Class<?> klass, String name, boolean isField, 537 String fn, boolean canAccess) { 538 throw new RuntimeException("Expected " + (isField ? "field " : "method ") + klass.getName() + 539 "." + name + " to " + (canAccess ? "" : "not ") + "be discoverable with " + fn + ". " + 540 configMessage); 541 } 542 throwAccessException(Class<?> klass, String name, boolean isField, String fn)543 private static void throwAccessException(Class<?> klass, String name, boolean isField, 544 String fn) { 545 throw new RuntimeException("Expected to be able to access " + (isField ? "field " : "method ") + 546 klass.getName() + "." + name + " using " + fn + ". " + configMessage); 547 } 548 throwModifiersException(Class<?> klass, String name, boolean isField)549 private static void throwModifiersException(Class<?> klass, String name, boolean isField) { 550 throw new RuntimeException("Expected " + (isField ? "field " : "method ") + klass.getName() + 551 "." + name + " to not expose hidden modifiers"); 552 } 553 554 private static DexDomain parentDomain; 555 private static DexDomain childDomain; 556 private static boolean everythingSdked; 557 558 private static String configMessage; 559 } 560