View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      https://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.jexl3.internal.introspection;
18  
19  import static org.junit.jupiter.api.Assertions.assertNotNull;
20  import static org.junit.jupiter.api.Assertions.assertTrue;
21  
22  import java.io.ByteArrayOutputStream;
23  import java.io.IOException;
24  import java.io.InputStream;
25  
26  import org.junit.jupiter.api.Test;
27  
28  /**
29   * Regression test for the class-initialization cycle between {@code Permissions} and
30   * {@link org.apache.commons.jexl3.introspection.JexlPermissions}.
31   * <p>{@code Permissions implements JexlPermissions}, and {@code JexlPermissions} carries default
32   * methods, so initializing {@code Permissions} first forces {@code JexlPermissions.<clinit>} to run
33   * while {@code Permissions} is only partially initialized (JLS 12.4.2). {@code JexlPermissions.<clinit>}
34   * computes {@code RESTRICTED}/{@code SECURE} through {@code PermissionsParser}, which reads the
35   * allow/deny marker singletons. If those markers (or the {@code UNRESTRICTED} singleton) were still
36   * {@code null} at that point, initialization threw an NPE.</p>
37   * <p>Because class initialization is a once-per-loader event, this test must run each ordering in a
38   * fresh child-first class loader that re-defines every {@code org.apache.commons.jexl3.*} class, so
39   * that touching {@code Permissions} really does trigger initialization from scratch.</p>
40   */
41  class PermissionsInitOrderTest {
42      /** A child-first loader that re-defines all jexl3 classes so their {@code <clinit>} runs afresh. */
43      private static final class FreshLoader extends ClassLoader {
44          FreshLoader() {
45              super(FreshLoader.class.getClassLoader());
46          }
47  
48          @Override
49          protected Class<?> findClass(final String name) throws ClassNotFoundException {
50              final String path = name.replace('.', '/') + ".class";
51              try (InputStream in = getParent().getResourceAsStream(path)) {
52                  if (in == null) {
53                      throw new ClassNotFoundException(name);
54                  }
55                  final ByteArrayOutputStream bos = new ByteArrayOutputStream();
56                  final byte[] buf = new byte[4096];
57                  int r;
58                  while ((r = in.read(buf)) >= 0) {
59                      bos.write(buf, 0, r);
60                  }
61                  final byte[] b = bos.toByteArray();
62                  return defineClass(name, b, 0, b.length);
63              } catch (final IOException ex) {
64                  throw new ClassNotFoundException(name, ex);
65              }
66          }
67  
68          @Override
69          protected Class<?> loadClass(final String name, final boolean resolve) throws ClassNotFoundException {
70              if (name.startsWith("org.apache.commons.jexl3.")) {
71                  synchronized (getClassLoadingLock(name)) {
72                      Class<?> c = findLoadedClass(name);
73                      if (c == null) {
74                          c = findClass(name);
75                      }
76                      if (resolve) {
77                          resolveClass(c);
78                      }
79                      return c;
80                  }
81              }
82              return super.loadClass(name, resolve);
83          }
84      }
85  
86      /**
87       * Initializes the classes in the given order within a fresh loader and asserts that the
88       * public {@code JexlPermissions} singletons come out fully initialized and functional.
89       *
90       * @param first fully-qualified name of the class to initialize first
91       * @throws Exception if any reflective access fails (an init NPE surfaces as ExceptionInInitializerError)
92       */
93      private void assertSingletonsUsable(final String first) throws Exception {
94          final ClassLoader cl = new FreshLoader();
95          // touching 'first' with initialize=true triggers its <clinit> before anything else
96          Class.forName(first, true, cl);
97          final Class<?> iface = Class.forName("org.apache.commons.jexl3.introspection.JexlPermissions", true, cl);
98          for (final String name : new String[] {"UNRESTRICTED", "RESTRICTED", "SECURE", "NONE"}) {
99              final Object singleton = iface.getField(name).get(null);
100             assertNotNull(singleton, name + " singleton must be initialized");
101         }
102         // RESTRICTED must actually be usable: deny ProcessBuilder, allow StringWriter
103         final Object restricted = iface.getField("RESTRICTED").get(null);
104         final boolean denyPB = (Boolean) iface.getMethod("allow", Class.class)
105             .invoke(restricted, ProcessBuilder.class);
106         final boolean allowSW = (Boolean) iface.getMethod("allow", Class.class)
107             .invoke(restricted, java.io.StringWriter.class);
108         assertTrue(!denyPB, "RESTRICTED must deny ProcessBuilder regardless of init order");
109         assertTrue(allowSW, "RESTRICTED must allow StringWriter regardless of init order");
110     }
111 
112     @Test
113     void testInitJexlPermissionsFirst() throws Exception {
114         assertSingletonsUsable("org.apache.commons.jexl3.introspection.JexlPermissions");
115     }
116 
117     @Test
118     void testInitPermissionsFirst() throws Exception {
119         // the problematic order: Permissions before JexlPermissions
120         assertSingletonsUsable("org.apache.commons.jexl3.internal.introspection.Permissions");
121     }
122 
123     @Test
124     void testInitPermissionsParserFirst() throws Exception {
125         assertSingletonsUsable("org.apache.commons.jexl3.internal.introspection.PermissionsParser");
126     }
127 }