読者です 読者をやめる 読者になる 読者になる

この日記は私的なものであり所属会社の見解とは無関係です。 GitHub: takahashikzn

TldScannerでNullPointerExceptionが出て起動しない

僕は普段、開発用としてTomcat-9.0.0系を使っています。

先ほどTomcat-9.0.0-M6に更新したら、このようなエラーが出て起動しなくなりました。

java.util.concurrent.ExecutionException: org.apache.catalina.LifecycleException: Failed to start component [StandardEngine[Catalina].StandardHost[localhost].StandardContext[/foobar]]
    at java.util.concurrent.FutureTask.report(FutureTask.java:122)
    at java.util.concurrent.FutureTask.get(FutureTask.java:192)
    at org.apache.catalina.core.ContainerBase.startInternal(ContainerBase.java:911)
    at org.apache.catalina.core.StandardHost.startInternal(StandardHost.java:890)
    at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:152)
    at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1403)
    at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1393)
    at java.util.concurrent.FutureTask.run(FutureTask.java:266)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
    at java.lang.Thread.run(Thread.java:745)
Caused by: org.apache.catalina.LifecycleException: Failed to start component [StandardEngine[Catalina].StandardHost[localhost].StandardContext[/foobar]]
    at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:158)
    ... 6 more
Caused by: java.lang.NullPointerException
    at org.apache.jasper.servlet.TldScanner$TldScannerCallback.scanWebInfClasses(TldScanner.java:395)
    at org.apache.tomcat.util.scan.StandardJarScanner.scan(StandardJarScanner.java:208)
    at org.apache.jasper.servlet.TldScanner.scanJars(TldScanner.java:262)
    at org.apache.jasper.servlet.TldScanner.scan(TldScanner.java:104)
    at org.apache.jasper.servlet.JasperInitializer.onStartup(JasperInitializer.java:103)
    at org.apache.catalina.core.StandardContext.startInternal(StandardContext.java:5183)
    at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:152)
    ... 6 more

該当箇所のソースは以下の通り。

@Override
public void scanWebInfClasses() throws IOException {
    // This is used when scanAllDirectories is enabled and one or more
    // JARs have been unpacked into WEB-INF/classes as happens with some
    // IDEs.

    Set<String> paths = context.getResourcePaths(WEB_INF + "classes/META-INF");

    for (String path : paths) { // ← 395行目
        if (path.endsWith(TLD_EXT)) {
            try {
                parseTld(path);
            } catch (SAXException e) {
                throw new IOException(e);
            }
        }
    }
}

見ての通り、pathsがnullです。

なぜだ…?と思って当該アプリケーションのMETA-INFを見てみると、空ディレクトリでした。 もしやと思い、META-INFを消すと元通りに起動するようになりました。まぁ軽いバグの一種なんですかね。

ちなみに、この現象は8.5.2でも起きるようです。(該当箇所のソースが同一なので)

Eclipse 4.6 "Neon" M7リリース

f:id:takahashikzn:20160511132958p:plain

そろそろ毎年恒例のイベントがやってきます。 今回のコードネームはNeonですが、惑星シリーズのネーミングは止めたようですね。

毎年のリリースペースならばマイルストーンビルドはこれで打ち止めです。次からはRC(Release Candidate)扱いになるでしょう。

(2016-05-14追記 RC1がリリースされました)

ちなみに4.7のコードネームはOxygenだそうです。 参考: Eclipse (統合開発環境) - Wikipedia

個人的に気になった追加機能

http://www.eclipse.org/eclipse/news/4.6/M7/


Improved interactive performance and reduced memory consumption (UI反応性の改善と消費メモリ低減)

This and earlier milestones contain a multitude of fixes to enhance interactive and startup performance of the Eclipse IDE, and to reduce overall memory consumption.

ちょっと前のマイルストーンビルドから様々な改善が取り入れられた結果、UI反応性と起動時のパフォーマンスが改善している。全体的な消費メモリも低減されている。


Link widget background color can be styled via CSS (リンクウィジェットの背景へCSSを適用可能に)

You can now style the background color of the SWT Link widget. This is used in the default dark theme provided by Eclipse.

SWTのリンクウィジェット背景色をCSSで設定可能にした。この機能はデフォルトのダークテーマで使用されている。


Themed scroll bar enabled for editors in dark theme (ダークテーマのためにスクロールバーへスタイルを適用可能に)

It's now possible to replace the native scroll bar of a StyledText by a styled overlay. This is enabled by default in the dark theme on Windows.

とうとうネイティブスクロールバーにスタイルを適用できるようになった。この機能はWindows上のダークテーマで使用されている。


Quick Access improvements (クイックアクセスの改善)

Quick Access (Ctrl+3) is a small text field in the toolbar. You can use it to trigger any command in the Eclipse IDE.

 

You can now restrict the search to Views, Commands, etc. by typing the category name followed by a colon. For example, to filter the list of all the views, start typing "Views: " in the search-box.

 

A few usability bugs have been fixed: The tooltip shows the keyboard shortcut, the number of search results per category is independent of the size of the proposals window, and the list with previous choices already opens when you click the field with the mouse.

Ctrl+3で起動されるクイックアクセスはツールバーにある小さなテキストボックスだが、ここで好きなコマンドを実行できるようになった。

コロン(:)に続いてカテゴリ名を入力すれば、検索される範囲をビュー(訳注:「問題」や「コンソール」などの表示領域のこと)や、コマンドに制限できる。例えば、ビューの一覧に絞って検索したければ"Views: "と入力すれば良い。

幾つかのバグも解消されている。キーボードショートカットやカテゴリごとにグループ分けされた検索結果を表示するツールチップは候補ウィンドウとは独立したサイズを持つようになった。「以前に選択したもの」リストはマウスクリックした時点でオープンされるようになる。


Full Screen (フルスクリーン)

The Full Screen feature is now also available on Windows and Linux. You can toggle the mode via shortcut (Alt+F11) or menu (Window > Appearance > Toggle Full Screen). When Full Screen is activated, you'll see a dialog which tells you how to turn it off again.

LinuxWindowsでフルスクリーン表示ができるようになった。Alt+F11でトグルされる。またはメニューからも可。(ウィンドウ → 外見 → フルスクリーン) フルスクリーンが有効にされたとき、無効にする方法を知らせるダイアログが表示されるだろう。(訳注: Chromeを全画面表示した時に表示されるアレのようなもの)


New options in code formatter

A few new options have been added in the formatter profile editor.

 

(1) In the new Parentheses tab, you can order the formatter to keep parentheses of various Java elements on separate lines, i.e. put a line break after the opening parenthesis and before the closing parenthesis. This can be done always, only when parentheses are not empty, or when their content is wrapped. There's also an > option to preserve existing positions, if you want to manually manage parentheses positions on a case-by-case basis.

コードフォーマットのオプションが増えた。

(1) 括弧位置調整のためのオプションタブが追加された。そこではJavaの構文要素ごとに、「開き括弧の前と閉じ括弧の後に改行を入れるか否かを、括弧の中身が空かどうかで個別に指定」のような細かい設定が可能だ。 また、括弧の位置を自分で調整したい場合、フォーマッターの処理対象外にすることが出来るようになった。


(2) In the Line Wrapping tab, you can set the wrapping policy for parameterized types.

型引数の行折り返し設定が追加された。


(3) Also in the Line Wrapping tab, you can decide to wrap before or after operators in assignments and conditional expressions.

演算子の前後における行折り返しポリシーが設定できるようになった。

Struts2のS2-032の要点

またOGNL絡みで脆弱性が出ました。ホントにこの機構は問題児ですね… とはいえOGNLはStruts2の根幹に関わる機能なので、OFFにすることは難しい。

「1アクション=1アクションクラス」派でない方にとっては、DMIを使えないのは死活問題になりかねません。

ちなみに僕は「1アクション=1アクションクラス」派です。


さて、POCが出回っているので、今回の攻撃が成功する原因を調べてみました。POCはこのあたりを参考に。

ひとつ目のリンクはそもそも今回の脆弱性を発見した企業のブログです。 リンク先に同等の解説があるようなのですが中国語なので僕には読解不能。

結論から言うとDefaultActionInvocation、お前だ

com.opensymphony.xwork2.DefaultActionInvocationのinvokeActionが問題です。 こちらのメソッドの抜粋は以下の通り。

protected String invokeAction(Object action, ActionConfig actionConfig) throws Exception {
    String methodName = proxy.getMethod();

    ...
    
    try {
    
        ...
        
        try {
            methodResult = ognlUtil.getValue(methodName + "()", getStack().getContext(), action);
        } catch (MethodFailedException e) {
            ...
        }
        
        ...
    } catch (NoSuchPropertyException e) {
        ...
    } catch (MethodFailedException e) {
        ...
    } finally {
        ...
    }
}

よく見てみましょう。

methodName + "()"

コレが原因です。

まず、Struts2のDMIは、method:cancelOrderのようにして、ActionクラスのcancelOrderメソッドを呼び出すような使い方を想定しています。 しかしながらパラメータの妥当性検証が不足しているために、

method:(#_memberAccess).setExcludedClasses(@java.util.Collections@EMPTY_SET),
(#_memberAccess).setExcludedPackageNamePatterns(@java.util.Collections@EMPTY_SET),
@java.lang.System@out.println('やっほー'),
new java.lang.String

というクエリパラメータが

String methodName = "(#_memberAccess).setExcludedClasses(@java.util.Collections@EMPTY_SET),(#_memberAccess).setExcludedPackageNamePatterns(@java.util.Collections@EMPTY_SET),@java.lang.System@out.println('やっほー'),new java.lang.String";

という値で格納されてしまい、

ognlUtil.getValue("@java.lang.System@out.println('やっほー'),new java.lang.String" + "()", ...)

という形へ展開されて、SecureMemberAccessのガード機構を無効化した上で(#_memberAccess.setExcludedClassesのあたり)、OGNLとして実行されてしまうわけです。 最後のjava.lang.Stringは、"()"が末尾にくっついた時に妥当なOGNL式にするための調整であり、その範疇内であれば何でも良い。 だから

method:(#_memberAccess).setExcludedClasses(@java.util.Collections@EMPTY_SET),
(#_memberAccess).setExcludedPackageNamePatterns(@java.util.Collections@EMPTY_SET),
@java.lang.System.exit

のような式も可。

対策

DMIをOFFにすることがベストです。なお、デフォルト状態ではOFFです。(※struts-2.3.15.2以降)

また、com.opensymphony.xwork2.DefaultActionProxy#prepareでDMIのメソッド名はスクリーニングされているので、

protected void prepare() {
    ...
    try {
        ...

        if (config.isAllowedMethod(method)) { // ←許可されたメソッド名であるか検査する
            invocation.init(this);
        } else {
            throw new ConfigurationException("This method: " + method + " for action " + actionName + " is not allowed!");
        }
    } finally {
    }
}

struts.xmlでDMIのメソッド名を何でもあり、すなわちregex:.*などと指定していない限り今回の脆弱性を突くことはできません。

(※手元で試した限りでは。責任は持ちませんので悪しからず)

Thanks to @Annotations, @Progress is @Unstoppable! (@Annotationが進化を促進する!)

面白いサイトを見つけたので訳してみました。

http://www.annotatiomania.com/

2000年に書いたコード

private Collection employees;

2004年に書いたコード

private Collection<Employee> employees;

2005年に書いたコード

// 糞コンパイラが文句を言うため。後で修正する
@SuppressWarnings({"unchecked", "rawtypes"})
private Collection employees;

今日書いたコード

@SuppressWarnings({"unchecked", "rawtypes"})
@Deprecated
@OneToMany(@HowManyDBADoYouNeedToChangeALightBulb)
@OneToManyMore @AnyOne @AnyBody
@YouDoNotTalkAboutOneToMany // Fightclub, LOL
@TweakThisWithThat(
    tweak = {
        @TweakID(name = "id", preferredValue = 1839),
        @TweakID(name = "test", preferredValue = 839),
        @TweakID(name = "test.old", preferredValue = 34),
    },
    inCaseOf = {
        @ConditionalXMLFiltering(run = 5),
    }
)
@ManyToMany @Many @AnnotationsTotallyRock @DeclarativeProgrammingRules @NoMoreExplicitAlgorithms
@Fetch @FetchMany @FetchWithDiscriminator(name = "no_name")
@SeveralAndThenNothing @MaybeThisDoesSomething
@JoinTable(joinColumns = {
    @JoinColumn(name = "customer_id", referencedColumnName = "id")
})
@DoesThisEvenMeanAnything @DoesAnyoneEvenReadThis
@PrefetchJoinWithDiscriminator @JustTrollingYouKnow @LOL
@IfJoiningAvoidHashJoins @ButUseHashJoinsWhenMoreThan(records = 1000)
@XmlDataTransformable @SpringPrefechAdapter
private Collection employees;

明日書くコード

var employees;

私達もオススメします!

@RadCortez, @Annotatiomaniac of the Year 2014

近頃のJavaEEの進歩のおかげで、QW@RTYキーボードを販売するビジネスを始めることができました。

エンタープライズ開発者は2個分の値段で3つ買うことができます!

@Gregor_Riegler, @Annotatiomaniac of the Year 2013

専門家として我々は自分の仕事に責任を持ち、常にベストを尽くす。そこにはミスをする余地はなく、バグの余地もない。

ごく最近、素晴らしい発明によりバグを60%近く減らすことができた。それは @CatchNullPointerException だ。

@MarkusWinand, @Annotatiomaniac of the Year 2012

大企業の顧客が、ウンザリするようなXML形式から洗練されたアノテーション形式へ移行するのを、つい最近支援したところだ。

これは顧客にとって重要な一歩だ ― アノテーションからJSONへ移行するためのね。

@simas_ch, @Annotatiomaniac of the Year 2011

顧客のために、この業界の最新の流行を身につけてもらうためのカリキュラムを用意している。

「制御構造によるレガシープログラミング」および

「モダンプログラミング ー アノテーション、応用アノテーションアノテーションプロセッサプロセッサ、高階アノテーションによるメタプログラミングエンタープライズアノテーション」だ。

アノテーションを使い続けても良いし、

jOOQでアノテーションに振り回されるのを終わりにするのも良い。

…というわけでこのサイトはjOOQの宣伝サイト?なのでした。

Tomcat-8.5.0を試す

Tomcat-8.5.0-betaがリリースされています。

リリースノートによると、

The Apache Tomcat Project is proud to announce the release of version 8.5.0 of Apache Tomcat. Apache Tomcat 8.5.0 is intended to replace 8.0.x and includes new features pulled forward from Tomcat 9.0.x. The minimum Java version and implemented specification versions remain unchanged. The notable changes compared to 8.0.x include:

Added support for HTTP/2, and TLS virtual hosting
Added support for JASPIC 1.1
The BIO connectors, support for Windows Itanium and support for Comet have been removed

Full details of these changes, and all the other changes, are available in the Tomcat 8.5 changelog.
Apache Tomcatプロジェクトはバージョン8.5.0をリリースしたことをお知らせします。バージョン8.5.0は8.0.x系の後継バージョンで、9.0.x系の新機能をバックポートしています。動作するために必要なJavaの最小バージョンは変わっていません。(訳注:Java7が必要)
8.0.xとの差は次の通り。

* HTTP/2およびTLSバーチャルホスティングをサポート
* [JASPIC1.1](http://jaspic-spec.java.net/)のサポート
* BIOコネクタのWindows アイテニアム版およびCometのサポートを削除

詳細な変更点はチェンジログを参照してください。

とのこと。

そして、ChangeLogは次の通り。

The Tomcat 8.5.x branch was created from the Tomcat 9.0.0.M4 tag. Changes were applied to restore Java 7 compatibility and to align the specification APIs with Servlet 3.1, JSP 2.3, EL 3.0, WebSocket 1.1 and JASPIC 1.1.
Tomcat 9.0.0M4をブランチしてTomcat 8.5.xを作った。Java7との互換性を書き戻し、Servlet3.1、JSP2.3、EL3.0、WebSocket1.1、JASPIC1.1の仕様に従うように調整した。

つまりTomcat 8.5.0はTomcat 9.0.0M4だったものを流用しているようですが、その発端はこのメールのようです。

https://marc.info/?l=tomcat-dev&m=145640836214046&w=2

Subject: Tomcat 8.next

This has been hinted at in the past, but is not being discussed anymore.

Possible options:

a) Release a new 8.x branch that would include the connectors from 9 to
support HTTP/2 [OpenSSL now allows realistic support without having to wait
for Java 9], and thus would remove a few legacy items.

b) A more radical option is to use 9 as 8.x but remove the Servlet API
changes. This would force Java 8 and many incompatible changes.

c) Give up on 8.x and instead release 9 as beta, then stable, with an
explicit exception about the Servlet 4 API additions being "preview" until
further notice. That's probably the solution which involves the least
effort by far.

d) Nothing. No 8.x release. 9 will be released sometimes in 2017 when
Servlet 4 is released. The main issue is that there's no HTTP/2 support
until then. The longer we wait, the more a major release will conflict with
the "intuitive" 9 release cycle in 2017.
タイトル: Tomcat8の次をどうするか

以前に話が出たけど、もう議論されてないよね、この件。
選択肢は次の通り。

(選択肢A)
8.xの新しいブランチをリリースし、バージョン9で実装されたHTTP/2コネクタをバックポートする。(今やOpenSSLがあるのでJava9を待たなくても良い)
ついでに古い機能を削除する。

(選択肢B)
8.x系での機能追加を諦めて、9をベータとしてリリースする。Servlet4.0 APIのサポートはプレビュー扱いとしておく。
このプランがおそらく一番簡単である。

(選択肢C)
何もしない。8.xはリリースしない。(訳注: 8.0系を維持するという意味)
9はServlet 4 APIのリリース後にリリースされる。たぶん2017年のどこか。
この選択肢だと、HTTP/2はTomcat9のリリースまでサポートされない。
結構待つことになるし、恒例のリリースサイクル(訳注: Tomcatの通常のメジャーアップデートリリースのこと)と一致しないことになりそう。

起動すると変な警告メッセージが大量に出る

とりあえずインストールして起動してみたところ、

2016-03-29 14:48:42,754 WARN  [Cache] localhost-startStop-1 - Unable to add the resource at [/WEB-INF/classes/applicationContext.xml] to the cache because there was insufficient free space available after evicting expired cache entries - consider increasing the maximum size of the cache

のような警告メッセージが大量に出力され続けます。Tomcat 8.0.x系では見たことのないメッセージです。

おとなしく文言でググってみるとこの記事がヒット。 http://stackoverflow.com/questions/26893297/tomcat-8-throwing-org-apache-catalina-webresources-cache-getresource

詳しくは調べていませんが、どうもTomcatのキャッシュ機構に変更が入ったようです。 まだ各種パラメータの調整が足りていないようですね。まだベータ版なのでこんなものでしょう。

ともかく、この文言が出続けるのはウザったいので消すための設定を調べたところ、 次のように記述したcontext.xmlを以下のいずれかに配置すれば良い模様です。

  • ${CATALINA_BASE}/conf/context.xml
  • ${CATALINA_BASE}/webapps/[context]/META-INF/context.xml

こちらが参考になります。

http://tomcat.apache.org/tomcat-8.0-doc/config/resources.html

<?xml version="1.0" encoding="UTF-8"?>
<Context>
    <Resources cacheMaxSize="1048576" />
</Context>

Eclipseで起動する方法

ちなみに、まだTomcat 8.5用のサーバアダプタは出回っていないので、そのままではEclipse上で起動できません。 catalina.jarの中身を改変する必要があります。詳しくはこちら。

http://takahashikzn.root42.jp/entry/20131208/1386484856

Tomcat-8.0.32上でRhinoの初期化に失敗する問題を強引に解消する

Tomcat-8.0.32がリリースされています。

いつも通り、早速アップデートして試してみたのですがそもそもアプリが起動しなくなりました。 こんなエラーを吐きます。

Caused by: java.lang.StringIndexOutOfBoundsException: String index out of range: 3
    at java.lang.String.charAt(String.java:658)
    at org.apache.catalina.loader.WebappClassLoaderBase.filter(WebappClassLoaderBase.java:2780)
    at org.apache.catalina.loader.WebappClassLoaderBase.loadClass(WebappClassLoaderBase.java:1253)
    at org.apache.catalina.loader.WebappClassLoaderBase.loadClass(WebappClassLoaderBase.java:1142)
    at org.mozilla.javascript.Kit.classOrNull(Kit.java:60)
    at org.mozilla.javascript.NativeJavaPackage.getPkgProperty(NativeJavaPackage.java:129)
    at org.mozilla.javascript.NativeJavaPackage.get(NativeJavaPackage.java:84)
    at org.mozilla.javascript.NativeJavaTopPackage.init(NativeJavaTopPackage.java:96)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.mozilla.javascript.ScriptableObject.buildClassCtor(ScriptableObject.java:1309)
    at org.mozilla.javascript.LazilyLoadedCtor.buildValue0(LazilyLoadedCtor.java:105)
    at org.mozilla.javascript.LazilyLoadedCtor.access$000(LazilyLoadedCtor.java:18)
    at org.mozilla.javascript.LazilyLoadedCtor$1.run(LazilyLoadedCtor.java:90)
    at java.security.AccessController.doPrivileged(Native Method)
    at org.mozilla.javascript.LazilyLoadedCtor.buildValue(LazilyLoadedCtor.java:86)
    at org.mozilla.javascript.LazilyLoadedCtor.init(LazilyLoadedCtor.java:66)
    at org.mozilla.javascript.ScriptableObject.sealObject(ScriptableObject.java:2221)

何事?と思ってorg.apache.catalina.loader.WebappClassLoaderBase.filterのソースを眺めてみるとこんな感じ。

    protected boolean filter(String name, boolean isClassName) {

        if (name == null)
            return false;

        char ch;
        if (name.startsWith("javax")) {
            /* 5 == length("javax") */
            ch = name.charAt(5);
            if (isClassName && ch == '.') {
                /* 6 == length("javax.") */
                if (name.startsWith("servlet.jsp.jstl.", 6)) {
                    return false;
                }
                if (name.startsWith("el.", 6) ||
                    name.startsWith("servlet.", 6) ||
                    name.startsWith("websocket.", 6)) {
                    return true;
                }
            } else if (!isClassName && ch == '/') {
                /* 6 == length("javax/") */
                if (name.startsWith("servlet/jsp/jstl/", 6)) {
                    return false;
                }
                if (name.startsWith("el/", 6) ||
                    name.startsWith("servlet/", 6) ||
                    name.startsWith("websocket/", 6)) {
                    return true;
                }
            }
        } else if (name.startsWith("org")) {
            /* 3 == length("org") */
            ch = name.charAt(3); // ← 2780行目はココ!!
            if (isClassName && ch == '.') {
                /* 4 == length("org.") */
                if (name.startsWith("apache.", 4)) {
                    /* 11 == length("org.apache.") */
                    if (name.startsWith("tomcat.jdbc.", 11)) {
                        return false;
                    }
                    if (name.startsWith("el.", 11) ||
                        name.startsWith("catalina.", 11) ||
                        name.startsWith("jasper.", 11) ||
                        name.startsWith("juli.", 11) ||
                        name.startsWith("tomcat.", 11) ||
                        name.startsWith("naming.", 11) ||
                        name.startsWith("coyote.", 11)) {
                        return true;
                    }
                }
            } else if (!isClassName && ch == '/') {
                /* 4 == length("org/") */
                if (name.startsWith("apache/", 4)) {
                    /* 11 == length("org/apache/") */
                    if (name.startsWith("tomcat/jdbc/", 11)) {
                        return false;
                    }
                    if (name.startsWith("el/", 11) ||
                        name.startsWith("catalina/", 11) ||
                        name.startsWith("jasper/", 11) ||
                        name.startsWith("juli/", 11) ||
                        name.startsWith("tomcat/", 11) ||
                        name.startsWith("naming/", 11) ||
                        name.startsWith("coyote/", 11)) {
                        return true;
                    }
                }
            }
        }
        return false;
    }

どうやら、nameに"org"という文字列が渡されているようです。何だこのクラス名?

Rhinoの初期化ロジックを調べる

Rhinoに問題があるのだろうと思ってソースを見てみたところ、NativeJavaPackage#getPkgPropertyはこんな感じです。

    synchronized Object getPkgProperty(String name, Scriptable start,
                                       boolean createPkg)
    {

        // ...(略) 

        if (shutter == null || shutter.visibleToScripts(className)) {
            Class<?> cl = null;
            if (classLoader != null) {
                cl = Kit.classOrNull(classLoader, className); // ←129行目はココ!!
            } else {
                cl = Kit.classOrNull(className);
            }
            if (cl != null) {
                WrapFactory wrapFactory = cx.getWrapFactory();
                newValue = wrapFactory.wrapJavaClass(cx, getTopLevelScope(this), cl);
                newValue.setPrototype(getPrototype());
            }
        }

        // ...(略) 
    }

で、getPkgPropertyの呼び出し元であるNativeJavaTopPackageはこんな感じです。

public class NativeJavaTopPackage
    extends NativeJavaPackage implements Function, IdFunctionCall
{
    // we know these are packages so we can skip the class check
    // note that this is ok even if the package isn't present.
    private static final String[][] commonPackages = {
            {"java", "lang", "reflect"},
            {"java", "io"},
            {"java", "math"},
            {"java", "net"},
            {"java", "util", "zip"},
            {"java", "text", "resources"},
            {"java", "applet"},
            {"javax", "swing"}
    };

    // ...(略)

    public static void init(Context cx, Scriptable scope, boolean sealed)
    {
        // ...(略)

        // We want to get a real alias, and not a distinct JavaPackage
        // with the same packageName, so that we share classes and top
        // that are underneath.
        String[] topNames = ScriptRuntime.getTopPackageNames();
        NativeJavaPackage[] topPackages = new NativeJavaPackage[topNames.length];
        for (int i=0; i < topNames.length; i++) {
            topPackages[i] = (NativeJavaPackage)top.get(topNames[i], top); // ← 96行目はココ!!
        }

        // ...(略)
    }

    // ...(略)
}

そして、ScriptRuntime#getTopPackageNames()はこれ。

    static String[] getTopPackageNames() {
        // Include "android" top package if running on Android
        return "Dalvik".equals(System.getProperty("java.vm.name")) ?
            new String[] { "java", "javax", "org", "com", "edu", "net", "android" } :
            new String[] { "java", "javax", "org", "com", "edu", "net" };
    }

つまり、NativeJavaPackage#getPkgPropertyの使い方に問題がありそうです。

nameからパッケージ名を導出したいわけですが、

  • 引数nameがFQCNならクラスロードが成功するのでその所属パッケージを取得
  • クラスロードが失敗するならnameはパッケージ名なのでnameをそのまま使う

という手抜きな実装になっています。

Kit#classOrNullはその名前の通り、クラスロード失敗時にnullを返すことが期待されています。 しかしTomcat-8.0.32のクラスローダの仕様が変わったためにIllegalArgumentExceptionではなくStringIndexOutOfBoundsExceptionが発生するようになり、catchで握りつぶせなくなったわけです。

    public static Class<?> classOrNull(ClassLoader loader, String className)
    {
        try {
            return loader.loadClass(className);
        } catch (ClassNotFoundException ex) {
        } catch (SecurityException ex) {
        } catch (LinkageError ex) {
        } catch (IllegalArgumentException e) { // ←クラスロード失敗時にはIllegalArgumentExceptionのみが発生する想定
            // Can be thrown if name has characters that a class name
            // can not contain
        }
        return null;
    }

パッチを当てる

対応方法は色々あると思いますが、僕はお馴染みのJavassistでパッチを当てることにしました。 catch節を追加し、StringIndexOutOfBoundsExceptionを無視するようにします。

final ClassPool classPool = ClassPool.getDefault();
final CtClass kitCls = classPool.get(Kit.class.getName());
final CtClass classLoaderType = classPool.get(ClassLoader.class.getName());
final CtClass stringType = classPool.get(String.class.getName());
final CtClass indexOutOfBoundsType = classPool.get(StringIndexOutOfBoundsException.class.getName());

ctClass //
    .getDeclaredMethod("classOrNull", array(classLoaderType, stringType)) //
    .addCatch("{ return null; }", indexOutOfBoundsType);

ctClass //
    .getDeclaredMethod("classOrNull", array(stringType)) //
    .addCatch("{ return null; }", indexOutOfBoundsType);

// これをクラスパス上のどこかに書き込む
final byte[] patchedClass = ctClass.toBytecode();

これで元のエラーは発生せず、正常に起動するようになりました。めでたしめでたし。

Chrome49でShadow DOMの属性セレクタが効かなくなった

だいぶ前に、日付入力フォームの"年"、"月"、"日"をShadow DOMを用いて消す方法を紹介しました。

http://takahashikzn.root42.jp/entry/20130715/1373865102

要するにこのように指定すればOKです。(※Lessです)

::-webkit-datetime-edit-year-field,
::-webkit-datetime-edit-month-field,
::-webkit-datetime-edit-day-field {
    &:not([aria-valuenow]) {
        color: transparent;
    }
}

しかし本日気がついたのですが、どうやらChrome49からはShadow DOMの要素を選択する際に属性セレクタを使えません。 こんな風になります。(デフォルトの表示)

f:id:takahashikzn:20160201195940p:plain

つまり、上記のセレクタがマッチしていないということ。

::-webkit-datetime-edit-year-field,
::-webkit-datetime-edit-month-field,
::-webkit-datetime-edit-day-field {
    color: transparent;
}

のようにして属性セレクタを消すと有効になるので、属性セレクタが使えないということで間違いないようです。Chromeのバグかな?