Move dex path and CLC validation to JNI.

Bug: 298183834
Bug: 296034438
Test: atest CtsCompilationTestCases
Test: atest ArtServiceTests
Test: atest art_standalone_artd_tests
Test: atest art_standalone_libartservice_tests
Change-Id: I7ed1a36392ea8ca6f583e4743baef043c9927ee5
Merged-In: I7ed1a36392ea8ca6f583e4743baef043c9927ee5
diff --git a/artd/Android.bp b/artd/Android.bp
index cacbe28..6de6aef 100644
--- a/artd/Android.bp
+++ b/artd/Android.bp
@@ -35,6 +35,7 @@
         "profman_headers",
     ],
     shared_libs: [
+        "libartservice",
         "libarttools", // Contains "libc++fs".
         "libbase",
         "libbinder_ndk",
diff --git a/artd/artd.cc b/artd/artd.cc
index 09168a2..8b46d91 100644
--- a/artd/artd.cc
+++ b/artd/artd.cc
@@ -73,6 +73,7 @@
 #include "path_utils.h"
 #include "profman/profman_result.h"
 #include "selinux/android.h"
+#include "service.h"
 #include "tools/cmdline_builder.h"
 #include "tools/tools.h"
 
@@ -109,6 +110,7 @@
 using ::android::base::StringReplace;
 using ::android::base::WriteStringToFd;
 using ::android::fs_mgr::FstabEntry;
+using ::art::service::ValidateDexPath;
 using ::art::tools::CmdlineBuilder;
 using ::ndk::ScopedAStatus;
 
@@ -1177,44 +1179,6 @@
   return ScopedAStatus::ok();
 }
 
-ScopedAStatus Artd::validateDexPath(const std::string& in_dexPath,
-                                    std::optional<std::string>* _aidl_return) {
-  if (Result<void> result = ValidateDexPath(in_dexPath); !result.ok()) {
-    *_aidl_return = result.error().message();
-  } else {
-    *_aidl_return = std::nullopt;
-  }
-  return ScopedAStatus::ok();
-}
-
-ScopedAStatus Artd::validateClassLoaderContext(const std::string& in_dexPath,
-                                               const std::string& in_classLoaderContext,
-                                               std::optional<std::string>* _aidl_return) {
-  if (in_classLoaderContext == ClassLoaderContext::kUnsupportedClassLoaderContextEncoding) {
-    *_aidl_return = std::nullopt;
-    return ScopedAStatus::ok();
-  }
-
-  std::unique_ptr<ClassLoaderContext> context = ClassLoaderContext::Create(in_classLoaderContext);
-  if (context == nullptr) {
-    *_aidl_return = ART_FORMAT("Class loader context '{}' is invalid", in_classLoaderContext);
-    return ScopedAStatus::ok();
-  }
-
-  std::vector<std::string> flattened_context = context->FlattenDexPaths();
-  std::string dex_dir = Dirname(in_dexPath);
-  for (const std::string& context_element : flattened_context) {
-    std::string context_path = std::filesystem::path(dex_dir).append(context_element);
-    if (Result<void> result = ValidateDexPath(context_path); !result.ok()) {
-      *_aidl_return = result.error().message();
-      return ScopedAStatus::ok();
-    }
-  }
-
-  *_aidl_return = std::nullopt;
-  return ScopedAStatus::ok();
-}
-
 Result<void> Artd::Start() {
   OR_RETURN(SetLogVerbosity());
 
diff --git a/artd/artd.h b/artd/artd.h
index 8d9fa6f..c78aaa9 100644
--- a/artd/artd.h
+++ b/artd/artd.h
@@ -172,13 +172,6 @@
       const aidl::com::android::server::art::RuntimeArtifactsPath& in_runtimeArtifactsPath,
       int64_t* _aidl_return) override;
 
-  ndk::ScopedAStatus validateDexPath(const std::string& in_dexPath,
-                                     std::optional<std::string>* _aidl_return) override;
-
-  ndk::ScopedAStatus validateClassLoaderContext(const std::string& in_dexPath,
-                                                const std::string& in_classLoaderContext,
-                                                std::optional<std::string>* _aidl_return) override;
-
   android::base::Result<void> Start();
 
  private:
diff --git a/artd/binder/com/android/server/art/IArtd.aidl b/artd/binder/com/android/server/art/IArtd.aidl
index a296337..014f63e 100644
--- a/artd/binder/com/android/server/art/IArtd.aidl
+++ b/artd/binder/com/android/server/art/IArtd.aidl
@@ -190,17 +190,4 @@
      */
     long deleteRuntimeArtifacts(
             in com.android.server.art.RuntimeArtifactsPath runtimeArtifactsPath);
-
-    /**
-     * Returns an error message if the given dex path is invalid, or null if the validation
-     * passes.
-     */
-    @nullable @utf8InCpp String validateDexPath(@utf8InCpp String dexPath);
-
-    /**
-     * Returns an error message if the given class loader context is invalid, or null if the
-     * validation passes.
-     */
-    @nullable @utf8InCpp String validateClassLoaderContext(@utf8InCpp String dexPath,
-            @utf8InCpp String classLoaderContext);
 }
diff --git a/artd/path_utils.cc b/artd/path_utils.cc
index aa5a2b1..61d185b 100644
--- a/artd/path_utils.cc
+++ b/artd/path_utils.cc
@@ -31,6 +31,7 @@
 #include "fstab/fstab.h"
 #include "oat_file_assistant.h"
 #include "runtime_image.h"
+#include "service.h"
 #include "tools/tools.h"
 
 namespace art {
@@ -49,6 +50,9 @@
 using ::android::fs_mgr::Fstab;
 using ::android::fs_mgr::FstabEntry;
 using ::android::fs_mgr::ReadFstabFromProcMounts;
+using ::art::service::ValidateDexPath;
+using ::art::service::ValidatePathElement;
+using ::art::service::ValidatePathElementSubstring;
 
 using PrebuiltProfilePath = ProfilePath::PrebuiltProfilePath;
 using PrimaryCurProfilePath = ProfilePath::PrimaryCurProfilePath;
@@ -61,45 +65,6 @@
 // Only to be changed for testing.
 std::string_view gListRootDir = "/";
 
-Result<void> ValidateAbsoluteNormalPath(const std::string& path_str) {
-  if (path_str.empty()) {
-    return Errorf("Path is empty");
-  }
-  if (path_str.find('\0') != std::string::npos) {
-    return Errorf("Path '{}' has invalid character '\\0'", path_str);
-  }
-  std::filesystem::path path(path_str);
-  if (!path.is_absolute()) {
-    return Errorf("Path '{}' is not an absolute path", path_str);
-  }
-  if (path.lexically_normal() != path_str) {
-    return Errorf("Path '{}' is not in normal form", path_str);
-  }
-  return {};
-}
-
-Result<void> ValidatePathElementSubstring(const std::string& path_element_substring,
-                                          const std::string& name) {
-  if (path_element_substring.empty()) {
-    return Errorf("{} is empty", name);
-  }
-  if (path_element_substring.find('/') != std::string::npos) {
-    return Errorf("{} '{}' has invalid character '/'", name, path_element_substring);
-  }
-  if (path_element_substring.find('\0') != std::string::npos) {
-    return Errorf("{} '{}' has invalid character '\\0'", name, path_element_substring);
-  }
-  return {};
-}
-
-Result<void> ValidatePathElement(const std::string& path_element, const std::string& name) {
-  OR_RETURN(ValidatePathElementSubstring(path_element, name));
-  if (path_element == "." || path_element == "..") {
-    return Errorf("Invalid {} '{}'", name, path_element);
-  }
-  return {};
-}
-
 }  // namespace
 
 Result<std::string> GetAndroidDataOrError() {
@@ -190,11 +155,6 @@
   return {};
 }
 
-Result<void> ValidateDexPath(const std::string& dex_path) {
-  OR_RETURN(ValidateAbsoluteNormalPath(dex_path));
-  return {};
-}
-
 Result<std::string> BuildArtBinPath(const std::string& binary_name) {
   return ART_FORMAT("{}/bin/{}", OR_RETURN(GetArtRootOrError()), binary_name);
 }
diff --git a/artd/path_utils.h b/artd/path_utils.h
index 77aa82a..b42b639 100644
--- a/artd/path_utils.h
+++ b/artd/path_utils.h
@@ -46,8 +46,6 @@
 android::base::Result<void> ValidateRuntimeArtifactsPath(
     const aidl::com::android::server::art::RuntimeArtifactsPath& runtime_artifacts_path);
 
-android::base::Result<void> ValidateDexPath(const std::string& dex_path);
-
 android::base::Result<std::string> BuildArtBinPath(const std::string& binary_name);
 
 // Returns the absolute path to the OAT file built from the `ArtifactsPath`.
diff --git a/artd/path_utils_test.cc b/artd/path_utils_test.cc
index 960bf97..47b503f 100644
--- a/artd/path_utils_test.cc
+++ b/artd/path_utils_test.cc
@@ -62,29 +62,12 @@
       HasValue(android_data_ + "/dalvik-cache/arm64/a@b.apk@classes.dex"));
 }
 
-TEST_F(PathUtilsTest, BuildOatPathEmptyDexPath) {
-  EXPECT_THAT(BuildOatPath(ArtifactsPath{.dexPath = "", .isa = "arm64", .isInDalvikCache = false}),
-              HasError(WithMessage("Path is empty")));
-}
-
-TEST_F(PathUtilsTest, BuildOatPathRelativeDexPath) {
+TEST_F(PathUtilsTest, BuildOatPathInvalidDexPath) {
   EXPECT_THAT(
       BuildOatPath(ArtifactsPath{.dexPath = "a/b.apk", .isa = "arm64", .isInDalvikCache = false}),
       HasError(WithMessage("Path 'a/b.apk' is not an absolute path")));
 }
 
-TEST_F(PathUtilsTest, BuildOatPathNonNormalDexPath) {
-  EXPECT_THAT(BuildOatPath(ArtifactsPath{
-                  .dexPath = "/a/c/../b.apk", .isa = "arm64", .isInDalvikCache = false}),
-              HasError(WithMessage("Path '/a/c/../b.apk' is not in normal form")));
-}
-
-TEST_F(PathUtilsTest, BuildOatPathNul) {
-  EXPECT_THAT(BuildOatPath(ArtifactsPath{
-                  .dexPath = "/a/\0/b.apk"s, .isa = "arm64", .isInDalvikCache = false}),
-              HasError(WithMessage("Path '/a/\0/b.apk' has invalid character '\\0'"s)));
-}
-
 TEST_F(PathUtilsTest, BuildOatPathInvalidIsa) {
   EXPECT_THAT(BuildOatPath(
                   ArtifactsPath{.dexPath = "/a/b.apk", .isa = "invalid", .isInDalvikCache = false}),
@@ -109,51 +92,27 @@
   EXPECT_THAT(BuildPrimaryRefProfilePath(
                   PrimaryRefProfilePath{.packageName = "...", .profileName = "primary"}),
               HasValue(android_data_ + "/misc/profiles/ref/.../primary.prof"));
-  EXPECT_THAT(BuildPrimaryRefProfilePath(
-                  PrimaryRefProfilePath{.packageName = "!@#$%^&*()_+-=", .profileName = "primary"}),
-              HasValue(android_data_ + "/misc/profiles/ref/!@#$%^&*()_+-=/primary.prof"));
 }
 
 TEST_F(PathUtilsTest, BuildPrimaryRefProfilePathPackageNameWrong) {
   EXPECT_THAT(BuildPrimaryRefProfilePath(
-                  PrimaryRefProfilePath{.packageName = "", .profileName = "primary"}),
-              HasError(WithMessage("packageName is empty")));
-  EXPECT_THAT(BuildPrimaryRefProfilePath(
-                  PrimaryRefProfilePath{.packageName = ".", .profileName = "primary"}),
-              HasError(WithMessage("Invalid packageName '.'")));
-  EXPECT_THAT(BuildPrimaryRefProfilePath(
                   PrimaryRefProfilePath{.packageName = "..", .profileName = "primary"}),
               HasError(WithMessage("Invalid packageName '..'")));
   EXPECT_THAT(BuildPrimaryRefProfilePath(
                   PrimaryRefProfilePath{.packageName = "a/b", .profileName = "primary"}),
               HasError(WithMessage("packageName 'a/b' has invalid character '/'")));
-  EXPECT_THAT(BuildPrimaryRefProfilePath(
-                  PrimaryRefProfilePath{.packageName = "a\0b"s, .profileName = "primary"}),
-              HasError(WithMessage("packageName 'a\0b' has invalid character '\\0'"s)));
 }
 
 TEST_F(PathUtilsTest, BuildPrimaryRefProfilePathProfileNameOk) {
   EXPECT_THAT(BuildPrimaryRefProfilePath(
-                  PrimaryRefProfilePath{.packageName = "com.android.foo", .profileName = "."}),
-              HasValue(android_data_ + "/misc/profiles/ref/com.android.foo/..prof"));
-  EXPECT_THAT(BuildPrimaryRefProfilePath(
                   PrimaryRefProfilePath{.packageName = "com.android.foo", .profileName = ".."}),
               HasValue(android_data_ + "/misc/profiles/ref/com.android.foo/...prof"));
-  EXPECT_THAT(BuildPrimaryRefProfilePath(PrimaryRefProfilePath{.packageName = "com.android.foo",
-                                                               .profileName = "!@#$%^&*()_+-="}),
-              HasValue(android_data_ + "/misc/profiles/ref/com.android.foo/!@#$%^&*()_+-=.prof"));
 }
 
 TEST_F(PathUtilsTest, BuildPrimaryRefProfilePathProfileNameWrong) {
   EXPECT_THAT(BuildPrimaryRefProfilePath(
-                  PrimaryRefProfilePath{.packageName = "com.android.foo", .profileName = ""}),
-              HasError(WithMessage("profileName is empty")));
-  EXPECT_THAT(BuildPrimaryRefProfilePath(
                   PrimaryRefProfilePath{.packageName = "com.android.foo", .profileName = "a/b"}),
               HasError(WithMessage("profileName 'a/b' has invalid character '/'")));
-  EXPECT_THAT(BuildPrimaryRefProfilePath(
-                  PrimaryRefProfilePath{.packageName = "com.android.foo", .profileName = "a\0b"s}),
-              HasError(WithMessage("profileName 'a\0b' has invalid character '\\0'"s)));
 }
 
 TEST_F(PathUtilsTest, BuildFinalProfilePathForPrimary) {
@@ -193,18 +152,8 @@
   EXPECT_THAT(BuildTmpProfilePath(TmpProfilePath{
                   .finalPath = PrimaryRefProfilePath{.packageName = "com.android.foo",
                                                      .profileName = "primary"},
-                  .id = ""}),
-              HasError(WithMessage("id is empty")));
-  EXPECT_THAT(BuildTmpProfilePath(TmpProfilePath{
-                  .finalPath = PrimaryRefProfilePath{.packageName = "com.android.foo",
-                                                     .profileName = "primary"},
                   .id = "123/45"}),
               HasError(WithMessage("id '123/45' has invalid character '/'")));
-  EXPECT_THAT(BuildTmpProfilePath(TmpProfilePath{
-                  .finalPath = PrimaryRefProfilePath{.packageName = "com.android.foo",
-                                                     .profileName = "primary"},
-                  .id = "123\0a"s}),
-              HasError(WithMessage("id '123\0a' has invalid character '\\0'"s)));
 }
 
 TEST_F(PathUtilsTest, BuildPrebuiltProfilePath) {
diff --git a/libartservice/service/Android.bp b/libartservice/service/Android.bp
index 6287acc..19b3bef 100644
--- a/libartservice/service/Android.bp
+++ b/libartservice/service/Android.bp
@@ -30,8 +30,11 @@
     srcs: [
         "native/service.cc",
     ],
+    export_include_dirs: ["native"],
     shared_libs: [
+        "libarttools", // Contains "libc++fs".
         "libbase",
+        "libnativehelper",
     ],
 }
 
@@ -43,6 +46,8 @@
         "com.android.art.debug",
     ],
     shared_libs: [
+        "libart",
+        "libartbase",
     ],
 }
 
@@ -57,6 +62,8 @@
         "com.android.art.debug",
     ],
     shared_libs: [
+        "libartd",
+        "libartbased",
     ],
 }
 
@@ -178,6 +185,9 @@
     srcs: [
         "native/service_test.cc",
     ],
+    static_libs: [
+        "libgmock",
+    ],
 }
 
 // Version of ART gtest `art_libartservice_tests` bundled with the ART APEX on target.
@@ -230,6 +240,7 @@
     ],
 
     jni_libs: [
+        "libartservice",
         // The two libraries below are required by ExtendedMockito.
         "libdexmakerjvmtiagent",
         "libstaticjvmtiagent",
diff --git a/libartservice/service/java/com/android/server/art/ArtJni.java b/libartservice/service/java/com/android/server/art/ArtJni.java
new file mode 100644
index 0000000..2cc9ea8
--- /dev/null
+++ b/libartservice/service/java/com/android/server/art/ArtJni.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.art;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
+import dalvik.system.VMRuntime;
+
+/**
+ * JNI methods for ART Service, with wrappers added for testability because Mockito cannot mock JNI
+ * methods.
+ *
+ * @hide
+ */
+public class ArtJni {
+    static {
+        if (VMRuntime.getRuntime().vmLibrary().equals("libartd.so")) {
+            System.loadLibrary("artserviced");
+        } else {
+            System.loadLibrary("artservice");
+        }
+    }
+
+    private ArtJni() {}
+
+    /**
+     * Returns an error message if the given dex path is invalid, or null if the validation passes.
+     */
+    @Nullable
+    public static String validateDexPath(@NonNull String dexPath) {
+        return validateDexPathNative(dexPath);
+    }
+
+    /**
+     * Returns an error message if the given class loader context is invalid, or null if the
+     * validation passes.
+     */
+    @Nullable
+    public static String validateClassLoaderContext(
+            @NonNull String dexPath, @NonNull String classLoaderContext) {
+        return validateClassLoaderContextNative(dexPath, classLoaderContext);
+    }
+
+    @Nullable private static native String validateDexPathNative(@NonNull String dexPath);
+    @Nullable
+    private static native String validateClassLoaderContextNative(
+            @NonNull String dexPath, @NonNull String classLoaderContext);
+}
diff --git a/libartservice/service/java/com/android/server/art/DexUseManagerLocal.java b/libartservice/service/java/com/android/server/art/DexUseManagerLocal.java
index 97b3c8a..c73f71b 100644
--- a/libartservice/service/java/com/android/server/art/DexUseManagerLocal.java
+++ b/libartservice/service/java/com/android/server/art/DexUseManagerLocal.java
@@ -566,7 +566,8 @@
             }
             mDexUse = new DexUse();
             if (proto != null) {
-                mDexUse.fromProto(proto, this::validateDexPath, this::validateClassLoaderContext);
+                mDexUse.fromProto(
+                        proto, ArtJni::validateDexPath, ArtJni::validateClassLoaderContext);
             }
         }
     }
@@ -600,12 +601,12 @@
 
         for (var entry : classLoaderContextByDexContainerFile.entrySet()) {
             Utils.assertNonEmpty(entry.getKey());
-            String errorMsg = validateDexPath(entry.getKey());
+            String errorMsg = ArtJni.validateDexPath(entry.getKey());
             if (errorMsg != null) {
                 throw new IllegalArgumentException(errorMsg);
             }
             Utils.assertNonEmpty(entry.getValue());
-            errorMsg = validateClassLoaderContext(entry.getKey(), entry.getValue());
+            errorMsg = ArtJni.validateClassLoaderContext(entry.getKey(), entry.getValue());
             if (errorMsg != null) {
                 throw new IllegalArgumentException(errorMsg);
             }
@@ -623,29 +624,6 @@
         }
     }
 
-    @Nullable
-    private String validateDexPath(@NonNull String dexPath) {
-        try {
-            return mInjector.getArtd().validateDexPath(dexPath);
-        } catch (RemoteException e) {
-            String errorMsg = "Failed to validate dex path " + dexPath;
-            Log.e(TAG, errorMsg, e);
-            return errorMsg;
-        }
-    }
-
-    @Nullable
-    private String validateClassLoaderContext(
-            @NonNull String dexPath, @NonNull String classLoaderContext) {
-        try {
-            return mInjector.getArtd().validateClassLoaderContext(dexPath, classLoaderContext);
-        } catch (RemoteException e) {
-            String errorMsg = "Failed to validate class loader context " + classLoaderContext;
-            Log.e(TAG, errorMsg, e);
-            return errorMsg;
-        }
-    }
-
     /** @hide */
     @Nullable
     public String getSecondaryClassLoaderContext(
diff --git a/libartservice/service/javatests/com/android/server/art/DexUseManagerTest.java b/libartservice/service/javatests/com/android/server/art/DexUseManagerTest.java
index f34cd43..83cb81c 100644
--- a/libartservice/service/javatests/com/android/server/art/DexUseManagerTest.java
+++ b/libartservice/service/javatests/com/android/server/art/DexUseManagerTest.java
@@ -81,8 +81,8 @@
     private static final String SPLIT_APK = "/data/app/" + OWNING_PKG_NAME + "/split_0.apk";
 
     @Rule
-    public StaticMockitoRule mockitoRule =
-            new StaticMockitoRule(SystemProperties.class, Constants.class, Process.class);
+    public StaticMockitoRule mockitoRule = new StaticMockitoRule(
+            SystemProperties.class, Constants.class, Process.class, ArtJni.class);
 
     private final UserHandle mUserHandle = Binder.getCallingUserHandle();
 
@@ -155,8 +155,8 @@
         mTempFile = File.createTempFile("package-dex-usage", ".pb");
         mTempFile.deleteOnExit();
 
-        lenient().when(mArtd.validateDexPath(any())).thenReturn(null);
-        lenient().when(mArtd.validateClassLoaderContext(any(), any())).thenReturn(null);
+        lenient().when(ArtJni.validateDexPath(any())).thenReturn(null);
+        lenient().when(ArtJni.validateClassLoaderContext(any(), any())).thenReturn(null);
 
         lenient().when(mInjector.getArtd()).thenReturn(mArtd);
         lenient().when(mInjector.getCurrentTimeMillis()).thenReturn(0l);
@@ -712,14 +712,14 @@
 
     @Test(expected = IllegalArgumentException.class)
     public void testInvalidDexPath() throws Exception {
-        lenient().when(mArtd.validateDexPath(any())).thenReturn("invalid");
+        lenient().when(ArtJni.validateDexPath(any())).thenReturn("invalid");
         mDexUseManager.notifyDexContainersLoaded(
                 mSnapshot, OWNING_PKG_NAME, Map.of("/a/b.jar", "PCL[]"));
     }
 
     @Test(expected = IllegalArgumentException.class)
     public void testInvalidClassLoaderContext() throws Exception {
-        lenient().when(mArtd.validateClassLoaderContext(any(), any())).thenReturn("invalid");
+        lenient().when(ArtJni.validateClassLoaderContext(any(), any())).thenReturn("invalid");
         mDexUseManager.notifyDexContainersLoaded(
                 mSnapshot, OWNING_PKG_NAME, Map.of("/a/b.jar", "PCL[]"));
     }
diff --git a/libartservice/service/native/service.cc b/libartservice/service/native/service.cc
index d33cb59..7d223e5 100644
--- a/libartservice/service/native/service.cc
+++ b/libartservice/service/native/service.cc
@@ -16,12 +16,105 @@
 
 #include "service.h"
 
+#include <jni.h>
+
+#include <filesystem>
+
+#include "android-base/errors.h"
+#include "android-base/file.h"
+#include "android-base/result.h"
+#include "class_loader_context.h"
+#include "nativehelper/utils.h"
+
 namespace art {
 namespace service {
 
-std::string getMsg() {
-    return "hello world!";
+using ::android::base::Dirname;
+using ::android::base::Result;
+
+Result<void> ValidateAbsoluteNormalPath(const std::string& path_str) {
+  if (path_str.empty()) {
+    return Errorf("Path is empty");
+  }
+  if (path_str.find('\0') != std::string::npos) {
+    return Errorf("Path '{}' has invalid character '\\0'", path_str);
+  }
+  std::filesystem::path path(path_str);
+  if (!path.is_absolute()) {
+    return Errorf("Path '{}' is not an absolute path", path_str);
+  }
+  if (path.lexically_normal() != path_str) {
+    return Errorf("Path '{}' is not in normal form", path_str);
+  }
+  return {};
 }
 
+Result<void> ValidatePathElementSubstring(const std::string& path_element_substring,
+                                          const std::string& name) {
+  if (path_element_substring.empty()) {
+    return Errorf("{} is empty", name);
+  }
+  if (path_element_substring.find('/') != std::string::npos) {
+    return Errorf("{} '{}' has invalid character '/'", name, path_element_substring);
+  }
+  if (path_element_substring.find('\0') != std::string::npos) {
+    return Errorf("{} '{}' has invalid character '\\0'", name, path_element_substring);
+  }
+  return {};
 }
+
+Result<void> ValidatePathElement(const std::string& path_element, const std::string& name) {
+  OR_RETURN(ValidatePathElementSubstring(path_element, name));
+  if (path_element == "." || path_element == "..") {
+    return Errorf("Invalid {} '{}'", name, path_element);
+  }
+  return {};
 }
+
+Result<void> ValidateDexPath(const std::string& dex_path) {
+  OR_RETURN(ValidateAbsoluteNormalPath(dex_path));
+  return {};
+}
+
+extern "C" JNIEXPORT jstring JNICALL
+Java_com_android_server_art_ArtJni_validateDexPathNative(JNIEnv* env, jobject, jstring j_dex_path) {
+  std::string dex_path(GET_UTF_OR_RETURN(env, j_dex_path));
+
+  if (Result<void> result = ValidateDexPath(dex_path); !result.ok()) {
+    return CREATE_UTF_OR_RETURN(env, result.error().message()).release();
+  } else {
+    return nullptr;
+  }
+}
+
+extern "C" JNIEXPORT jstring JNICALL
+Java_com_android_server_art_ArtJni_validateClassLoaderContextNative(
+    JNIEnv* env, jobject, jstring j_dex_path, jstring j_class_loader_context) {
+  ScopedUtfChars dex_path = GET_UTF_OR_RETURN(env, j_dex_path);
+  std::string class_loader_context(GET_UTF_OR_RETURN(env, j_class_loader_context));
+
+  if (class_loader_context == ClassLoaderContext::kUnsupportedClassLoaderContextEncoding) {
+    return nullptr;
+  }
+
+  std::unique_ptr<ClassLoaderContext> context = ClassLoaderContext::Create(class_loader_context);
+  if (context == nullptr) {
+    return CREATE_UTF_OR_RETURN(
+               env, ART_FORMAT("Class loader context '{}' is invalid", class_loader_context))
+        .release();
+  }
+
+  std::vector<std::string> flattened_context = context->FlattenDexPaths();
+  std::string dex_dir = Dirname(dex_path);
+  for (const std::string& context_element : flattened_context) {
+    std::string context_path = std::filesystem::path(dex_dir).append(context_element);
+    if (Result<void> result = ValidateDexPath(context_path); !result.ok()) {
+      return CREATE_UTF_OR_RETURN(env, result.error().message()).release();
+    }
+  }
+
+  return nullptr;
+}
+
+}  // namespace service
+}  // namespace art
diff --git a/libartservice/service/native/service.h b/libartservice/service/native/service.h
index 2b680a2..7f43f17 100644
--- a/libartservice/service/native/service.h
+++ b/libartservice/service/native/service.h
@@ -19,10 +19,20 @@
 
 #include <string>
 
+#include "android-base/result.h"
+
 namespace art {
 namespace service {
 
-std::string getMsg();
+android::base::Result<void> ValidateAbsoluteNormalPath(const std::string& path_str);
+
+android::base::Result<void> ValidatePathElementSubstring(const std::string& path_element_substring,
+                                                         const std::string& name);
+
+android::base::Result<void> ValidatePathElement(const std::string& path_element,
+                                                const std::string& name);
+
+android::base::Result<void> ValidateDexPath(const std::string& dex_path);
 
 }  // namespace service
 }  // namespace art
diff --git a/libartservice/service/native/service_test.cc b/libartservice/service/native/service_test.cc
index eb84b73..8300bf5 100644
--- a/libartservice/service/native/service_test.cc
+++ b/libartservice/service/native/service_test.cc
@@ -15,14 +15,97 @@
  */
 
 #include "service.h"
+
+#include "android-base/result-gmock.h"
 #include "gtest/gtest.h"
 
 namespace art {
+namespace service {
+namespace {
+
+using ::android::base::testing::HasError;
+using ::android::base::testing::Ok;
+using ::android::base::testing::WithMessage;
+
+using std::literals::operator""s;  // NOLINT
 
 class ArtServiceTest : public testing::Test {};
 
-TEST_F(ArtServiceTest, Hello) {
-  EXPECT_EQ("hello world!", art::service::getMsg());
+TEST_F(ArtServiceTest, ValidatePathElementOk) {
+  EXPECT_THAT(ValidatePathElement("com.android.foo", "packageName"), Ok());
+  EXPECT_THAT(ValidatePathElement("...", "packageName"), Ok());
+  EXPECT_THAT(ValidatePathElement("!@#$%^&*()_+-=", "packageName"), Ok());
 }
 
+TEST_F(ArtServiceTest, ValidatePathElementEmpty) {
+  EXPECT_THAT(ValidatePathElement("", "packageName"),
+              HasError(WithMessage("packageName is empty")));
+}
+
+TEST_F(ArtServiceTest, ValidatePathElementDot) {
+  EXPECT_THAT(ValidatePathElement(".", "packageName"),
+              HasError(WithMessage("Invalid packageName '.'")));
+}
+
+TEST_F(ArtServiceTest, ValidatePathElementDotDot) {
+  EXPECT_THAT(ValidatePathElement("..", "packageName"),
+              HasError(WithMessage("Invalid packageName '..'")));
+}
+
+TEST_F(ArtServiceTest, ValidatePathElementSlash) {
+  EXPECT_THAT(ValidatePathElement("a/b", "packageName"),
+              HasError(WithMessage("packageName 'a/b' has invalid character '/'")));
+}
+
+TEST_F(ArtServiceTest, ValidatePathElementNul) {
+  EXPECT_THAT(ValidatePathElement("a\0b"s, "packageName"),
+              HasError(WithMessage("packageName 'a\0b' has invalid character '\\0'"s)));
+}
+
+TEST_F(ArtServiceTest, ValidatePathElementSubstringOk) {
+  EXPECT_THAT(ValidatePathElementSubstring("com.android.foo", "packageName"), Ok());
+  EXPECT_THAT(ValidatePathElementSubstring(".", "packageName"), Ok());
+  EXPECT_THAT(ValidatePathElementSubstring("..", "packageName"), Ok());
+  EXPECT_THAT(ValidatePathElementSubstring("...", "packageName"), Ok());
+  EXPECT_THAT(ValidatePathElementSubstring("!@#$%^&*()_+-=", "packageName"), Ok());
+}
+
+TEST_F(ArtServiceTest, ValidatePathElementSubstringEmpty) {
+  EXPECT_THAT(ValidatePathElementSubstring("", "packageName"),
+              HasError(WithMessage("packageName is empty")));
+}
+
+TEST_F(ArtServiceTest, ValidatePathElementSubstringSlash) {
+  EXPECT_THAT(ValidatePathElementSubstring("a/b", "packageName"),
+              HasError(WithMessage("packageName 'a/b' has invalid character '/'")));
+}
+
+TEST_F(ArtServiceTest, ValidatePathElementSubstringNul) {
+  EXPECT_THAT(ValidatePathElementSubstring("a\0b"s, "packageName"),
+              HasError(WithMessage("packageName 'a\0b' has invalid character '\\0'"s)));
+}
+
+TEST_F(ArtServiceTest, ValidateDexPathOk) { EXPECT_THAT(ValidateDexPath("/a/b.apk"), Ok()); }
+
+TEST_F(ArtServiceTest, ValidateDexPathEmpty) {
+  EXPECT_THAT(ValidateDexPath(""), HasError(WithMessage("Path is empty")));
+}
+
+TEST_F(ArtServiceTest, ValidateDexPathRelative) {
+  EXPECT_THAT(ValidateDexPath("a/b.apk"),
+              HasError(WithMessage("Path 'a/b.apk' is not an absolute path")));
+}
+
+TEST_F(ArtServiceTest, ValidateDexPathNonNormal) {
+  EXPECT_THAT(ValidateDexPath("/a/c/../b.apk"),
+              HasError(WithMessage("Path '/a/c/../b.apk' is not in normal form")));
+}
+
+TEST_F(ArtServiceTest, ValidateDexPathNul) {
+  EXPECT_THAT(ValidateDexPath("/a/\0/b.apk"s),
+              HasError(WithMessage("Path '/a/\0/b.apk' has invalid character '\\0'"s)));
+}
+
+}  // namespace
+}  // namespace service
 }  // namespace art