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