001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.wicket.core.util.objects.checker; 018 019import javax.security.auth.Subject; 020import java.beans.PropertyChangeSupport; 021import java.beans.VetoableChangeSupport; 022import java.io.Externalizable; 023import java.io.IOException; 024import java.io.ObjectOutput; 025import java.io.ObjectOutputStream; 026import java.io.ObjectStreamClass; 027import java.io.ObjectStreamField; 028import java.io.OutputStream; 029import java.lang.reflect.Field; 030import java.lang.reflect.InvocationTargetException; 031import java.lang.reflect.Method; 032import java.lang.reflect.Proxy; 033import java.net.InetAddress; 034import java.net.SocketAddress; 035import java.security.Permission; 036import java.security.Permissions; 037import java.util.BitSet; 038import java.util.Date; 039import java.util.HashSet; 040import java.util.IdentityHashMap; 041import java.util.Iterator; 042import java.util.LinkedList; 043import java.util.Locale; 044import java.util.Map; 045import java.util.Random; 046import java.util.Set; 047import java.util.Vector; 048import java.util.concurrent.ConcurrentHashMap; 049import java.util.concurrent.ThreadLocalRandom; 050 051import org.apache.wicket.Component; 052import org.apache.wicket.WicketRuntimeException; 053import org.apache.wicket.util.lang.Classes; 054import org.slf4j.Logger; 055import org.slf4j.LoggerFactory; 056 057/** 058 * Checks an object tree during serialization for wrong state by delegating the work 059 * to the used {@link IObjectChecker IObjectChecker}s. 060 * <p> 061 * As this class depends heavily on JDK's serialization internals using introspection, analyzing may 062 * not be possible, for instance when the runtime environment does not have sufficient rights to set 063 * fields accessible that would otherwise be hidden. You should call 064 * {@link CheckingObjectOutputStream#isAvailable()} to see whether this class can operate properly. 065 * 066 * 067 * An ObjectOutputStream that uses {@link IObjectChecker IObjectChecker}s to check the 068 * state of the object before serializing it. If the checker returns 069 * {@link org.apache.wicket.core.util.objects.checker.IObjectChecker.Result.Status#FAILURE} 070 * then the serialization process is stopped and the error is logged. 071 * </p> 072 */ 073public class CheckingObjectOutputStream extends ObjectOutputStream 074{ 075 private static final Logger log = LoggerFactory.getLogger(CheckingObjectOutputStream.class); 076 077 public static class ObjectCheckException extends WicketRuntimeException 078 { 079 public ObjectCheckException(String message, Throwable cause) 080 { 081 super(message, cause); 082 } 083 } 084 085 /** 086 * Does absolutely nothing. 087 */ 088 private static class NoopOutputStream extends OutputStream 089 { 090 @Override 091 public void close() 092 { 093 } 094 095 @Override 096 public void flush() 097 { 098 } 099 100 @Override 101 public void write(byte[] b) 102 { 103 } 104 105 @Override 106 public void write(byte[] b, int i, int l) 107 { 108 } 109 110 @Override 111 public void write(int b) 112 { 113 } 114 } 115 116 private static abstract class ObjectOutputAdaptor implements ObjectOutput 117 { 118 119 @Override 120 public void close() throws IOException 121 { 122 } 123 124 @Override 125 public void flush() throws IOException 126 { 127 } 128 129 @Override 130 public void write(byte[] b) throws IOException 131 { 132 } 133 134 @Override 135 public void write(byte[] b, int off, int len) throws IOException 136 { 137 } 138 139 @Override 140 public void write(int b) throws IOException 141 { 142 } 143 144 @Override 145 public void writeBoolean(boolean v) throws IOException 146 { 147 } 148 149 @Override 150 public void writeByte(int v) throws IOException 151 { 152 } 153 154 @Override 155 public void writeBytes(String s) throws IOException 156 { 157 } 158 159 @Override 160 public void writeChar(int v) throws IOException 161 { 162 } 163 164 @Override 165 public void writeChars(String s) throws IOException 166 { 167 } 168 169 @Override 170 public void writeDouble(double v) throws IOException 171 { 172 } 173 174 @Override 175 public void writeFloat(float v) throws IOException 176 { 177 } 178 179 @Override 180 public void writeInt(int v) throws IOException 181 { 182 } 183 184 @Override 185 public void writeLong(long v) throws IOException 186 { 187 } 188 189 @Override 190 public void writeShort(int v) throws IOException 191 { 192 } 193 194 @Override 195 public void writeUTF(String str) throws IOException 196 { 197 } 198 } 199 200 /** Holds information about the field and the resulting object being traced. */ 201 private static final class TraceSlot 202 { 203 private final String fieldDescription; 204 205 private final Object object; 206 207 TraceSlot(Object object, String fieldDescription) 208 { 209 this.object = object; 210 this.fieldDescription = fieldDescription; 211 } 212 213 @Override 214 public String toString() 215 { 216 return object.getClass() + " - " + fieldDescription; 217 } 218 } 219 220 private static final NoopOutputStream DUMMY_OUTPUT_STREAM = new NoopOutputStream(); 221 222 /** Whether we can execute the tests. If false, check will just return. */ 223 private static boolean available = true; 224 225 // this hack - accessing the serialization API through introspection - is 226 // the only way to use Java serialization for our purposes without writing 227 // the whole thing from scratch (and even then, it would be limited). This 228 // way of working is of course fragile for internal API changes, but as we 229 // do an extra check on availability and we report when we can't use this 230 // introspection fu, we'll find out soon enough and clients on this class 231 // can fall back on Java's default exception for serialization errors (which 232 // sucks and is the main reason for this attempt). 233 private static Method LOOKUP_METHOD; 234 235 private static Method GET_CLASS_DATA_LAYOUT_METHOD; 236 237 private static Method GET_NUM_OBJ_FIELDS_METHOD; 238 239 private static Method GET_OBJ_FIELD_VALUES_METHOD; 240 241 private static Method GET_FIELD_METHOD; 242 243 private static Method HAS_WRITE_REPLACE_METHOD_METHOD; 244 245 private static Method INVOKE_WRITE_REPLACE_METHOD; 246 247 static 248 { 249 try 250 { 251 LOOKUP_METHOD = ObjectStreamClass.class.getDeclaredMethod("lookup", new Class[] { 252 Class.class, Boolean.TYPE }); 253 LOOKUP_METHOD.setAccessible(true); 254 255 GET_CLASS_DATA_LAYOUT_METHOD = ObjectStreamClass.class.getDeclaredMethod( 256 "getClassDataLayout", (Class[])null); 257 GET_CLASS_DATA_LAYOUT_METHOD.setAccessible(true); 258 259 GET_NUM_OBJ_FIELDS_METHOD = ObjectStreamClass.class.getDeclaredMethod( 260 "getNumObjFields", (Class[])null); 261 GET_NUM_OBJ_FIELDS_METHOD.setAccessible(true); 262 263 GET_OBJ_FIELD_VALUES_METHOD = ObjectStreamClass.class.getDeclaredMethod( 264 "getObjFieldValues", new Class[] { Object.class, Object[].class }); 265 GET_OBJ_FIELD_VALUES_METHOD.setAccessible(true); 266 267 GET_FIELD_METHOD = ObjectStreamField.class.getDeclaredMethod("getField", (Class[])null); 268 GET_FIELD_METHOD.setAccessible(true); 269 270 HAS_WRITE_REPLACE_METHOD_METHOD = ObjectStreamClass.class.getDeclaredMethod( 271 "hasWriteReplaceMethod", (Class[])null); 272 HAS_WRITE_REPLACE_METHOD_METHOD.setAccessible(true); 273 274 INVOKE_WRITE_REPLACE_METHOD = ObjectStreamClass.class.getDeclaredMethod( 275 "invokeWriteReplace", new Class[] { Object.class }); 276 INVOKE_WRITE_REPLACE_METHOD.setAccessible(true); 277 } 278 catch (Exception e) 279 { 280 log.warn("SerializableChecker not available", e); 281 available = false; 282 } 283 } 284 285 private final IObjectChecker[] checkers; 286 287 /** 288 * Gets whether we can execute the tests. If false, calling {@link #check(Object)} will just 289 * return and you are advised to rely on the {@link java.io.NotSerializableException}. Clients are 290 * advised to call this method prior to calling the check method. 291 * 292 * @return whether security settings and underlying API etc allow for accessing the 293 * serialization API using introspection 294 */ 295 public static boolean isAvailable() 296 { 297 return available; 298 } 299 300 /** 301 * The output stream where the serialized object will be written upon successful check 302 */ 303 private final ObjectOutputStream out; 304 305 /** object stack with the trace path. */ 306 private final LinkedList<TraceSlot> traceStack = new LinkedList<>(); 307 308 /** set for checking circular references. */ 309 private final Map<Object, Object> checked = new IdentityHashMap<>(); 310 311 /** string stack with current names pushed. */ 312 private final LinkedList<CharSequence> nameStack = new LinkedList<>(); 313 314 /** root object being analyzed. */ 315 private Object root; 316 317 /** set of classes that had no writeObject methods at lookup (to avoid repeated checking) */ 318 private final Set<Class<?>> writeObjectMethodMissing = new HashSet<>(); 319 320 /** current simple field name. */ 321 private CharSequence simpleName = ""; 322 323 /** current full field description. */ 324 private String fieldDescription; 325 326 /** 327 * Constructor. 328 * 329 * @param outputStream 330 * the output stream where the serialized object will be written upon successful check 331 * @param checkers 332 * the {@link IObjectChecker checkers} that will actually check the objects 333 * @throws IOException 334 * @throws SecurityException 335 */ 336 public CheckingObjectOutputStream(final OutputStream outputStream, final IObjectChecker... checkers) throws IOException, SecurityException 337 { 338 this.out = new ObjectOutputStream(outputStream); 339 this.checkers = checkers; 340 } 341 342 private void check(Object obj) 343 { 344 if (obj == null) 345 { 346 return; 347 } 348 349 if (checked.containsKey(obj)) 350 { 351 return; 352 } 353 354 internalCheck(obj); 355 } 356 357 private void internalCheck(Object obj) 358 { 359 final Object original = obj; 360 Class<?> cls = obj.getClass(); 361 nameStack.add(simpleName); 362 traceStack.add(new TraceSlot(obj, fieldDescription)); 363 364 for (IObjectChecker checker : checkers) 365 { 366 IObjectChecker.Result result = checker.check(obj); 367 if (result.status == IObjectChecker.Result.Status.FAILURE) 368 { 369 String prettyPrintMessage = toPrettyPrintedStack(Classes.name(cls)); 370 String exceptionMessage = result.reason + '\n' + prettyPrintMessage; 371 throw new ObjectCheckException(exceptionMessage, result.cause); 372 } 373 } 374 375 ObjectStreamClass desc; 376 for (;;) 377 { 378 try 379 { 380 desc = (ObjectStreamClass)LOOKUP_METHOD.invoke(null, cls, Boolean.TRUE); 381 Class<?> repCl; 382 if (!(Boolean)HAS_WRITE_REPLACE_METHOD_METHOD.invoke(desc, (Object[])null) || 383 (obj = INVOKE_WRITE_REPLACE_METHOD.invoke(desc, obj)) == null || 384 (repCl = obj.getClass()) == cls) 385 { 386 break; 387 } 388 cls = repCl; 389 } 390 catch (IllegalAccessException | InvocationTargetException e) 391 { 392 throw new RuntimeException(e); 393 } 394 } 395 396 if (cls.isPrimitive()) 397 { 398 // skip 399 } 400 else if (cls.isArray()) 401 { 402 checked.put(original, null); 403 Class<?> ccl = cls.getComponentType(); 404 if (!(ccl.isPrimitive())) 405 { 406 Object[] objs = (Object[])obj; 407 for (int i = 0; i < objs.length; i++) 408 { 409 if (!isKnownToBeSerializable(objs[i])) { 410 CharSequence arrayPos = new StringBuilder(4).append('[').append(i).append(']'); 411 simpleName = arrayPos; 412 fieldDescription += arrayPos; 413 check(objs[i]); 414 } 415 } 416 } 417 } 418 else if (obj instanceof Externalizable && (!Proxy.isProxyClass(cls))) 419 { 420 Externalizable extObj = (Externalizable)obj; 421 try 422 { 423 extObj.writeExternal(new ObjectOutputAdaptor() 424 { 425 private int count = 0; 426 427 @Override 428 public void writeObject(Object streamObj) throws IOException 429 { 430 // Check for circular reference. 431 if (checked.containsKey(streamObj)) 432 { 433 return; 434 } 435 436 CharSequence arrayPos = new StringBuilder(10).append("[write:").append(count++).append(']'); 437 simpleName = arrayPos; 438 fieldDescription += arrayPos; 439 440 check(streamObj); 441 442 checked.put(streamObj, null); 443 } 444 }); 445 } 446 catch (Exception e) 447 { 448 if (e instanceof ObjectCheckException) 449 { 450 throw (ObjectCheckException)e; 451 } 452 log.warn("Error delegating to Externalizable. Path: {}", currentPath(), e); 453 } 454 } 455 else 456 { 457 Method writeObjectMethod = null; 458 if (writeObjectMethodMissing.contains(cls) == false) 459 { 460 try 461 { 462 writeObjectMethod = cls.getDeclaredMethod("writeObject", java.io.ObjectOutputStream.class); 463 } 464 catch (SecurityException | NoSuchMethodException e) 465 { 466 // we can't access / set accessible to true 467 writeObjectMethodMissing.add(cls); 468 } 469 } 470 471 if (writeObjectMethod != null) 472 { 473 class InterceptingObjectOutputStream extends ObjectOutputStream 474 { 475 private int counter; 476 477 InterceptingObjectOutputStream() throws IOException 478 { 479 super(DUMMY_OUTPUT_STREAM); 480 enableReplaceObject(true); 481 } 482 483 @Override 484 protected Object replaceObject(Object streamObj) throws IOException 485 { 486 if (streamObj == original) 487 { 488 return streamObj; 489 } 490 491 counter++; 492 // Check for circular reference. 493 if (checked.containsKey(streamObj)) 494 { 495 return null; 496 } 497 498 CharSequence arrayPos = new StringBuilder(10).append("[write:").append(counter).append(']'); 499 simpleName = arrayPos; 500 fieldDescription += arrayPos; 501 check(streamObj); 502 checked.put(streamObj, null); 503 return streamObj; 504 } 505 } 506 try 507 { 508 InterceptingObjectOutputStream ioos = new InterceptingObjectOutputStream(); 509 ioos.writeObject(obj); 510 } 511 catch (Exception e) 512 { 513 if (e instanceof ObjectCheckException) 514 { 515 throw (ObjectCheckException)e; 516 } 517 log.warn("error delegating to writeObject : {}, path: {}", e.getMessage(), currentPath()); 518 } 519 } 520 else 521 { 522 Object[] slots; 523 try 524 { 525 slots = (Object[])GET_CLASS_DATA_LAYOUT_METHOD.invoke(desc, (Object[])null); 526 } 527 catch (Exception e) 528 { 529 throw new RuntimeException(e); 530 } 531 for (Object slot : slots) 532 { 533 ObjectStreamClass slotDesc; 534 try 535 { 536 Field descField = slot.getClass().getDeclaredField("desc"); 537 descField.setAccessible(true); 538 slotDesc = (ObjectStreamClass)descField.get(slot); 539 } 540 catch (Exception e) 541 { 542 throw new RuntimeException(e); 543 } 544 checked.put(original, null); 545 checkFields(obj, slotDesc); 546 } 547 } 548 } 549 550 traceStack.removeLast(); 551 nameStack.removeLast(); 552 } 553 554 private void checkFields(Object obj, ObjectStreamClass desc) 555 { 556 int numFields; 557 try 558 { 559 numFields = (Integer)GET_NUM_OBJ_FIELDS_METHOD.invoke(desc, (Object[])null); 560 } 561 catch (IllegalAccessException | InvocationTargetException e) 562 { 563 throw new RuntimeException(e); 564 } 565 566 if (numFields > 0) 567 { 568 int numPrimFields; 569 ObjectStreamField[] fields = desc.getFields(); 570 Object[] objVals = new Object[numFields]; 571 numPrimFields = fields.length - objVals.length; 572 try 573 { 574 GET_OBJ_FIELD_VALUES_METHOD.invoke(desc, obj, objVals); 575 } 576 catch (IllegalAccessException | InvocationTargetException e) 577 { 578 throw new RuntimeException(e); 579 } 580 for (int i = 0; i < objVals.length; i++) 581 { 582 if (isKnownToBeSerializable(objVals[i])) 583 { 584 // filter out common cases 585 continue; 586 } 587 588 // Check for circular reference. 589 if (checked.containsKey(objVals[i])) 590 { 591 continue; 592 } 593 594 ObjectStreamField fieldDesc = fields[numPrimFields + i]; 595 Field field; 596 try 597 { 598 field = (Field)GET_FIELD_METHOD.invoke(fieldDesc, (Object[])null); 599 } 600 catch (IllegalAccessException | InvocationTargetException e) 601 { 602 throw new RuntimeException(e); 603 } 604 605 simpleName = field.getName(); 606 fieldDescription = field.toString(); 607 check(objVals[i]); 608 } 609 } 610 } 611 612 private boolean isKnownToBeSerializable(Object obj) { 613 return isCommonClass(obj) || hasCustomSerialization(obj); 614 } 615 616 private boolean isCommonClass(Object obj) { 617 return obj instanceof String || obj instanceof Number || 618 obj instanceof Date || obj instanceof Boolean || 619 obj instanceof Class || obj instanceof Throwable; 620 } 621 622 /** 623 * Some classes use {@link ObjectOutputStream.PutField} in their implementation of 624 * <em>private void writeObject(ObjectOutputStream s) throws IOException</em> and this 625 * (sometimes) breaks the introspection done by this class, and even crashes the JVM! 626 * 627 * @see <a href="https://issues.apache.org/jira/browse/WICKET-6704">WICKET-6704</a> 628 * @param obj The object to check 629 * @return {@code true} if the object type is one of these special ones 630 */ 631 private boolean hasCustomSerialization(Object obj) { 632 return obj instanceof PropertyChangeSupport || 633 obj instanceof VetoableChangeSupport || 634 obj instanceof Permission || 635 obj instanceof Permissions || 636 obj instanceof BitSet || 637 obj instanceof ConcurrentHashMap || 638 obj instanceof Vector || 639 obj instanceof InetAddress || 640 obj instanceof SocketAddress || 641 obj instanceof Locale || 642 obj instanceof Random || 643 obj instanceof ThreadLocalRandom || 644 obj instanceof StringBuffer || 645 obj instanceof Subject; 646 } 647 648 /** 649 * @return name from root to current node concatenated with slashes 650 */ 651 private StringBuilder currentPath() 652 { 653 StringBuilder b = new StringBuilder(); 654 for (Iterator<CharSequence> it = nameStack.iterator(); it.hasNext();) 655 { 656 b.append(it.next()); 657 if (it.hasNext()) 658 { 659 b.append('/'); 660 } 661 } 662 return b; 663 } 664 665 /** 666 * Dump with indentation. 667 * 668 * @param type 669 * the type that couldn't be serialized 670 * @return A very pretty dump 671 */ 672 protected final String toPrettyPrintedStack(String type) 673 { 674 StringBuilder result = new StringBuilder(512); 675 StringBuilder spaces = new StringBuilder(32); 676 result.append("A problem occurred while checking object with type: "); 677 result.append(type); 678 result.append("\nField hierarchy is:"); 679 for (TraceSlot slot : traceStack) 680 { 681 spaces.append(' ').append(' '); 682 result.append('\n').append(spaces).append(slot.fieldDescription); 683 result.append(" [class=").append(Classes.name(slot.object.getClass())); 684 if (slot.object instanceof Component) 685 { 686 Component component = (Component)slot.object; 687 result.append(", path=").append(component.getPath()); 688 } 689 result.append(']'); 690 } 691 result.append(" <----- field that is causing the problem"); 692 return result.toString(); 693 } 694 695 /** 696 * @see java.io.ObjectOutputStream#writeObjectOverride(java.lang.Object) 697 */ 698 @Override 699 protected final void writeObjectOverride(Object obj) throws IOException 700 { 701 if (!available) 702 { 703 return; 704 } 705 root = obj; 706 if (fieldDescription == null) 707 { 708 fieldDescription = (root instanceof Component) ? ((Component)root).getPath() : ""; 709 } 710 711 check(root); 712 out.writeObject(obj); 713 } 714 715 /** 716 * @see java.io.ObjectOutputStream#reset() 717 */ 718 @Override 719 public void reset() throws IOException 720 { 721 root = null; 722 checked.clear(); 723 fieldDescription = null; 724 simpleName = null; 725 traceStack.clear(); 726 nameStack.clear(); 727 writeObjectMethodMissing.clear(); 728 } 729 730 @Override 731 public void close() throws IOException 732 { 733 // do not call super.close() because SerializableChecker uses ObjectOutputStream's no-arg constructor 734 735 // just null-ify the declared members 736 reset(); 737 } 738}