CheckPackageMatching.java revision 12745:f068a4ffddd2
1/*
2 * Copyright (c) 2015, Oracle and/or its affiliates. All rights reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation.
8 *
9 * This code is distributed in the hope that it will be useful, but WITHOUT
10 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
12 * version 2 for more details (a copy is included in the LICENSE file that
13 * accompanied this code).
14 *
15 * You should have received a copy of the GNU General Public License version
16 * 2 along with this work; if not, write to the Free Software Foundation,
17 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
18 *
19 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
20 * or visit www.oracle.com if you need additional information or have any
21 * questions.
22 */
23
24/*
25 * @test
26 * @bug 8072692
27 * @summary Check the matching implemented by SecurityManager.checkPackageAccess
28 * @run main/othervm CheckPackageMatching
29 */
30
31import java.util.ArrayList;
32import java.util.Arrays;
33import java.util.Collection;
34import java.util.Collections;
35import java.util.List;
36
37/*
38 * The purpose of this test is not to verify the content of the package
39 * access list - but to ensure that the matching implemented by the
40 * SecurityManager is correct. This is why we have our own pattern matching
41 * algorithm here.
42 */
43public class CheckPackageMatching {
44
45    /**
46     * The restricted packages listed in the package.access property of the
47     * java.security file.
48     */
49    private static final String[] packages =
50        RestrictedPackages.actual().toArray(new String[0]);
51
52    private static final boolean OPEN_JDK = isOpenJDKOnly();
53
54    /**
55     * PackageMatcher implements a state machine that matches package
56     * names against packages parsed from the package access list.
57     */
58    private abstract static class PackageMatcher {
59        // For each state, chars[state] contains the chars that matches.
60        private final char[][] chars;
61        // For each state, states[state][i] contains the next state to go
62        // to when chars[state][i] matches the current character.
63        private final int[][] states;
64
65        // Some markers. We're making the assumption that 0
66        // cannot be a valid character for a package name.
67        //
68        // We use 0 for marking that we expect an end of string in
69        // char[state][i].
70        private static final char END_OF_STRING = 0;
71        // This special state value indicates that we expect the string to end
72        // there.
73        private static final int END_STATE = -1;
74        // This special state value indicates that we can accept any character
75        // from now on.
76        private static final int WILDCARD_STATE = Integer.MIN_VALUE;
77
78        // Create the data for a new state machine to match package names from
79        // the array of package names passed as argument.
80        // Each package name in the array is expected to end with '.'
81        // For each package in packages we're going to compile state data
82        // that will match the regexp:
83        // ^packages[i].substring(0, packages[i].length()-1).replace(".","\\.")$|^packages[i].replace(".","\\.").*
84        //
85        // Let's say the package array is:
86        //
87        // String[] packages = { "sun.", "com.sun.jmx.", "com.sun.proxy.",
88        //                       "apple." };
89        //
90        // then the state machine will need data that looks like:
91        //
92        // char[][] chars = {
93        //    { 'a', 'c', 's' }, { 'p' }, { 'p' }, { 'l' }, { 'e' }, { 0, '.' },
94        //    { 'o' }, { 'm' }, { '.' }, { 's' }, { 'u' }, { 'n' }, { '.' },
95        //    { 'j', 'p'},
96        //    { 'm' }, { 'x' }, { 0, '.' },
97        //    { 'r' }, { 'o' }, { 'x' }, { 'y' }, { 0, '.' },
98        //    { 'u' }, { 'n' }, { 0, '.' }
99        // }
100        // int[][] states = {
101        //    { 1, 6, 22 }, { 2 }, { 3 }, { 4 }, { 5 },
102        //    { END_STATE, WILDCARD_STATE },
103        //    { 7 }, { 8 }, { 9 }, { 10 }, { 11 }, { 12 }, { 13 }, { 14, 17 },
104        //    { 15 }, { 16 }, { END_STATE, WILDCARD_STATE },
105        //    { 18 }, { 19 }, { 20 }, { 21 }, { END_STATE, WILDCARD_STATE },
106        //    { 23 }, { 24 }, { END_STATE, WILDCARD_STATE }
107        // }
108        //
109        // The machine will start by loading the chars and states for state 0
110        // chars[0] => { 'a', 'c', 's' } states[0] => { 1, 6, 22 }
111        // then it examines the char at index 0 in the candidate name.
112        // if the char matches one of the characters in chars[0], then it goes
113        // to the corresponding state in states[0]. For instance - if the first
114        // char in the candidate name is 's', which corresponds to chars[0][2] -
115        // then it will proceed with the next char in the candidate name and go
116        // to state 22 (as indicated by states[0][2]) - where it will load the
117        // chars and states for states 22: chars[22] = { 'u' },
118        // states[22] = { 23 } etc... until the candidate char at the current
119        // index matches no char in chars[states] => the candidate name doesn't
120        // match - or until it finds a success termination condition: the
121        // candidate chars are exhausted and states[state][0] is END_STATE, or
122        // the candidate chars are not exhausted - and
123        // states[state][chars[state]] is WILDCARD_STATE indicating a '.*' like
124        // regexp.
125        //
126        // [Note that the chars in chars[i] are sorted]
127        //
128        // The compile(...) method is reponsible for building the state machine
129        // data and is called only once in the constructor.
130        //
131        // The matches(String candidate) method will tell whether the candidate
132        // matches by implementing the algorithm described above.
133        //
134        PackageMatcher(String[] packages) {
135            final boolean[] selected = new boolean[packages.length];
136            Arrays.fill(selected, true);
137            final ArrayList<char[]> charList = new ArrayList<>();
138            final ArrayList<int[]> stateList = new ArrayList<>();
139            compile(0, 0, packages, selected, charList, stateList);
140            chars = charList.toArray(new char[0][0]);
141            states = stateList.toArray(new int[0][0]);
142        }
143
144        /**
145         * Compiles the state machine data (recursive).
146         *
147         * @param step  The index of the character which we're looking at in
148         *              this step.
149         * @param state The current state (starts at 0).
150         * @param pkgs  The list of packages from which the automaton is built.
151         * @param selected  Indicates which packages we're looking at in this
152                            step.
153         * @param charList  The list from which we will build
154                            {@code char[][] chars;}
155         * @param stateList The list from which we will build
156                            {@code int[][]  states;}
157         * @return the next available state.
158         */
159        private int compile(int step, int state, String[] pkgs,
160                            boolean[] selected, ArrayList<char[]> charList,
161                            ArrayList<int[]> stateList) {
162            final char[] next = new char[pkgs.length];
163            final int[] nexti = new int[pkgs.length];
164            int j = 0;
165            char min = Character.MAX_VALUE; char max = 0;
166            for (int i = 0; i < pkgs.length; i++) {
167                if (!selected[i]) continue;
168                final String p = pkgs[i];
169                final int len = p.length();
170                if (step > len) {
171                    selected[i] = false;
172                    continue;
173                }
174                if (len - 1 == step) {
175                    boolean unknown = true;
176                    for (int k = 0; k < j ; k++) {
177                        if (next[k] == END_OF_STRING) {
178                            unknown = false;
179                            break;
180                        }
181                    }
182                    if (unknown) {
183                        next[j] = END_OF_STRING;
184                        j++;
185                    }
186                    nexti[i] = END_STATE;
187                }
188                final char c = p.charAt(step);
189                nexti[i] = len - 1 == step ? END_STATE : c;
190                boolean unknown = j == 0 || c < min || c > max;
191                if (!unknown) {
192                    if (c != min || c != max) {
193                        unknown = true;
194                        for (int k = 0; k < j ; k++) {
195                            if (next[k] == c) {
196                                unknown = false;
197                                break;
198                            }
199                        }
200                    }
201                }
202                if (unknown) {
203                    min = min > c ? c : min;
204                    max = max < c ? c : max;
205                    next[j] = c;
206                    j++;
207                }
208            }
209            final char[] nc = new char[j];
210            final int[]  nst = new int[j];
211            System.arraycopy(next, 0, nc, 0, nc.length);
212            Arrays.sort(nc);
213            final boolean ns[] = new boolean[pkgs.length];
214
215            charList.ensureCapacity(state + 1);
216            stateList.ensureCapacity(state + 1);
217            charList.add(state, nc);
218            stateList.add(state, nst);
219            state = state + 1;
220            for (int k = 0; k < nc.length; k++) {
221                int selectedCount = 0;
222                boolean endStateFound = false;
223                boolean wildcardFound = false;
224                for (int l = 0; l < nexti.length; l++) {
225                    if (!(ns[l] = selected[l])) {
226                        continue;
227                    }
228                    ns[l] = nexti[l] == nc[k] || nexti[l] == END_STATE
229                            && nc[k] == '.';
230                    endStateFound = endStateFound || nc[k] == END_OF_STRING
231                                    && nexti[l] == END_STATE;
232                    wildcardFound = wildcardFound || nc[k] == '.'
233                                    && nexti[l] == END_STATE;
234                    if (ns[l]) {
235                        selectedCount++;
236                    }
237                }
238                nst[k] = (endStateFound ? END_STATE
239                         : wildcardFound ? WILDCARD_STATE : state);
240                if (selectedCount == 0 || wildcardFound) {
241                    continue;
242                }
243                state = compile(step + 1, state, pkgs, ns, charList, stateList);
244            }
245            return state;
246        }
247
248        /**
249         * Matches 'pkg' against the list of package names compiled in the
250         * state machine data.
251         *
252         * @param pkg The package name to match. Must not end with '.'.
253         * @return true if the package name matches, false otherwise.
254         */
255        public boolean matches(String pkg) {
256            int state = 0;
257            int i;
258            final int len = pkg.length();
259            next: for (i = 0; i <= len; i++) {
260                if (state == WILDCARD_STATE) {
261                    return true; // all characters will match.
262                }
263                if (state == END_STATE) {
264                    return i == len;
265                }
266                final char[] ch = chars[state];
267                final int[] st = states[state];
268                if (i == len) {
269                    // matches only if we have exhausted the string.
270                    return st[0] == END_STATE;
271                }
272                if (st[0] == END_STATE && st.length == 1) {
273                    // matches only if we have exhausted the string.
274                    return i == len;
275                }
276                final char c = pkg.charAt(i); // look at next char...
277                for (int j = st[0] == END_STATE ? 1 : 0; j < ch.length; j++) {
278                    final char n = ch[j];
279                    if (c == n) {      // found a match
280                        state = st[j]; // get the next state.
281                        continue next; // go to next state
282                    } else if (c < n) {
283                        break; // chars are sorted. we won't find it. no match.
284                    }
285                }
286                break; // no match
287            }
288            return false;
289        }
290    }
291
292    private static final class TestPackageMatcher extends PackageMatcher {
293        private final List<String> list;
294
295        TestPackageMatcher(String[] packages) {
296            super(packages);
297            this.list = Collections.unmodifiableList(Arrays.asList(packages));
298        }
299
300        @Override
301        public boolean matches(String pkg) {
302            final boolean match1 = super.matches(pkg);
303            boolean match2 = false;
304            String p2 = pkg + ".";
305            for (String p : list) {
306                if (pkg.startsWith(p) || p2.equals(p)) {
307                    match2 = true;
308                    break;
309                }
310            }
311            if (match1 != match2) {
312                System.err.println("Test Bug: PackageMatcher.matches(\"" +
313                                   pkg + "\") returned " + match1);
314                System.err.println("Package Access List is: " + list);
315                throw new Error("Test Bug: PackageMatcher.matches(\"" +
316                                pkg + "\") returned " + match1);
317            }
318            return match1;
319        }
320    }
321
322    private static void smokeTest() {
323        // these checks should pass.
324        System.getSecurityManager().checkPackageAccess("com.sun.blah");
325        System.getSecurityManager().checkPackageAccess("com.sun.jm");
326        System.getSecurityManager().checkPackageAccess("com.sun.jmxa");
327        System.getSecurityManager().checkPackageAccess("jmx");
328        List<String> actual = Arrays.asList(packages);
329        for (String p : actual) {
330            if (!actual.contains(p)) {
331                System.err.println("Warning: '" + p + " not in package.access");
332            }
333        }
334        if (!actual.contains("sun.")) {
335            throw new Error("package.access does not contain 'sun.'");
336        }
337    }
338
339    // This is a sanity test for our own test code.
340    private static void testTheTest(String[] pkgs, char[][] chars,
341                                    int[][] states) {
342
343        PackageMatcher m = new TestPackageMatcher(pkgs);
344        String unexpected = "";
345        if (!Arrays.deepEquals(chars, m.chars)) {
346            System.err.println("Char arrays differ");
347            if (chars.length != m.chars.length) {
348                System.err.println("Char array lengths differ: expected="
349                        + chars.length + " actual=" + m.chars.length);
350            }
351            System.err.println(Arrays.deepToString(m.chars).replace((char)0,
352                                                   '0'));
353            unexpected = "chars[]";
354        }
355        if (!Arrays.deepEquals(states, m.states)) {
356            System.err.println("State arrays differ");
357            if (states.length != m.states.length) {
358                System.err.println("Char array lengths differ: expected="
359                        + states.length + " actual=" + m.states.length);
360            }
361            System.err.println(Arrays.deepToString(m.states));
362            if (unexpected.length() > 0) {
363                unexpected = unexpected + " and ";
364            }
365            unexpected = unexpected + "states[]";
366        }
367
368        if (unexpected.length() > 0) {
369            throw new Error("Unexpected "+unexpected+" in PackageMatcher");
370        }
371
372        testMatches(m, pkgs);
373    }
374
375    // This is a sanity test for our own test code.
376    private static void testTheTest() {
377        final String[] packages2 = { "sun.", "com.sun.jmx.",
378                                     "com.sun.proxy.", "apple." };
379
380        final int END_STATE = PackageMatcher.END_STATE;
381        final int WILDCARD_STATE = PackageMatcher.WILDCARD_STATE;
382
383        final char[][] chars2 = {
384            { 'a', 'c', 's' }, { 'p' }, { 'p' }, { 'l' }, { 'e' }, { 0, '.' },
385            { 'o' }, { 'm' }, { '.' }, { 's' }, { 'u' }, { 'n' }, { '.' },
386            { 'j', 'p'},
387            { 'm' }, { 'x' }, { 0, '.' },
388            { 'r' }, { 'o' }, { 'x' }, { 'y' }, { 0, '.' },
389            { 'u' }, { 'n' }, { 0, '.' }
390         };
391
392         final int[][] states2 = {
393            { 1, 6, 22 }, { 2 }, { 3 }, { 4 }, { 5 },
394            { END_STATE, WILDCARD_STATE },
395            { 7 }, { 8 }, { 9 }, { 10 }, { 11 }, { 12 }, { 13 }, { 14, 17 },
396            { 15 }, { 16 }, { END_STATE, WILDCARD_STATE },
397            { 18 }, { 19 }, { 20 }, { 21 }, { END_STATE, WILDCARD_STATE },
398            { 23 }, { 24 }, { END_STATE, WILDCARD_STATE }
399         };
400
401         testTheTest(packages2, chars2, states2);
402
403         final String[] packages3 = { "sun.", "com.sun.pro.",
404                                      "com.sun.proxy.", "apple." };
405
406         final char[][] chars3 = {
407            { 'a', 'c', 's' }, { 'p' }, { 'p' }, { 'l' }, { 'e' }, { 0, '.' },
408            { 'o' }, { 'm' }, { '.' }, { 's' }, { 'u' }, { 'n' }, { '.' },
409            { 'p' }, { 'r' }, { 'o' }, { 0, '.', 'x' },
410            { 'y' }, { 0, '.' },
411            { 'u' }, { 'n' }, { 0, '.' }
412         };
413
414         final int[][] states3 = {
415            { 1, 6, 19 }, { 2 }, { 3 }, { 4 }, { 5 },
416            { END_STATE, WILDCARD_STATE },
417            { 7 }, { 8 }, { 9 }, { 10 }, { 11 }, { 12 }, { 13 }, { 14 },
418            { 15 }, { 16 }, { END_STATE, WILDCARD_STATE, 17 },
419            { 18 }, { END_STATE, WILDCARD_STATE },
420            { 20 }, { 21 }, { END_STATE, WILDCARD_STATE }
421         };
422
423         testTheTest(packages3, chars3, states3);
424    }
425
426    private static volatile boolean sanityTesting = false;
427
428    public static void main(String[] args) {
429        System.setSecurityManager(new SecurityManager());
430
431        // Some smoke tests.
432        smokeTest();
433        System.out.println("Smoke tests passed.");
434
435        // Test our own pattern matching algorithm. Here we actually test
436        // the PackageMatcher class from our own test code.
437        sanityTesting = true;
438        try {
439            testTheTest();
440            System.out.println("Sanity tests passed.");
441        } finally {
442            sanityTesting = false;
443        }
444
445        // Now test the package matching in the security manager.
446        PackageMatcher matcher = new TestPackageMatcher(packages);
447
448        // These should not match.
449        for (String pkg : new String[] {"gloups.machin", "su",
450                                        "org.jcp.xml.dsig.interna",
451                                        "com.sun.jm", "com.sun.jmxa"}) {
452            testMatch(matcher, pkg, false, true);
453        }
454
455        // These should match.
456        for (String pkg : Arrays.asList(
457                new String[] {"sun.gloups.machin", "sun", "sun.com",
458                              "com.sun.jmx", "com.sun.jmx.a",
459                              "org.jcp.xml.dsig.internal",
460                              "org.jcp.xml.dsig.internal.foo"})) {
461            testMatch(matcher, pkg, true, true);
462        }
463
464        // Derive a list of packages that should match or not match from
465        // the list in 'packages' - and check that the security manager
466        // throws the appropriate exception.
467        testMatches(matcher, packages);
468    }
469
470    private static void testMatches(PackageMatcher matcher, String[] pkgs) {
471        Collection<String> pkglist = Arrays.asList(pkgs);
472        PackageMatcher ref = new TestPackageMatcher(packages);
473
474        for (String pkg : pkgs) {
475            String candidate = pkg + "toto";
476            boolean expected = true;
477            testMatch(matcher, candidate, expected,
478                      ref.matches(candidate) == expected);
479        }
480
481        for (String pkg : pkgs) {
482            String candidate = pkg.substring(0, pkg.length() - 1);
483            boolean expected = pkglist.contains(candidate + ".");
484            testMatch(matcher, candidate, expected,
485                      ref.matches(candidate) == expected);
486        }
487
488        for (String pkg : pkgs) {
489            if (!OPEN_JDK && pkg.equals("com.sun.media.sound.")) {
490                // don't test com.sun.media.sound since there is an entry
491                // for com.sun.media in non OpenJDK builds. Otherwise,
492                // the test for this package will fail unexpectedly.
493                continue;
494            }
495            String candidate = pkg.substring(0, pkg.length() - 2);
496            boolean expected = pkglist.contains(candidate + ".");
497            testMatch(matcher, candidate, expected,
498                      ref.matches(candidate) == expected);
499        }
500    }
501
502    private static void testMatch(PackageMatcher matcher, String candidate,
503                                  boolean expected, boolean testSecurityManager)
504    {
505        final boolean m = matcher.matches(candidate);
506        if (m != expected) {
507            final String msg = "\"" + candidate + "\": " +
508                (m ? "matches" : "does not match");
509            throw new Error("PackageMatcher does not give expected results: "
510                            + msg);
511        }
512
513        if (sanityTesting) {
514            testSecurityManager = false;
515        }
516
517        if (testSecurityManager) {
518            System.out.println("Access to " + candidate + " should be " +
519                               (expected ? "rejected" : "granted"));
520            final String errormsg = "\"" + candidate + "\" : " +
521                (expected ? "granted" : "not granted");
522            try {
523                System.getSecurityManager().checkPackageAccess(candidate);
524                if (expected) {
525                    System.err.println(errormsg);
526                    throw new Error("Expected exception not thrown: " +
527                                    errormsg);
528                }
529            } catch (SecurityException x) {
530                if (!expected) {
531                    System.err.println(errormsg);
532                    throw new Error(errormsg + " - unexpected exception: " +
533                                    x, x);
534                } else {
535                    System.out.println("Got expected exception: " + x);
536                }
537            }
538        }
539    }
540
541    private static boolean isOpenJDKOnly() {
542        String prop = System.getProperty("java.runtime.name");
543        return prop != null && prop.startsWith("OpenJDK");
544    }
545}
546