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 }