1 /*
2 * Copyright (C) 2016 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 #include "common_helper.h"
18
19 #include <cstdio>
20 #include <deque>
21 #include <map>
22 #include <sstream>
23 #include <string>
24 #include <vector>
25
26 #include "jni.h"
27 #include "jvmti.h"
28
29 #include "jvmti_helper.h"
30 #include "test_env.h"
31
32 namespace art {
33
34 enum class RedefineType {
35 kNormal,
36 kStructural,
37 };
38
39 static void SetupCommonRedefine();
40 static void SetupCommonRetransform(RedefineType type);
41 static void SetupCommonTransform();
42 template <bool is_redefine>
throwCommonRedefinitionError(jvmtiEnv * jvmti,JNIEnv * env,jint num_targets,jclass * target,jvmtiError res)43 static void throwCommonRedefinitionError(jvmtiEnv* jvmti,
44 JNIEnv* env,
45 jint num_targets,
46 jclass* target,
47 jvmtiError res) {
48 std::stringstream err;
49 char* error = nullptr;
50 jvmti->GetErrorName(res, &error);
51 err << "Failed to " << (is_redefine ? "redefine" : "retransform") << " class";
52 if (num_targets > 1) {
53 err << "es";
54 }
55 err << " <";
56 for (jint i = 0; i < num_targets; i++) {
57 char* signature = nullptr;
58 char* generic = nullptr;
59 jvmti->GetClassSignature(target[i], &signature, &generic);
60 if (i != 0) {
61 err << ", ";
62 }
63 err << signature;
64 jvmti->Deallocate(reinterpret_cast<unsigned char*>(signature));
65 jvmti->Deallocate(reinterpret_cast<unsigned char*>(generic));
66 }
67 err << "> due to " << error;
68 std::string message = err.str();
69 jvmti->Deallocate(reinterpret_cast<unsigned char*>(error));
70 env->ThrowNew(env->FindClass("java/lang/Exception"), message.c_str());
71 }
72
73 #define CONFIGURATION_COMMON_REDEFINE 0
74 #define CONFIGURATION_COMMON_RETRANSFORM 1
75 #define CONFIGURATION_COMMON_TRANSFORM 2
76 #define CONFIGURATION_STRUCTURAL_TRANSFORM 3
77
Java_art_Redefinition_nativeSetTestConfiguration(JNIEnv *,jclass,jint type)78 extern "C" JNIEXPORT void JNICALL Java_art_Redefinition_nativeSetTestConfiguration(JNIEnv*,
79 jclass,
80 jint type) {
81 switch (type) {
82 case CONFIGURATION_COMMON_REDEFINE: {
83 SetupCommonRedefine();
84 return;
85 }
86 case CONFIGURATION_COMMON_RETRANSFORM: {
87 SetupCommonRetransform(RedefineType::kNormal);
88 return;
89 }
90 case CONFIGURATION_COMMON_TRANSFORM: {
91 SetupCommonTransform();
92 return;
93 }
94 case CONFIGURATION_STRUCTURAL_TRANSFORM: {
95 SetupCommonRetransform(RedefineType::kStructural);
96 return;
97 }
98 default: {
99 LOG(FATAL) << "Unknown test configuration: " << type;
100 }
101 }
102 }
103
104 template<RedefineType kType>
SupportsAndIsJVM()105 static bool SupportsAndIsJVM() {
106 if constexpr (kType == RedefineType::kStructural) {
107 return false;
108 } else {
109 return IsJVM();
110 }
111 }
112
113
114 namespace common_redefine {
115
116 template <RedefineType kType>
CallRedefineEntrypoint(JNIEnv * env,jvmtiEnv * jvmti,jint num_defs,const jvmtiClassDefinition * defs)117 static jvmtiError CallRedefineEntrypoint(JNIEnv* env,
118 jvmtiEnv* jvmti,
119 jint num_defs,
120 const jvmtiClassDefinition* defs) {
121 decltype(jvmti->functions->RedefineClasses) entrypoint = nullptr;
122 if constexpr (kType == RedefineType::kNormal) {
123 entrypoint = jvmti->functions->RedefineClasses;
124 } else {
125 entrypoint = GetExtensionFunction<decltype(entrypoint)>(
126 env, jvmti_env, "com.android.art.class.structurally_redefine_classes");
127 }
128 if (entrypoint == nullptr) {
129 LOG(INFO) << "Could not find entrypoint!";
130 return JVMTI_ERROR_NOT_AVAILABLE;
131 }
132 return entrypoint(jvmti, num_defs, defs);
133 }
134
throwRedefinitionError(jvmtiEnv * jvmti,JNIEnv * env,jint num_targets,jclass * target,jvmtiError res)135 static void throwRedefinitionError(jvmtiEnv* jvmti,
136 JNIEnv* env,
137 jint num_targets,
138 jclass* target,
139 jvmtiError res) {
140 return throwCommonRedefinitionError<true>(jvmti, env, num_targets, target, res);
141 }
142
143 template<RedefineType kType>
DoMultiClassRedefine(jvmtiEnv * jvmti_env,JNIEnv * env,jint num_redefines,jclass * targets,jbyteArray * class_file_bytes,jbyteArray * dex_file_bytes)144 static void DoMultiClassRedefine(jvmtiEnv* jvmti_env,
145 JNIEnv* env,
146 jint num_redefines,
147 jclass* targets,
148 jbyteArray* class_file_bytes,
149 jbyteArray* dex_file_bytes) {
150 std::vector<jvmtiClassDefinition> defs;
151 for (jint i = 0; i < num_redefines; i++) {
152 jbyteArray desired_array = SupportsAndIsJVM<kType>() ? class_file_bytes[i] : dex_file_bytes[i];
153 jint len = static_cast<jint>(env->GetArrayLength(desired_array));
154 const unsigned char* redef_bytes = reinterpret_cast<const unsigned char*>(
155 env->GetByteArrayElements(desired_array, nullptr));
156 defs.push_back({targets[i], static_cast<jint>(len), redef_bytes});
157 }
158 jvmtiError res = CallRedefineEntrypoint<kType>(env, jvmti_env, num_redefines, defs.data());
159 if (res != JVMTI_ERROR_NONE) {
160 throwRedefinitionError(jvmti_env, env, num_redefines, targets, res);
161 }
162 }
163
164 template<RedefineType kType>
DoClassRedefine(jvmtiEnv * jvmti_env,JNIEnv * env,jclass target,jbyteArray class_file_bytes,jbyteArray dex_file_bytes)165 static void DoClassRedefine(jvmtiEnv* jvmti_env,
166 JNIEnv* env,
167 jclass target,
168 jbyteArray class_file_bytes,
169 jbyteArray dex_file_bytes) {
170 return DoMultiClassRedefine<kType>(jvmti_env, env, 1, &target, &class_file_bytes, &dex_file_bytes);
171 }
172
173 extern "C" JNIEXPORT jboolean JNICALL
Java_art_Redefinition_isStructurallyModifiable(JNIEnv * env,jclass,jclass target)174 Java_art_Redefinition_isStructurallyModifiable(JNIEnv* env, jclass, jclass target) {
175 using ArtCanStructurallyRedefineClass =
176 jvmtiError (*)(jvmtiEnv * env, jclass k, jboolean * result);
177 ArtCanStructurallyRedefineClass can_redef = GetExtensionFunction<ArtCanStructurallyRedefineClass>(
178 env, jvmti_env, "com.android.art.class.is_structurally_modifiable_class");
179 if (can_redef == nullptr || env->ExceptionCheck()) {
180 return false;
181 }
182 jboolean result = false;
183 JvmtiErrorToException(env, jvmti_env, can_redef(jvmti_env, target, &result));
184 return result;
185 }
186
Java_art_Redefinition_doCommonStructuralClassRedefinition(JNIEnv * env,jclass,jclass target,jbyteArray dex_file_bytes)187 extern "C" JNIEXPORT void JNICALL Java_art_Redefinition_doCommonStructuralClassRedefinition(
188 JNIEnv* env, jclass, jclass target, jbyteArray dex_file_bytes) {
189 DoClassRedefine<RedefineType::kStructural>(jvmti_env, env, target, nullptr, dex_file_bytes);
190 }
191
192 // Magic JNI export that classes can use for redefining classes.
193 // To use classes should declare this as a native function with signature (Ljava/lang/Class;[B[B)V
Java_art_Redefinition_doCommonClassRedefinition(JNIEnv * env,jclass,jclass target,jbyteArray class_file_bytes,jbyteArray dex_file_bytes)194 extern "C" JNIEXPORT void JNICALL Java_art_Redefinition_doCommonClassRedefinition(
195 JNIEnv* env, jclass, jclass target, jbyteArray class_file_bytes, jbyteArray dex_file_bytes) {
196 DoClassRedefine<RedefineType::kNormal>(jvmti_env, env, target, class_file_bytes, dex_file_bytes);
197 }
198
199 // Magic JNI export that classes can use for redefining classes.
200 // To use classes should declare this as a native function with signature
201 // ([Ljava/lang/Class;[[B[[B)V
Java_art_Redefinition_doCommonMultiStructuralClassRedefinition(JNIEnv * env,jclass,jobjectArray targets,jobjectArray dex_file_bytes)202 extern "C" JNIEXPORT void JNICALL Java_art_Redefinition_doCommonMultiStructuralClassRedefinition(
203 JNIEnv* env,
204 jclass,
205 jobjectArray targets,
206 jobjectArray dex_file_bytes) {
207 std::vector<jclass> classes;
208 std::vector<jbyteArray> class_files;
209 std::vector<jbyteArray> dex_files;
210 jint len = env->GetArrayLength(targets);
211 if (len != env->GetArrayLength(dex_file_bytes)) {
212 env->ThrowNew(env->FindClass("java/lang/IllegalArgumentException"),
213 "the three array arguments passed to this function have different lengths!");
214 return;
215 }
216 for (jint i = 0; i < len; i++) {
217 classes.push_back(static_cast<jclass>(env->GetObjectArrayElement(targets, i)));
218 dex_files.push_back(static_cast<jbyteArray>(env->GetObjectArrayElement(dex_file_bytes, i)));
219 class_files.push_back(nullptr);
220 }
221 return DoMultiClassRedefine<RedefineType::kStructural>(jvmti_env,
222 env,
223 len,
224 classes.data(),
225 class_files.data(),
226 dex_files.data());
227 }
228
229 // Magic JNI export that classes can use for redefining classes.
230 // To use classes should declare this as a native function with signature
231 // ([Ljava/lang/Class;[[B[[B)V
Java_art_Redefinition_doCommonMultiClassRedefinition(JNIEnv * env,jclass,jobjectArray targets,jobjectArray class_file_bytes,jobjectArray dex_file_bytes)232 extern "C" JNIEXPORT void JNICALL Java_art_Redefinition_doCommonMultiClassRedefinition(
233 JNIEnv* env,
234 jclass,
235 jobjectArray targets,
236 jobjectArray class_file_bytes,
237 jobjectArray dex_file_bytes) {
238 std::vector<jclass> classes;
239 std::vector<jbyteArray> class_files;
240 std::vector<jbyteArray> dex_files;
241 jint len = env->GetArrayLength(targets);
242 if (len != env->GetArrayLength(class_file_bytes) || len != env->GetArrayLength(dex_file_bytes)) {
243 env->ThrowNew(env->FindClass("java/lang/IllegalArgumentException"),
244 "the three array arguments passed to this function have different lengths!");
245 return;
246 }
247 for (jint i = 0; i < len; i++) {
248 classes.push_back(static_cast<jclass>(env->GetObjectArrayElement(targets, i)));
249 dex_files.push_back(static_cast<jbyteArray>(env->GetObjectArrayElement(dex_file_bytes, i)));
250 class_files.push_back(static_cast<jbyteArray>(env->GetObjectArrayElement(class_file_bytes, i)));
251 }
252 return DoMultiClassRedefine<RedefineType::kNormal>(jvmti_env,
253 env,
254 len,
255 classes.data(),
256 class_files.data(),
257 dex_files.data());
258 }
259
260 // Get all capabilities except those related to retransformation.
OnLoad(JavaVM * vm,char * options ATTRIBUTE_UNUSED,void * reserved ATTRIBUTE_UNUSED)261 jint OnLoad(JavaVM* vm,
262 char* options ATTRIBUTE_UNUSED,
263 void* reserved ATTRIBUTE_UNUSED) {
264 if (vm->GetEnv(reinterpret_cast<void**>(&jvmti_env), JVMTI_VERSION_1_0)) {
265 printf("Unable to get jvmti env!\n");
266 return 1;
267 }
268 SetupCommonRedefine();
269 return 0;
270 }
271
272 } // namespace common_redefine
273
274 namespace common_retransform {
275
276 struct CommonTransformationResult {
277 std::vector<unsigned char> class_bytes;
278 std::vector<unsigned char> dex_bytes;
279
CommonTransformationResultart::common_retransform::CommonTransformationResult280 CommonTransformationResult(size_t class_size, size_t dex_size)
281 : class_bytes(class_size), dex_bytes(dex_size) {}
282
283 CommonTransformationResult() = default;
284 CommonTransformationResult(CommonTransformationResult&&) = default;
285 CommonTransformationResult(CommonTransformationResult&) = default;
286 };
287
288 // Map from class name to transformation result.
289 std::map<std::string, std::deque<CommonTransformationResult>> gTransformations;
290 bool gPopTransformations = true;
291
Java_art_Redefinition_addCommonTransformationResult(JNIEnv * env,jclass,jstring class_name,jbyteArray class_array,jbyteArray dex_array)292 extern "C" JNIEXPORT void JNICALL Java_art_Redefinition_addCommonTransformationResult(
293 JNIEnv* env, jclass, jstring class_name, jbyteArray class_array, jbyteArray dex_array) {
294 const char* name_chrs = env->GetStringUTFChars(class_name, nullptr);
295 std::string name_str(name_chrs);
296 env->ReleaseStringUTFChars(class_name, name_chrs);
297 CommonTransformationResult trans(env->GetArrayLength(class_array),
298 env->GetArrayLength(dex_array));
299 if (env->ExceptionOccurred()) {
300 return;
301 }
302 env->GetByteArrayRegion(class_array,
303 0,
304 env->GetArrayLength(class_array),
305 reinterpret_cast<jbyte*>(trans.class_bytes.data()));
306 if (env->ExceptionOccurred()) {
307 return;
308 }
309 env->GetByteArrayRegion(dex_array,
310 0,
311 env->GetArrayLength(dex_array),
312 reinterpret_cast<jbyte*>(trans.dex_bytes.data()));
313 if (env->ExceptionOccurred()) {
314 return;
315 }
316 if (gTransformations.find(name_str) == gTransformations.end()) {
317 std::deque<CommonTransformationResult> list;
318 gTransformations[name_str] = std::move(list);
319 }
320 gTransformations[name_str].push_back(std::move(trans));
321 }
322
323 // The hook we are using.
CommonClassFileLoadHookRetransformable(jvmtiEnv * jvmti_env,JNIEnv * jni_env ATTRIBUTE_UNUSED,jclass class_being_redefined ATTRIBUTE_UNUSED,jobject loader ATTRIBUTE_UNUSED,const char * name,jobject protection_domain ATTRIBUTE_UNUSED,jint class_data_len ATTRIBUTE_UNUSED,const unsigned char * class_dat ATTRIBUTE_UNUSED,jint * new_class_data_len,unsigned char ** new_class_data)324 void JNICALL CommonClassFileLoadHookRetransformable(jvmtiEnv* jvmti_env,
325 JNIEnv* jni_env ATTRIBUTE_UNUSED,
326 jclass class_being_redefined ATTRIBUTE_UNUSED,
327 jobject loader ATTRIBUTE_UNUSED,
328 const char* name,
329 jobject protection_domain ATTRIBUTE_UNUSED,
330 jint class_data_len ATTRIBUTE_UNUSED,
331 const unsigned char* class_dat ATTRIBUTE_UNUSED,
332 jint* new_class_data_len,
333 unsigned char** new_class_data) {
334 std::string name_str(name);
335 if (gTransformations.find(name_str) != gTransformations.end() &&
336 gTransformations[name_str].size() > 0) {
337 CommonTransformationResult& res = gTransformations[name_str][0];
338 const std::vector<unsigned char>& desired_array = IsJVM() ? res.class_bytes : res.dex_bytes;
339 unsigned char* new_data;
340 CHECK_EQ(JVMTI_ERROR_NONE, jvmti_env->Allocate(desired_array.size(), &new_data));
341 memcpy(new_data, desired_array.data(), desired_array.size());
342 *new_class_data = new_data;
343 *new_class_data_len = desired_array.size();
344 if (gPopTransformations) {
345 gTransformations[name_str].pop_front();
346 }
347 }
348 }
349
Java_art_Redefinition_setPopRetransformations(JNIEnv *,jclass,jboolean enable)350 extern "C" JNIEXPORT void Java_art_Redefinition_setPopRetransformations(JNIEnv*,
351 jclass,
352 jboolean enable) {
353 gPopTransformations = enable;
354 }
355
Java_art_Redefinition_popTransformationFor(JNIEnv * env,jclass,jstring class_name)356 extern "C" JNIEXPORT void Java_art_Redefinition_popTransformationFor(JNIEnv* env,
357 jclass,
358 jstring class_name) {
359 const char* name_chrs = env->GetStringUTFChars(class_name, nullptr);
360 std::string name_str(name_chrs);
361 env->ReleaseStringUTFChars(class_name, name_chrs);
362 if (gTransformations.find(name_str) != gTransformations.end() &&
363 gTransformations[name_str].size() > 0) {
364 gTransformations[name_str].pop_front();
365 } else {
366 std::stringstream err;
367 err << "No transformations found for class " << name_str;
368 std::string message = err.str();
369 env->ThrowNew(env->FindClass("java/lang/Exception"), message.c_str());
370 }
371 }
372
Java_art_Redefinition_enableCommonRetransformation(JNIEnv * env,jclass,jboolean enable)373 extern "C" JNIEXPORT void Java_art_Redefinition_enableCommonRetransformation(JNIEnv* env,
374 jclass,
375 jboolean enable) {
376 jvmtiError res = jvmti_env->SetEventNotificationMode(enable ? JVMTI_ENABLE : JVMTI_DISABLE,
377 JVMTI_EVENT_CLASS_FILE_LOAD_HOOK,
378 nullptr);
379 if (res != JVMTI_ERROR_NONE) {
380 JvmtiErrorToException(env, jvmti_env, res);
381 }
382 }
383
throwRetransformationError(jvmtiEnv * jvmti,JNIEnv * env,jint num_targets,jclass * targets,jvmtiError res)384 static void throwRetransformationError(jvmtiEnv* jvmti,
385 JNIEnv* env,
386 jint num_targets,
387 jclass* targets,
388 jvmtiError res) {
389 return throwCommonRedefinitionError<false>(jvmti, env, num_targets, targets, res);
390 }
391
DoClassRetransformation(jvmtiEnv * jvmti_env,JNIEnv * env,jobjectArray targets)392 static void DoClassRetransformation(jvmtiEnv* jvmti_env, JNIEnv* env, jobjectArray targets) {
393 std::vector<jclass> classes;
394 jint len = env->GetArrayLength(targets);
395 for (jint i = 0; i < len; i++) {
396 classes.push_back(static_cast<jclass>(env->GetObjectArrayElement(targets, i)));
397 }
398 jvmtiError res = jvmti_env->RetransformClasses(len, classes.data());
399 if (res != JVMTI_ERROR_NONE) {
400 throwRetransformationError(jvmti_env, env, len, classes.data(), res);
401 }
402 }
403
Java_art_Redefinition_doCommonClassRetransformation(JNIEnv * env,jclass,jobjectArray targets)404 extern "C" JNIEXPORT void JNICALL Java_art_Redefinition_doCommonClassRetransformation(
405 JNIEnv* env, jclass, jobjectArray targets) {
406 jvmtiCapabilities caps;
407 jvmtiError caps_err = jvmti_env->GetCapabilities(&caps);
408 if (caps_err != JVMTI_ERROR_NONE) {
409 env->ThrowNew(env->FindClass("java/lang/Exception"),
410 "Unable to get current jvmtiEnv capabilities");
411 return;
412 }
413
414 // Allocate a new environment if we don't have the can_retransform_classes capability needed to
415 // call the RetransformClasses function.
416 jvmtiEnv* real_env = nullptr;
417 if (caps.can_retransform_classes != 1) {
418 JavaVM* vm = nullptr;
419 if (env->GetJavaVM(&vm) != 0 ||
420 vm->GetEnv(reinterpret_cast<void**>(&real_env), JVMTI_VERSION_1_0) != 0) {
421 env->ThrowNew(env->FindClass("java/lang/Exception"),
422 "Unable to create temporary jvmtiEnv for RetransformClasses call.");
423 return;
424 }
425 SetStandardCapabilities(real_env);
426 } else {
427 real_env = jvmti_env;
428 }
429 DoClassRetransformation(real_env, env, targets);
430 if (caps.can_retransform_classes != 1) {
431 real_env->DisposeEnvironment();
432 }
433 }
434
435 // Get all capabilities except those related to retransformation.
OnLoad(JavaVM * vm,char * options ATTRIBUTE_UNUSED,void * reserved ATTRIBUTE_UNUSED)436 jint OnLoad(JavaVM* vm,
437 char* options ATTRIBUTE_UNUSED,
438 void* reserved ATTRIBUTE_UNUSED) {
439 if (vm->GetEnv(reinterpret_cast<void**>(&jvmti_env), JVMTI_VERSION_1_0)) {
440 printf("Unable to get jvmti env!\n");
441 return 1;
442 }
443 SetupCommonRetransform(RedefineType::kNormal);
444 return 0;
445 }
446
447 } // namespace common_retransform
448
449 namespace common_transform {
450
451 // Get all capabilities except those related to retransformation.
OnLoad(JavaVM * vm,char * options ATTRIBUTE_UNUSED,void * reserved ATTRIBUTE_UNUSED)452 jint OnLoad(JavaVM* vm,
453 char* options ATTRIBUTE_UNUSED,
454 void* reserved ATTRIBUTE_UNUSED) {
455 if (vm->GetEnv(reinterpret_cast<void**>(&jvmti_env), JVMTI_VERSION_1_0)) {
456 printf("Unable to get jvmti env!\n");
457 return 1;
458 }
459 SetupCommonTransform();
460 return 0;
461 }
462
463 } // namespace common_transform
464
SetupCommonRedefine()465 static void SetupCommonRedefine() {
466 jvmtiCapabilities caps = GetStandardCapabilities();
467 caps.can_retransform_classes = 0;
468 caps.can_retransform_any_class = 0;
469 jvmti_env->AddCapabilities(&caps);
470 }
471
SetupCommonRetransform(RedefineType type)472 static void SetupCommonRetransform(RedefineType type) {
473 SetStandardCapabilities(jvmti_env);
474 if (type == RedefineType::kNormal) {
475 current_callbacks.ClassFileLoadHook =
476 common_retransform::CommonClassFileLoadHookRetransformable;
477 jvmtiError res = jvmti_env->SetEventCallbacks(¤t_callbacks, sizeof(current_callbacks));
478 CHECK_EQ(res, JVMTI_ERROR_NONE);
479 } else {
480 jvmtiError res = jvmti_env->SetExtensionEventCallback(
481 GetExtensionEventId(jvmti_env, "com.android.art.class.structural_dex_file_load_hook"),
482 reinterpret_cast<jvmtiExtensionEvent>(
483 common_retransform::CommonClassFileLoadHookRetransformable));
484 CHECK_EQ(res, JVMTI_ERROR_NONE);
485 }
486 common_retransform::gTransformations.clear();
487 }
488
SetupCommonTransform()489 static void SetupCommonTransform() {
490 // Don't set the retransform caps
491 jvmtiCapabilities caps = GetStandardCapabilities();
492 caps.can_retransform_classes = 0;
493 caps.can_retransform_any_class = 0;
494 jvmti_env->AddCapabilities(&caps);
495
496 // Use the same callback as the retransform test.
497 current_callbacks.ClassFileLoadHook = common_retransform::CommonClassFileLoadHookRetransformable;
498 jvmtiError res = jvmti_env->SetEventCallbacks(¤t_callbacks, sizeof(current_callbacks));
499 CHECK_EQ(res, JVMTI_ERROR_NONE);
500 common_retransform::gTransformations.clear();
501 }
502
503 } // namespace art
504