Node.js で発生した Hash flooding DoS とその内容について

Node.js のセキュリティアップデート

7/11 に Node.js のセキュリティアップデートがリリースされました。

Security updates for all active release lines, July 2017 | Node.js

これには複数の脆弱性が報告されており、今回はそのうちの1つの Hash flooding DoS という脆弱性が何なのか、それに対して採用された対策が何なのかについてお話します。

Hash flooding DoS (hashdos)

Denial Of Service 、つまりサービス拒否攻撃の一種です。 JavaScript のオブジェクトは内部的にハッシュテーブルとして表現されています。

図はこちらから引用

f:id:yosuke_furukawa:20170715091518p:plain

ハッシュ関数は同じkeyなら同じ値を返しますが、別なkeyなら通常は別な値になります。

f:id:yosuke_furukawa:20170715091946p:plain

ハッシュテーブルのinsert, get, update, removeそれぞれ、通常時は O(1) でアクセスできることが期待されていますが、例外があります。ハッシュ値がぶつかり、 別なkeyにも関わらず、同じ値になった際(コリジョンした際) に、内部的にリストとして扱い、リストに追加させて持つ、という動きをします。ちなみにこれをハッシュの連鎖法と言います。

f:id:yosuke_furukawa:20170715092104p:plain

ハッシュ関数の偏りを狙って意図的に同じハッシュ値を持つ key を発生させる事で、O(1) で取得できていたはずの key の取得が O(n) となり、 複数(n個)のkeyを一度に取得しようとした場合は O(n2) になります。こうなるとアクセスする度に CPU使用率が向上し、結果として DoS になるという攻撃です。

f:id:yosuke_furukawa:20170715102018p:plain

V8 の Hash flooding DoS 対策 について

V8 の hash はこの Hash flooding DoS に関しては既に対策されており、ハッシュ関数の seed 値を乱数化することで、ハッシュ関数の偏りを推測することは困難になるように設計されています。

その対策は Node.js の過去の対応でも対応されていました。

github.com

しかしながら、今回はこの Hash flooding DoS「条件付きで発生してしまう」 という事になりました。

v8-snapshot という機能

Node.js のJavaScriptエンジンである v8 には heap のスナップショットを取るという機能が実装されています。 JavaScript には builtin object として色々なオブジェクトを作成するため、そもそもそのsetupに時間がかかります。このsetup時間を短縮させるために、 heap のスナップショットを build 時に作って serialized した状態で管理し、起動時にはこれを deserialized する改善が行われています。

これにより、起動時間を短縮する効果を産んでいますが、今回はこの機能が結果として hash の乱数化したはずの seed もserializedしてしまい、対策した結果が無効になる、という問題が起きてしまいました。

ソースからビルドすれば、ビルドのタイミングでは乱数化されるので、hashのseed値を推測することは困難になりますが、 Node.js はビルド済みのバイナリを公式で配布しており、この配布されたバイナリの中にはseed値を含んだsnapshotが入っているため、 Hash Flooding DoS が起きうるということで、 high severity vulnerabirity として対策されることになりました。

セキュリティアップデートでは既にbuild時に v8-snapshot を取らないように対策がされています。

どうしたらいいのか

既にパッチが公開されているのでアップデートして下さい。

Node v8.1.4 (Current) | Node.js

Node v6.11.1 (LTS) | Node.js

Node v4.8.4 (Maintenance) | Node.js

v4 以前の方は早めにv4以上にアップデートをおすすめします。

まとめ

  • Hash Flooding DoS (通称hashdos) の紹介
  • V8 の対策とv8-snapshotの話
  • セキュリティ対策

special thanks

今回はConstさんからv8 snapshotの話の解説をいただき記事にしました。

Node v8.0 がリリースされた

Node v8.0 is released!!!!!!

f:id:yosuke_furukawa:20170605234126p:plain

Node v7 から半年経過して次のLTS対象になる可能性が高い Node v8.0 がリリースされました。 いくつか Notable Changes を話そうかなと。ちなみに Node v8 と言うと内部で使っている JS エンジンの V8 と混同されるので、みんな Node8 とか呼んでるときが多いです。このブログの中ではまだ出たばかりとあって、 v8.0 と minor バージョン付きで紹介します。

LTS 候補

v6.x 依頼の一年ぶりの LTS 候補になります。 LTS になるのは 10月以降と予定されています。 Current から LTS になるためにはコアの成熟を待つ必要があり、リリースから半年経過させる予定です。

https://talks.continuation.io/nodeweek-4-17/images/lts.png

Notable Changes

いくつか変更点をピックアップして紹介します。

  • npm5
  • util.promisify
  • V8 5.8
  • async_hooks
  • N-API
  • buffer improves more secure API
  • WHATWG URL is not experimental

npm5

npm5がNode v8.0にバンドルされることになりました。

yosuke-furukawa.hatenablog.com

先日紹介したとおりですね。Node v8.0 ではnpm5がデフォルトになるのでNode8でnpm installとかやるとpackage-lock.jsonがデフォルトで作られます。 package-lock.json 自身はファイルを作った時にリポジトリへコミットすることを推奨されています。 package.jsonpackage-lock.json が同一階層に並ぶのが Node v8.0 の標準プロジェクト構成ということになるでしょう。

util.promisify

これも先日紹介した util.promisify 関数ですね、 Promise をもう少し Node.js フレンドリーに扱えるようになりました。

yosuke-furukawa.hatenablog.com

Promise自身は unhandledRejection の取扱い等で Node で使うには注意点が必要ですが、 PromiseがNodeで使いやすくなったという変更は朗報と言えるでしょう。

V8 5.8

V8 のバージョンが新しくなりました。これに伴いEcmaScript の新機能やエンジンの最適化で下記の機能が追加されました。

function trailing commas

function trailing commas という機能が追加されました。これは関数の定義と呼び出しでケツカンマを許容するというものです。

function clownPuppiesEverywhere(
  param1,
  param2, // 今まではここのカンマが許容されてなかった
) { /* ... */ }

clownPuppiesEverywhere(
  'foo',
  'bar', // 呼び出し時にもケツカンマ OK
);

String padStart/padEnd

文字列の行揃えをするための関数である、 padStart / padEnd が追加されました。

console.log('hello'.padStart(10));  // '     hello'
console.log('hello'.padEnd(10));    // 'hello     '

個人的には leftpad 問題で一気に有名になり、脚光を浴びた機能なので感慨深いですね。

yosuke-furukawa.hatenablog.com

--harmony でフラグ付きで有効になる機能

--harmony フラグありだと下記の機能が有効になります。

template literal revision (–harmony)

ES2015 の template literal ではバックスラッシュ付きの文字列が与えられた際に「特別な役割を持った文字列」として動いてしまうことが多いです。例えば、

  • \uunicode 文字列のエスケープ用の接頭語 例: \u{1F4A4} や \u004B
  • \x は16進数文字列のエスケープ用の接頭語 例: \x4B
  • \ に数字がつくと、8進数文字列のエスケープ用の接頭語 例: \121

として扱われていたりします。 こうなると、以下の文字は tagged template literal では扱えません。

latex`\unicode`
windowsPath`C:\uuu\xxx\111`

これを解決するのが今回の template literal revision です、これにより、 DSL 用途で使いやすくなります。

// --harmony
function tagFunc(tmplObj, substs) {
    return {
        Cooked: tmplObj,
        Raw: tmplObj.raw,
    };
}

tagFunc`\uu ${1} \xx`
// { Cooked: [ undefined, undefined ], Raw: [ '\\uu ', ' \\xx' ] }

Object rest/spread properties (–harmony)

ES2015 の時点では 配列にしか無かった Rest / Spread 演算子が Object でも使えるようになりました。

  • Rest properties
// --harmony
let { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 };
x; // 1
y; // 2
z; // { a: 3, b: 4 }
  • Spread properties
// --harmony
let n = { x, y, ...z };
n; // { x: 1, y: 2, a: 3, b: 4 }

これによって、 Object同士を merge するのには assign を使わなくても、以下のように書けるようになりました。

// --harmony
const merged = {...obj1, ...obj2};
// same: const merged = Object.assign({}, obj1, obj2);

async-await

また、v7.x の時点で既に追加されてましたが、 async-await も有効です。

util.promisify と組み合わせてコアの機能を async-await で書くことができます。

const fs = require('fs');
const { promisify } = require('util');
const readFile = promisify(fs.readFile);

const read = async (path) => {
  const result = await readFile(path);
  console.log(result.toString());
};

read('test.txt');

V8 Turbofan and Ignition

V8 に新しく Ignition と呼ばれるインタプリターと Turbofan と呼ばれる新しいJIT Optimizerが追加されました。

ただし、これらもまだデフォルトでは on になっていません。試したい場合は --ignition--turbo を付ける必要があります。

これまでは Crankshaft と呼ばれるJIT エンジンだけでしたが、 Ignition と呼ばれるインタプリターが追加されたことでメモリフットプリントの軽量化とJITの高速化が図られる予定です。

docs.google.com

v8project.blogspot.jp

こちらちなみに jsconf.eu に行った時にV8チームの人達から話を聞いた所、「モバイルでのページ参照の時に低スペックでも高速に動くように検討された結果生まれた」という話を聞きました。 Node では IoT などの環境で動く際には役立つ可能性が高いです。

この話はもう少し詳しく別なエントリで語ります。

async_hooks

Node.js 内部のtraceやdiagnosticに使うための新しいAPIである async_hooks が追加されました。 async_hooks はNode.js 内のイベントループで起きているイベントを監視できるようにするための API です。 まだ、 API 一覧に内容は書かれていませんが、以下の Pull Request には上がっているのでそこから抜粋します。

node/async_hooks.md at 5d7a544bd19d46f24f58c20a4530334ea9ada64d · thlorenz/node · GitHub

以下のコードは async_hooks を使って Node.js 内部のオブジェクトとコールバックの発火有無を監視するためのものです。

const async_hooks = require('async_hooks');
const net = require('net');

let ws = 0;
async_hooks.createHook({
  // Node.js の内部オブジェクトが初期化される際に実行される関数
  // この時点ではまだ実体は存在せず、リソースは確保されていない
  init (id, type, triggerId) {
    const cId = async_hooks.currentId();
    process._rawDebug(' '.repeat(ws) +
                      `${type}(${id}): trigger: ${triggerId} scope: ${cId}`);
  },
  // コールバックが実行される前に呼び出される関数
  before (id) {
    process._rawDebug(' '.repeat(ws) + 'before: ', id);
    ws += 2;
  },
  // コールバックが実行された後に呼び出される関数
  after (id) {
    ws -= 2;
    process._rawDebug(' '.repeat(ws) + 'after:  ', id);
  },
  // 実体が削除された時に呼び出される関数
  destroy (id) {
    process._rawDebug(' '.repeat(ws) + 'destroy:', id);
  },
}).enable();

net.createServer(() => {}).listen(8080, () => {
  // 10ms 待ってから async_hooks を使って今のEventのIDを表示する。
  setTimeout(() => {
    console.log('>>>', async_hooks.currentId());
  }, 10);
});

これを実行すると下記のようなログが出力されます。

TCPWRAP(2): trigger: 1 scope: 1
TickObject(3): trigger: 2 scope: 1
before:  3
  Timeout(4): trigger: 3 scope: 3
  TIMERWRAP(5): trigger: 3 scope: 3
after:   3
destroy: 3
before:  5
  before:  4
    TTYWRAP(6): trigger: 4 scope: 4
    SIGNALWRAP(7): trigger: 4 scope: 4
    TTYWRAP(8): trigger: 4 scope: 4
>>> 4
    TickObject(9): trigger: 4 scope: 4
  after:   4
after:   5
before:  9
after:   9
destroy: 4
destroy: 9
destroy: 5

TCPWRAP や Timer などのNode.js の内部オブジェクトが実行されてる様子が見て取れるかと思います。 これをうまく使えばエラーのデバッグに役立てる情報を出力できます。ログを毎回出したくない場合は enable/disable メソッドを使ってhookをオンオフさせることも可能です。

const async_hooks = require('async_hooks');
const net = require('net');

let ws = 0;
const hook = async_hooks.createHook({
  init (id, type, triggerId) {
    const cId = async_hooks.currentId();
    process._rawDebug(' '.repeat(ws) +
                      `${type}(${id}): trigger: ${triggerId} scope: ${cId}`);
  },
  before (id) {
    process._rawDebug(' '.repeat(ws) + 'before: ', id);
  },
  after (id) {
    process._rawDebug(' '.repeat(ws) + 'after:  ', id);
  },
  destroy (id) {
    process._rawDebug(' '.repeat(ws) + 'destroy:', id);
  },
});

net.createServer(() => {}).listen(8080, () => {
  setTimeout(() => {
   // ここで改めてログを出す。
    hook.enable();
    console.log('>>>', async_hooks.currentId());
  }, 10);
});

N-API

N-API は新しく Native Addon 用の機能を提供するラッパーAPIです。これまでの Node.js ではコアでサポートしているラッパーAPIは存在せず、NANと呼ばれる ライブラリがサポートされていました。

これを利用して、 node-sass や leveldown や imagemagick-native といった Native Addon のライブラリが提供されています。今回からは NAN だけではなく、 N-API というコアが新しくサポートした機能を使って Native Addon を書くことが可能です。

詳しくはここの資料を見ると良いでしょう。

medium.com

また既に N-API を使った leveldown を使って Native Addon を活用するためのデモも存在します。

github.com

buffer improves more secure API

Buffer の API を実行する際に必ず初期ヒープ領域が 0 埋めされるようになりました。 この変更の重要さを語るためにはまず、 Buffer(number) を実行したときの問題点から語る必要があります。

Buffer をコンストラクトするには Buffer(number) のように数字を渡す方法と Buffer(string) などの文字列を渡す方法、配列を渡したり、 TypedArray を渡す方法などの複数の種類がありました。僕がよく使うのは new Buffer(string, encoding) で文字列をエンコードする時でしょうか。 new Buffer(number) を使うとその number に渡したサイズ分のバッファを事前に確保して使うことが可能です。

しかしながら、 以前の new Buffer(number) は初期化段階ではヒープメモリ中の中身を 0 埋めしていません。つまりヒープメモリ中の値の中身を見ようと思えば見れてしまいます。

var token = 'paSsWord!ASD, totally secret!';
for (var step = 0; step < 100000; step++) {
    // ここで token の中身を取れる
    var buf = (new Buffer(200)).toString('ascii');
    if (buf.indexOf(token) !== -1) {
        console.log('Found at step ' + step + ': ' + buf);
    }
}

github.com

昔の ws という WebSocket のためのモジュールがこの問題をもろに引き起こし、 攻撃を引き起こす可能性があるという脆弱性を持っていました。

https://nodesecurity.io/advisories/67

この問題に対処するため、 new Buffer(number) を呼び出す際、初期値を Buffer.fill(0) として0埋めされたバッファとして確保してしまう対応がなされました。

これにはパフォーマンスの問題が伴う可能性があります。つまり、必ずサイズ分の全ての値を 0 埋めする結果、パフォーマンス的に遅延してしまう可能性も考えられます。

セキュリティよりもパフォーマンスを優先させたい場合は専用の Buffer.allocUnsafe メソッドを使って実行する必要があります。

// 必ず最初に 0 で初期化され、安全にメモリアロケートされる
const safeBuffer1 = Buffer.alloc(10);
const safeBuffer2 = new Buffer(10);

// 初期化されずにいきなりアロケートされる
const unsafeBuffer = Buffer.allocUnsafe(10);

それに伴い、 Buffer(number) での呼び出しは pending deprecated 扱いになってます。今後はエラーになる可能性もあるので、 Buffer.allocBuffer.allocUnsafe に移行できるようにしておいてください。

WHATWG URL is not experimental

WHATWG URL は v7 で追加された新しい URL Parser ですが、それの扱いが格上げされ、 stable な API として扱われることになりました。

const URL = require('url').URL;

const myUrl = new URL('/a/path', 'https://example.org/');

v7 の初期までは URLSearchParams などにも対応していませんでしたが、それらのすべてのspecに対応されました。

そのため、以下のコードも正常に動作するようになりました。

const URL = require('url').URL;

const u = new URL('https://jxck.io?log=warn&lang=ja');
const searchParams = u.searchParams;
searchParams.get('log') // "warn"
searchParams.getAll('log') // ["warn"]
searchParams.delete('log') // undefined
searchParams.has('log') // false
searchParams.append('debug', true) // undefined
searchParams.toString() // "lang=ja&debug=true"

for ([k, v] of searchParams) {
  console.log(k, v);
  // lang ja
  // debug true
}

以下より抜粋

blog.jxck.io

他にも

と盛りだくさんです。すべてをちゃんと紹介しきれる気がしないので、いくつか抜粋しましたが、ちゃんと変更点を追いたい方は、以下の記事をご一読ください。

Node v8.0.0 (Current) | Node.js

まとめ

Node.js v8.0 が日本時間の 6/1 朝に公開されました。今回はv8.0の主だった機能を紹介しました。一週間経過して、今のところ問題も散見されておりますが、概ね動作しているように見えます。

ちなみに今週中に v8.1 が出るようなのでその時また少し変わるかもしれません。

ひとまず、自分の目の届くプロダクトでは Node v8.0 を試すことにして、実績を積み、問題があったらフィードバックする形にしていこうと思っています。

この他にも V8 の変更であったり、 N-API であったりは実は別枠で語らないといけないような大きな変更なので、随時紹介して行こうと思います。

npm v5 がリリースされた

npm v5

f:id:yosuke_furukawa:20170530032206p:plain

The npm Blog — v5.0.0

npm に v5 がやっとリリースされました。この npm v5 は既に明日リリース予定の Node v8 にバンドルされる予定です。 かいつまんで、機能を紹介します。

Notable Changes

  • package-lock.json!!!
  • faster than npm v4
  • no more --save option
  • Offline mode
  • sha512 support

package-lock.json!!!

npm v4 まで問題だった npm-shrinkwrap の問題 を解消するための新しい lock ファイルが生まれました。

shrinkwrap は依存ライブラリを固定するための機能です。npm v4 までは shrinkwrap で固定していましたが、新しく npm v5 になってからはshrinkwrap は不要です。

shrinkwrap は現在の自分の node_modules フォルダ以下にある情報を元に shrinkwrap.json ファイルを作成します。 単純に今の自分の依存モジュールのスナップショットとして作るだけならいいのですが、実際には環境の差異で開発中にしかいらないモジュール(devDependencies)やOSX環境ではインストールできたけど、Linux環境ではインストールできないモジュール(optionalDependencies)などがあり、きちんと環境に合わせてshrinkwrapを構築する必要があります。これを回避するために色々 hack していましたが、今回の変更でそれらは全て不要になります。

package-lock.json は package.json に何か変更があったらそれと完全にsyncしてファイルが変更されます。 要は npm installnpm updatenpm uninstall などをした場合は毎回 package-lock.json にも同様の変更が行われます。

facebookのメンバーが作った yarn と同じ動きですね。

もしもこの動きを止めたければ --no-save オプションが追加されているのでそれを付けると package-lock.json には反映されなくなります。 --no-save を付ける時はちょっとだけ試してみたい時ですね、基本は何も付けずに npm install foobar とやるだけで lock ファイルもpackage.json も一緒に更新されていきます。

faster than npm v4

npm v4 よりも高速化されました。試しに手元で 適当なモジュール を使った所、下記のような結果になりました。

  • npm4 3.14sec
  • npm5 1.52sec
  • yarn 0.78sec
  • pnpm 0.75sec

手元では pnpm > yarn > npm v5 > npm v4 の順で速いのが観測されました。ただ正直 pnpm と yarn の差は誤差の範囲内です。 高速にはなりましたが、 npm5 と yarn, pnpm はまだ yarn, pnpmのが高速です。

こちらの資料にはもう少し詳細なデータが載っています。

docs.google.com

no more --save option

npm install でモジュールをインストールする際に --save オプションがデフォルトで付くようになりました。つまり、何らかのモジュールをインストールした場合デフォルトで package.json に変更が入ります。

--save-dev もしくは -D オプションを付けてインストールすれば devDependencies に入ります。--save-optional もしくは -O をつければ optionalDependencies に入ります。

これらモジュールをインストールした時には必ず package.json とともに package-lock.json にも変更が入ります。

offline mode

npm install --prefer-offlinenpm install --offline などのモードが追加されました。

--prefer-offline をつけると、npmのcacheがstaleしているかどうかを確認するためのrequestを発行しなくなり、今持っているローカルキャッシュを優先して実行するようになります。無かったら npm リポジトリに取りに行きます。

--offline をつけるとローカルキャッシュからしか取りません。もしもローカルキャッシュに見つからなかったらエラーになって終わりです。

この offline mode自身は「高速化というよりもnetwork 利用率を下げるために使っているオプションで、直接速度に影響するものではない」とのことを npm の中にいる Kat Marchan から教えてもらいました。

sha512 support

これまで sha-1 でハッシュを計算していましたが、 sha-1 はcollision の懸念もあるためよりcollisionが起きにくいsha512もサポートされることになりました。

現時点では、 sha512 と sha1 両方で併用される形になるようです。 sha512 だけにすると既存モジュール全てハッシュ値を再計算する必要があるからだと想定されます。なので既存のsha1も併用する形になるんじゃないかと。ただ npm v5 以降からは sha512 のフィールドに値が入っていれば優先的にチェックされます。

この他にも

cache の機能がいくつか deprecated になっていたり、 package.json や package-lock.json がデフォルトでインデントがされるように改修が入っています。

まとめ

npm v5 のリリースに関してざっくり紹介しました。 yarn や pnpm の速度は魅力的ですが、 npm v5 は Node v8 にバンドルされて標準的なツールとして組み込まれる事になるでしょう。まだ v5.0.0 が出たばかりですが、今のうちからでも npm i npm@5 -g で入れて試してみるのも悪くないでしょう。

util.promisify が追加された

Node.js のコアに util.promisify が追加された。 github.com

今回は util.promisify が持つ役割を中心に Node.js における Promise の立場についても話していけるといいと思う。

util.promisify とは

読んで字のごとく関数を Promise に変換してくれるユーティリティメソッド。 下記のような要領で変換できる。

const util = require('util');
const fs = require('fs');

const stat = util.promisify(fs.stat);
stat('.').then((stats) => {
  console.log(stats);
}).catch((error) => {
  console.error(error);
});

async-awaitを使いたい場合(Node.js v7の最新では既にenabled)は下記の通り

const util = require('util');
const fs = require('fs');

const stat = util.promisify(fs.stat);

async function callStat() {
  try {
    const stats = await stat('.');
    console.log(stats.uid);
  } catch(e) {
    console.error(e);
  }
}

util.promisify 注意事項

Node.js のコアメソッドに限らず、他のメソッドもPromiseに変換できる、ただし、変換する場合はその関数がpromisifyの規約に従っている必要がある。

その規約というのは、

  • コールバック関数を引数の最後に取ること ( function(arg1, arg2, cb){} )
  • コールバック関数の最初の引数はエラーであること ( cb(err, res) )

である、これに従わない関数の場合はうまくPromisifyされないので要注意、特に2つ目の規約に違反しているとエラーじゃないものがPromise.reject の対象になってしまうことがある。

Node.js のコアメソッドのほとんどは上記の規約に従ったコールバック関数を取るが、Node.jsの規約から外れたコールバックの使い方をしているメソッドの場合は一工夫が必要になる。

例えば、 setTimeoutsetImmediate の場合がそうなる、これらはコールバック関数を最初の引数に要求するし、コールバック関数の最初の引数はエラーとは限らない。

こういった関数を Promise に変換したい場合は util.promisify.custom をプロパティにしてカスタマイズされたPromisify関数を提供してあげる必要がある。

const test = function(cb, arg){ cb(arg) }
test[util.promisify.custom] = (arg) => { return new Promise((resolve, reject) => { test(resolve, arg); }) };

const p = util.promisify(test);
p('foo').then((arg) => console.log(arg));

Node.js の setTimeoutsetImmediate はこの util.promisify.custom を使ってcustomizeされたPromisifyを作っている。

Node.js における Promise の位置付け

フロントエンドでは async await が採用されたり、 Web 標準のAPIが採用していたりと、ほぼスタンダードな印象を受ける Promise だが、 Node.js の中では実はまだまだ議論の余地がある。

util.promisify を採用するかどうかを議論していた時にその場に居たので、要点をまとめると

unhandledRejectionの時の振る舞いが決まっていない

という一点につきる。

議論で話している感じは以下の通り:

  • Promise のニーズは高い、特に async await のような構文サポートまであるので強力
  • しかしながら、Promise の unhandledRejection が起きた時にNodeのデフォルトをどう動かすようにするかが未定
  • 現時点では warnings が出る。しかし、今やってる unhandledRejection はただ単に例外発生時に .catch をする Promise が その時点で いなかっただけであり、Promiseが例外をキャッチするのは仕様上いつでも良いので、この時点で出るwarningsとしては適切ではない(非同期にキャッチされる可能性があるため)。
  • 現在の仕様で Promies を使ったとして、例外をキャッチしなかった際に容易にメモリリークやファイルディスクリプタのリークが起きることは想像しやすく、やはりNodeコアの中でも簡単にリークが作り込めてしまうような状況にするべきではない、リークが気づきにくくなる位なら異常終了した方がマシ

というのが議論ポイントだった。

もう少し噛み砕くと、『 Promise を簡単に使えるようにする(util.promisifyを提供する)なら、 Promise を安全に使える手段として提供してあげる(リークを起こさないようにする)べき』という感じだろうか。

これに対しては現時点で提案中のデフォルトの unhandledRejection の動きで既に3,4候補存在する。

unhandledRejection が起きたら:

まだこの部分の議論は続いている。

Node.js Collaborators Summitにおける、約一時間の議論は発散して終わった感じがするが、 util.promisify を追加する件に関してはある程度の有用性、コアでやることの意義が認められてマージされた。

Promise とどう付き合っていくか

僕らアプリケーションをNode.jsで書いている側としては気をつけるべきなのは、 Promise が使いやすくなってきているが、まだまだ運用面での知見が少ないという点だと思う。

もしかしたらリークが起きてるけど気づいていないとか、例外がスローされていたけどそのまま放置されていたとかそういう事がないように Promise は気をつけて使うべきだろう。

実際に Node.js v4 では Promise でメモリリークが起きていた(現在は修正済み)

更に言うと、現時点の Promise には core-dump を出す仕組みもない(processが死んだ時の --abort-on-uncaught-exception 相当)。Promiseを使ってしまうとエラーになって死んだ時に解析がしにくいという側面もある。

www.joyent.com

util.promisifyができたことで、Promiseが使いやすくなっているが、この辺りはまだ仕様検討中なのでv8.0次第ではPromiseの使い勝手は変わる可能性もある。

2017/05/12 追記: Promiseを無限ループさせるとリークが起きるというのは仕様の問題であって、Node.jsの問題ではありませんでした。

まとめ

  • util.promisify の説明
  • Promise と Node の位置付け
  • Promise とどう付き合っていくか

node の security checkをするなら nsp が便利

nspとは

先日たまたま会社で Vulnerability の話になって色々と Node.js だとこういうのあるんですよって言ったら知らなかった方も多かったので紹介。 nsp は node security platform の頭文字を取ったプロジェクトである。

Node Security Platform はサイト上で脆弱性を公開している。 Node.js のコアの脆弱性というよりも npm モジュールなどのモジュールの脆弱性だ。

nsp に挙げられてる脆弱性の一例

例えばこの脆弱性なんかは2017年2月11日に公開された脆弱性である。

https://nodesecurity.io/advisories/313

github.com

どういう脆弱性かというと、このモジュールはJavaScript Objectをシリアライズするためのモジュールだが、そのserializeする時に関数までも変換してくれる、JSONよりも少しだけやってることが複雑である。問題はdeserializeする時で、deserializeする時はnew Function 等で括って eval として関数を実行している、こうすると不正な即時関数 {e: (function(){ eval('console.log("exploited")') })() } をserializeしたオブジェクトが渡された場合にdeserializeした側の環境で勝手に実行されてしまう。この例題コードはconsole.logだから良いが、child_processのexecFileやらなんやらがサーバで実行されたら目も当てられない。

さて、この手の脆弱性は実は週単位のペースで上っている。これをいちいちチェックしてたらキリがない。ツールで自動化させようというのがこの nsp である。

nsp 使い方

インストールはとりあえず簡単。

$ npm install nsp -g

別にローカルモジュールに入れて npm run security とかでチェックできるようにしても良い。

$ nsp check

(+) 2 vulnerabilities found
┌───────────────┬─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│               │ Code Execution Through IIFE                                                                                                                                                     │
├───────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ Name          │ serialize-to-js                                                                                                                                                                 │
├───────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ Installed     │ 0.5.0                                                                                                                                                                           │
├───────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ Vulnerable    │ <=0.5.0                                                                                                                                                                         │
├───────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ Patched       │ >=1.0.0                                                                                                                                                                         │
├───────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ Path          │ server-timing@1.1.0 > serialize-to-js@0.5.0                                                                                                                                     │
├───────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ More Info     │ https://nodesecurity.io/advisories/313                                                                                                                                          │
└───────────────┴─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
┌───────────────┬─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│               │ Regular Expression Denial of Service                                                                                                                                            │
├───────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ Name          │ ms                                                                                                                                                                              │
├───────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ Installed     │ 0.7.0                                                                                                                                                                           │
├───────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ Vulnerable    │ <=0.7.0                                                                                                                                                                         │
├───────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ Patched       │ >0.7.0                                                                                                                                                                          │
├───────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ Path          │ server-timing@1.1.0 > ms@0.7.0                                                                                                                                                  │
├───────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ More Info     │ https://nodesecurity.io/advisories/46                                                                                                                                           │
└───────────────┴─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

これだけ。これだけでshrinkwrap もしくは package.json の定義を眺めて脆弱性のあるモジュールがないかを Node Security Platform の API に投げて確認してくれる。

もう少し凝った使い方をしたければ、 .nsprc に 例外ルールを追加して無視することも可能。

{
  "exceptions": ["https://nodesecurity.io/advisories/12"]
}

もちろんこれだけで脆弱性が防げるわけではない。これは自分の依存モジュールに脆弱性が報告されていないことを見つけるための道具でしか無い。

自分のサイトの脆弱性や自分のモジュールがうっかりSQL Injectionしていたなんて事にならないようにしたい。

ちなみに yarn 対応なんかはまだの様子。

github.com

Demystifying webpack2 tree shaking

webpack2 に最近移行しました。

その時の知見とせっかくなので tree shaking が実際に中でやってることを追ってみたので紹介。

webpack2 移行時の注意

基本的にはほぼここに書いてあるとおり。

Migrating from v1 to v2

かいつまんで説明すると、configファイルの書き方がガラッと変わって、 module.loadersmodule.rules になったり、 resolve.root がなくなって resolve.modules に変わったり。この辺の書き換えは割りとすんなりいくはず。

辛いのはpostcss周りのオプションの渡し方辺り。これまではconfigのrootにpostcssプロパティを用意してそこに記述できたが、その記述はできなくなり、 webpack.LoaderOptionsPlugin 経由で渡すか postcss.config.js というファイルを作ってそこに渡す必要がある。どちらでも構わないが、 postcss.config.js で渡す方法が postcss-loader の issue でオススメされていたのでそれを採用することにした。

※コメントで教えてもらったが、 .postcssrc でやる手段もある様子。

また、 ExtractTextWebpack という Plugin がまだ v2 では beta 版という位置づけで、割りとオプションの渡し方周りが定まりきっていないので注意。 ハマったので issue とにらめっこしながらコード読みながら進めるとこのスレで解説されていたのでその通りやると良い。

github.com

webpack2 が出たからと言ってまだローダー周り、プラグイン周りが若干stableじゃないことに注意した上で移行すると良いだろう。

Tree Shakingとは

Rollup が言い始めたのか、出自を辿るとRich Harris が語るRollupの話が出てきた。 要は木を枝刈りするという意味。もうすこしかいつまむと、使っていないライブラリを枝刈りして削り、小さくしてbundleすることを指す。

webpack2 におけるおそらくはメイン機能の1つであり、webpack2 にする人はだいたい tree shaking までやる傾向にある。

webpack2 の Tree Shaking を試すのは簡単で、 babel での module トランスパイルを辞めれば良い。

{
  "presets": [
    "react", 
    ["es2015", {"modules": false}] // modules を false にする。
  ]
}

こうすると webpack2 では import/export 構文をそのまま扱えるようになる。これを使って、 export されてるけど、 import されていないものを見つけて、それだけはbundleしないという方法を取る。

実際にやってみると多少の効果はあり、このような結果になった。

Demystifying webpack2 tree shaking

じゃあ中で何をやってるんだろう、ということで中身を追ってみた*1

webpack2 は実際 export されているファイルはほぼそのまま展開する。ただし、 export されているが、 import されていない関数や変数、クラスに関しては実際には export する時に common js として export しない。

例を挙げる、下記のようなファイルが存在するとする。

// main.js
import { sum } from './math';

sum(1,2);
// math.js

export function sum(a, b){
  return a + b;
}

export function sub(a, b){
  return a - b;
}

export function mul(a, b){
  return a * b;
}

ここで、 webpack2 の変換をかけると下記のようになる。

"use strict";
/* harmony export sum */ 
exports["sum"] = sum;

/* unused harmony export sub */
/* unused harmony export mul */

function sum(a, b) { return a + b };
function sub(a, b) { return a - b };
function mul(a, b) {return a * b};

見てもらうと分かるが、関数はそのまま展開されているのがわかると思う。ただし、 exports オブジェクトには submul といった関数をマッピングしていない。その代わり「コメントでunusedなので export しなかった」というのがわかるようになっている。

このままだと、特にtree shakingの旨味はない。コメントがある分、むしろファイルサイズとしては増える可能性もある。

tree shaking しても何もおきないのかと思うのは時期尚早。ここで production 用のビルドをして、 UglifyJS の力を借りると下記のようになる。

a["sum"] = function(a,b){return a+b};

実は UglifyJS には unused な関数や変数を消してくれる、という機能が備わっている。

GitHub - mishoo/UglifyJS2: JavaScript parser / mangler / compressor / beautifier toolkit

この機能をwebpackは内部的に使うことで tree shaking を実現している、上の例で言うなら、 submul といった関数はそのまま出ていたとしても、結局 exports オブジェクトにマッピングされていないため、参照がなくなり、誰からも使われていない事になる。

これによって UglifyJS2 がunusedなものとして、削ることができるというわけだ。

ちなみに babel の場合、ファイル単位での変換を基本とするため、『export されているが import されていない』という情報を持たずに transpile している。これによって上述したような『この関数や変数だけは common js にマッピングしないでおこう』というような処理ができない。ただし、すごく単純な仕組みなのでいつか実装される可能性もある。

これまでtranspileされてcommonjs になるだけで特に強い意味は無かった ES6 modules 形式での記法だが、このような形で メリットが享受されるようになるなら書いてもいいのかもしれない。

まとめ

  • webpack2 移行の注意
  • Tree Shaking とは
  • Demystifying Webpack2 Tree Shaking

参考

webpack/examples/harmony-unused at master · webpack/webpack · GitHub Tree-shaking with webpack 2 and Babel 6

*1:というのも、実際に削除されてるのか追ってる内にへ~と思う発見があっただけ

ソフトウェア例え話、格言、小噺

2016年になってから色んなソフトウェアエンジニアの人と話してきて、その中で3人から聞いた例え話、格言、小噺が面白かったので、僕の中だけで留めておかずに開放しておく。

息継ぎをするには『まず息を吐く』という例え話

水泳で息継ぎをするなら『まず息を吐きなさい』と教わるらしい。これは息を吐かずにどこかで息を貯めてしまうと、ちゃんと息を吸えないという事を意味してる。息を吐くと苦しくなって顔は絶対に水面に出る。

これと同じことがソフトウェアの学習にも言える。

つまりまずアウトプットする、なんでも良い。作ったものをGitHubに公開するとか、発表するとか、ブログやQiitaに書くとか。ちゃんとアウトプットしたものはフィードバックがあり、そのフィードバックを受ける(PRやissue, 質問, マサカリ etc)、どんどん吐き出していくと吸わないとネタがなくなるので、吸い込むためにまたインプットする。

同じような話として、教えることで勉強するという学習法がある。

自分が誰かに教える役になるというのは実は一定の知識がないとできない。

アウトプットする、という最初の一歩は躊躇しがちかもしれないが、アウトプットすればするほど次のインプットになり、良い効果が得られる。

ちなみにこの例え話は及川卓也さんとの対談で語っていただきました(ちなみに下の記事じゃなくて多分別な記事になるはず)。

logmi.jp

プロジェクトを失敗させる方法

プロジェクトを絶対に失敗させる方法というのが1つある、関係者をひとつにまとめずにバラバラの部屋に分けること。

ソフトウェア開発プロジェクトでも考えると1つの部屋で広い部屋を借りるか複数の狭い部屋を借りて部屋を分けるのとどっちが都合が良いか、というと圧倒的に前者。

まず、一つの部屋にまとまってると直接顔を見てコミュニケーションがしやすい。部屋が分かれてるとそれだけでコミュニケーションコストがかかる。SlackやIRCなどでコミュニケーションはできるとはいえ、やっぱり表情を見ながらホワイトボードに書きながらの話ができるのとは少し違う。

さらに言うと、プロジェクトには『偶発的なコミュニケーション』が重要になる。偶発的なコミュニケーションというのは、たまたま聞こえてきた議論の内容だったり、そのへんに書かれてたホワイトボードの走り書きとかが見えるとか、そういう所から始まる突発的なコミュニケーションの事を指す。

よくタバコ部屋での会話がきっかけで仕事の話が回るとかいう話は出るが、それと少し似ている。要は偶発的なコミュニケーションが起こりやすい環境でトークするというのはきちんとした会議で決まることよりも重要な事がある。

さて、これを振り返ると、この偶発的なコミュニケーションというのは色んな所で実は応用できる。

例えばslackでの分報システムもそうだと思う。リモートというのを逆手に取って敢えて自分が今やってること、詰まってることを積極的に共有する仕組みで、偶発的なコミュニケーションを引き出そうとする。

c16e.com

もう少し話をすすめるとGitHubかなんかのリポジトリもチームやら言語やらで分けるよりもプロジェクトが1つのリポジトリでやった方が偶発的なコミュニケーションが生まれやすい。コードが見えるだけじゃなく、PRも見えるし、issueも見える。

実はgoogleなんかは1つの巨大なリポジトリでプロジェクトを管理してることが多いらしい。

Google の巨大レポジトリとブランチ無し運用 - Kato Kazuyoshi

ちなみにこの小噺はt-wadaさんとお昼を一緒になった時に教えてもらいました。

正しいものを正しく作る

正しさには厳密には2種類ある。 validation verification の2種類。

validation は語源を辿ると、value になる。つまり「価値があるかどうか」という意味。ソフトウェア開発で言うと、何かしらの数字に反映される事がvalidationを満たしていることになる。分かりやすく言えば売上があがる、コストが下がるとか。

verification は語源をたどると、veryになる。veryは「とても」と訳される事が多いが「まさしく」という意味。つまり、「まさしく在るべき姿」であるかどうかという意味。分かりやすく言えばテスト書いてるかどうかとかコードの設計が良いとか、直接的に数字に反映されるようなものじゃなく、プロジェクトとしてあるべき姿になっているかどうか。

どっちの正しさを満たすべきっていう話じゃなくて、両方満たすのが一番正しい。(ただこの手の締め切りを優先させるべきかテストを書いたほうが良いか等の話が出る度にどっちかに振り切った話が多い気がする。実際はどっちかだけ満たしても正しくない。)

確かに両方満たそうとしても実際はなんだかんだで取捨選択を迫られることは多い。もちろんその時はプロジェクトの状況や内容に応じて決めれば良い。繰り返しになるがどっちかを取れば良いというものじゃなくて、両方満たすのが圧倒的に正しい。

f:id:yosuke_furukawa:20161231225632p:plain

もちろん両方満たすのなんて理想だと思う。ただし、両方満たせないのだとした時に今度はそれを埋められるように研鑽を積むべきだし、チームとしてどうあるべきかを振り返っていく必要がある。要は理想だからといって諦めるんじゃなくて両方満たせるようになろうよ技術者なら。という事を説いていきたい。

ちなみにこの格言は弊社の隣で一緒に開発している。 koichik から教えてもらいました。

来年もよろしくお願いします。皆様。