<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Zeng Jinji</title>
    <description>The latest articles on DEV Community by Zeng Jinji (@zengjinji).</description>
    <link>https://dev.to/zengjinji</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4229972%2Fbfe4bb56-58d2-4abd-be7e-bc674e2349c8.jpg</url>
      <title>DEV Community: Zeng Jinji</title>
      <link>https://dev.to/zengjinji</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/zengjinji"/>
    <language>en</language>
    <item>
      <title>An OpenResty Worker Stuck in an Infinite Loop: How We Found a Missing LuaJIT ARM64 Instruction</title>
      <dc:creator>Zeng Jinji</dc:creator>
      <pubDate>Fri, 06 Nov 2026 09:34:03 +0000</pubDate>
      <link>https://dev.to/zengjinji/an-openresty-worker-stuck-in-an-infinite-loop-how-we-found-a-missing-luajit-arm64-instruction-8b3</link>
      <guid>https://dev.to/zengjinji/an-openresty-worker-stuck-in-an-infinite-loop-how-we-found-a-missing-luajit-arm64-instruction-8b3</guid>
      <description>&lt;p&gt;Our gateway runs on OpenResty, with most of its business logic written in Lua. OpenResty uses LuaJIT underneath. In JIT mode, LuaJIT compiles Lua code into machine instructions that the CPU can execute directly to improve performance. In everyday development, we usually focus on whether the Lua logic is correct. This time, however, the problem was in &lt;strong&gt;the machine code generated by the JIT compiler&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Symptoms in production
&lt;/h2&gt;

&lt;p&gt;Our production fleet originally consisted entirely of x86 machines, and everything worked normally. After introducing ARM machines, we began encountering a series of LuaJIT-related problems on those machines.&lt;/p&gt;

&lt;p&gt;The symptoms appeared &lt;strong&gt;only on ARM machines&lt;/strong&gt;, which made us suspect an issue in LuaJIT's ARM implementation. Eventually, we disabled JIT compilation on several ARM machines and observed that the same kind of infinite loop no longer occurred. This gradually shifted our investigation from business logic toward LuaJIT. At that point, though, we still did not know which compilation stage was responsible, and we had no reliable way to reproduce the problem.&lt;/p&gt;

&lt;h3&gt;
  
  
  1.1 Symptom one: a single worker stays at 100% CPU
&lt;/h3&gt;

&lt;p&gt;Occasionally, an nginx worker on an ARM machine would remain at 100% CPU for a long time.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We attached GDB to the process and used &lt;a href="https://github.com/openresty/openresty-gdb-utils" rel="noopener noreferrer"&gt;openresty-gdb-utils&lt;/a&gt; to inspect it. The process was stuck in a &lt;code&gt;for&lt;/code&gt; loop. The location of the infinite loop varied from one incident to another.&lt;/li&gt;
&lt;li&gt;Each machine had 40 workers, but only one or two would be affected. The timing was unpredictable: sometimes once in seven days, sometimes several times in one day.&lt;/li&gt;
&lt;li&gt;Some affected workers also showed continuous memory growth. Their loops allocated memory repeatedly until the machine ran out of memory and triggered an OOM condition.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One incident occurred in &lt;code&gt;captcha_map.lua&lt;/code&gt;. The business logic can be simplified to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;coordinates&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;point&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;ipairs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;coordinates&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="c1"&gt;-- Check whether the coordinates satisfy the condition&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This loop should not run forever. &lt;code&gt;idx&lt;/code&gt; should advance through 1, 2, and 3, then the loop should end. Instead, the observed sequence was &lt;code&gt;1, 2, 3, 1, 2, 3, 1, 2, 3, ...&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In the affected worker, the index kept cycling through 1, 2, and 3.&lt;/strong&gt; This was more difficult than an ordinary application bug: the Lua source did not contain an obvious infinite loop.&lt;/p&gt;

&lt;h3&gt;
  
  
  1.2 Symptom two: Lua behavior that contradicts the source code
&lt;/h3&gt;

&lt;p&gt;During the investigation, we also encountered production incidents that looked like values disappearing. Here are two examples from another incident report.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example A: a value is checked as a string, but is later reported as &lt;code&gt;nil&lt;/code&gt;.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fri85a1modd4gwamet5dz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fri85a1modd4gwamet5dz.png" alt="Code checking that verified_md5 is a string" width="800" height="241"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Line 131 checks that &lt;code&gt;verified_md5&lt;/code&gt; is a string before entering the following code. By the time the logging statement at line 135 executes, &lt;code&gt;verified_md5&lt;/code&gt; should therefore be a string.&lt;/p&gt;

&lt;p&gt;At runtime, however, the log at line 135 reported that &lt;code&gt;verified_md5&lt;/code&gt; was &lt;code&gt;nil&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp8lphwee42g5n62nf3xu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp8lphwee42g5n62nf3xu.png" alt="Runtime log reporting verified_md5 as nil" width="798" height="71"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example B: a variable is initialized to an integer, but later reported as a function.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In the following screenshot, the code first assigns &lt;strong&gt;&lt;code&gt;local times = 0&lt;/code&gt;&lt;/strong&gt;, which should make &lt;code&gt;times&lt;/code&gt; a number.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnjq6xxravxl2qw1j66vh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnjq6xxravxl2qw1j66vh.png" alt="Code initializing the times variable to zero" width="799" height="373"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When &lt;strong&gt;&lt;code&gt;times = times + 1&lt;/code&gt;&lt;/strong&gt; executes, however, the runtime reports that the variable is a function.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc5iqaul925ss43ifpr77.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc5iqaul925ss43ifpr77.png" alt="Runtime error reporting the variable as a function" width="800" height="67"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Too rare to reproduce reliably
&lt;/h2&gt;

&lt;p&gt;We had roughly 2,000 ARM machines in production, each with 40 workers. Around one to three workers would exhibit the problem per day, spread across different machines.&lt;/p&gt;

&lt;p&gt;At first, we approached it like an ordinary production incident: inspect the business code, examine logs, and try to reproduce it. None of these approaches gave us a reliable starting point.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What we tried&lt;/th&gt;
&lt;th&gt;What got in the way&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Inspecting the Lua business logic&lt;/td&gt;
&lt;td&gt;The loop conditions were normal; nothing explained an infinite loop.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Enabling LuaJIT compilation assertions&lt;/td&gt;
&lt;td&gt;Execution appeared normal, and no assertion fired before the infinite loop.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Upgrading LuaJIT twice&lt;/td&gt;
&lt;td&gt;Neither upgrade eliminated the problem at the time.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Purchasing OpenResty XRay and recording live worker execution&lt;/td&gt;
&lt;td&gt;Recording had too much performance overhead to leave it enabled for long periods in our workload.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Exporting production requests from logs and replaying them offline&lt;/td&gt;
&lt;td&gt;We could not reproduce the problem.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Narrowing the investigation by the domains in the triggering requests&lt;/td&gt;
&lt;td&gt;Too many domains were still involved to identify the business feature responsible.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Before finding the root cause, all we could do was monitor sustained 100% CPU usage, watch for individual workers allocating more than 10 GB of memory, and scan error logs periodically. We sent &lt;code&gt;kill -9&lt;/code&gt; to affected workers so that the master process would restart them. This was &lt;strong&gt;containment, not a fix&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Eventually, we changed our approach:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If we cannot reproduce the problem reliably, why not inspect exactly what an already-affected worker is executing?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When monitoring detected a worker in an infinite loop, we sent it &lt;code&gt;kill -6&lt;/code&gt; to generate a core dump. We used GDB to inspect its registers and instructions, then used Python's Unicorn emulator to replay a segment of ARM64 instructions from the core. After emulating a large number of instructions, we found that the program counter—the PC, or the address of the instruction the CPU is currently executing—kept returning to the same set of addresses.&lt;/p&gt;

&lt;p&gt;The important questions were no longer just whether the Lua source contained a bad loop:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;What had LuaJIT compiled this Lua code into?&lt;/li&gt;
&lt;li&gt;Did the generated machine code update the loop index correctly?&lt;/li&gt;
&lt;li&gt;If an instruction really was missing, which part of LuaJIT's source was responsible?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We decided to look for evidence inside LuaJIT itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Using jit.dump to find the missing instruction
&lt;/h2&gt;

&lt;h3&gt;
  
  
  3.1 What is &lt;code&gt;jit.dump&lt;/code&gt;?
&lt;/h3&gt;

&lt;p&gt;LuaJIT does not compile all Lua code at once. It focuses on repeatedly executed hot paths. Each compiled execution path is called a &lt;strong&gt;trace&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A useful way to think about a trace is this: &lt;strong&gt;LuaJIT notices a frequently executed path and builds a machine-code shortcut for it.&lt;/strong&gt; A loop may involve more than one trace, and execution can jump from one trace to another.&lt;/p&gt;

&lt;p&gt;LuaJIT's built-in &lt;code&gt;jit.dump&lt;/code&gt; module can print information about these traces: Lua bytecode, the JIT compiler's intermediate representation (IR), snapshots (SNAP) of the state needed at exits, and the resulting machine code.&lt;/p&gt;

&lt;p&gt;In OpenResty, it can be enabled as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="n"&gt;init_worker_by_lua_block&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;-- Place this at the very beginning of the block&lt;/span&gt;
    &lt;span class="nb"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"jit.dump"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"tbirsm"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"/tmp/luajit-traces.log"&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt; &lt;span class="n"&gt;ngx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pid&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="c1"&gt;--Business logic follows below&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A couple of points matter:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The output can be large, so arrange to clean up the logs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enable it before Lua code starts running whenever possible.&lt;/strong&gt; Otherwise, traces compiled earlier may be missing from the log.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The JIT log at &lt;code&gt;/tmp/luajit-traces.log&lt;/code&gt; is organized from top to bottom into three parts, separated by lines beginning with &lt;code&gt;----&lt;/code&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Trace start:&lt;/strong&gt; beginning with &lt;code&gt;---- TRACE 43 start&lt;/code&gt;, this identifies where trace compilation began in the Lua code and which parent trace this trace was derived from.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;IR:&lt;/strong&gt; beginning with &lt;code&gt;---- TRACE 43 IR&lt;/code&gt;, this is LuaJIT's internal representation of what the code is supposed to do.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Machine code (&lt;code&gt;mcode&lt;/code&gt;):&lt;/strong&gt; beginning with &lt;code&gt;---- TRACE 43 mcode&lt;/code&gt;, these are the ARM64 instructions the CPU will actually execute.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In outline, LuaJIT's compilation process is: load a Lua file → convert it to bytecode → record a hot execution path as IR → turn the IR into assembly instructions.&lt;/p&gt;

&lt;p&gt;Here is an excerpt from &lt;code&gt;jit.dump&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;---- TRACE 43 start 5/1 lrucache.lua:90
0031  TSETS    7   2   1  ; "next"       (lrucache.lua:90)
0000  . . FUNCC               ; ffi.meta.__newindex
0032  TSETS    2   7   0  ; "prev"       (lrucache.lua:91)
0000  . . FUNCC               ; ffi.meta.__newindex
0033  MOV      2   7       (lrucache.lua:92)
0034  JFORL    3   5       (lrucache.lua:87)
---- TRACE 43 IR
0001 x27      int SLOAD  #5    PI
0002 x28      int SLOAD  #6    PRI
0003 x25      p64 PVAL   #10
0004 [8]      cdt CNEWI  +161  0003
....              SNAP   #0   [ ---- ---- ---- ---- ---- 0001 0002 +1   ---- 0004 ---- ]
...........
---- TRACE 43 mcode 188
ffff8d5bfa60  mov   x25, x28
ffff8d5bfa74  mov   w1, #16
...
ffff8d5bfb10  add   x30, x0, x1, lsl #47
ffff8d5bfb14  str   x30, [x19, #16]
ffff8d5bfb18  b     0x8d5cfc0c
---- TRACE 43 stop -&amp;gt; 5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3.2 Replaying the ARM64 infinite loop with Python and Unicorn
&lt;/h3&gt;

&lt;p&gt;After a worker became stuck, we sent it &lt;code&gt;kill -6&lt;/code&gt; to generate a core dump. We opened the core in GDB and inspected the backtrace with &lt;code&gt;bt&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;(gdb) bt
#0  0x0000ffff8d4072e8 in ?? ()
#1  0x0000ffe5181ddb80 in ?? ()
#2  0x0000000001447740 in ?? ()
#3  0x0000000000000000 in ?? (
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The function names are all &lt;code&gt;??&lt;/code&gt;. GDB can read registers and memory from the core, but LuaJIT's runtime-generated machine code does not have the same complete debug symbols as an ordinary C function. As a result, &lt;code&gt;bt&lt;/code&gt; often shows only machine-code addresses, without identifying the corresponding Lua source line.&lt;/p&gt;

&lt;p&gt;In this affected worker, frame &lt;code&gt;#0&lt;/code&gt;, at &lt;code&gt;0x0000ffff8d4072e8&lt;/code&gt;, is the address currently held in the PC. It points into machine code generated by LuaJIT. Frame &lt;code&gt;#1&lt;/code&gt; is not meaningful here because the stack cannot be unwound normally through these JIT-generated calls.&lt;/p&gt;

&lt;p&gt;For comparison, a normal backtrace looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#0  epoll_wait (epfd=5, events=0x145acc0, maxevents=512, timeout=timeout@entry=63979) at ../sysdeps/unix/sysv/linux/epoll_wait.c:30
#1  0x000000000044b808 in ngx_epoll_process_events (cycle=0x14d1f60, timer=63979, flags=1) at src/event/modules/ngx_epoll_module.c:800
#2  0x0000000000442d56 in ngx_process_events_and_timers (cycle=cycle@entry=0x14d1f60) at src/event/ngx_event.c:257
#3  0x0000000000449c72 in ngx_worker_process_cycle (cycle=cycle@entry=0x14d1f60, data=data@entry=0x0) at src/os/unix/ngx_process_cycle.c:811
#4  0x0000000000448687 in ngx_spawn_process (cycle=cycle@entry=0x14d1f60, proc=0x449c00 &amp;lt;ngx_worker_process_cycle&amp;gt;, data=0x0, 
    name=0x539267 "worker process", respawn=respawn@entry=1) at src/os/unix/ngx_process.c:199
#5  0x000000000044ad81 in ngx_reap_children (cycle=0x14d1f60) at src/os/unix/ngx_process_cycle.c:678
#6  ngx_master_process_cycle (cycle=&amp;lt;optimized out&amp;gt;) at src/os/unix/ngx_process_cycle.c:180
#7  0x00000000004231d8 in main (argc=&amp;lt;optimized out&amp;gt;, argv=&amp;lt;optimized out&amp;gt;) at src/core/nginx.c:385
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We then wrote a Python script to read the register state from the core and emulate the machine instructions. The PC repeatedly returned to particular instruction addresses, such as &lt;code&gt;ffff8d4072e8&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fevi4diqytmuvue2ccv7o.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fevi4diqytmuvue2ccv7o.png" alt="Emulator output showing repeated ARM64 instruction addresses" width="800" height="139"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We searched the JIT log at &lt;code&gt;/tmp/luajit-traces.log&lt;/code&gt; for &lt;code&gt;ffff8d4072e8&lt;/code&gt; and found that it was the start address of trace 2235. The relevant excerpts were:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;---- TRACE 60 start captcha_map.lua:278
---- TRACE 60 IR
ffff8d5c8118 mov x7, #0x3a98
.........
ffff8d5c8348 b   0xffff8d5c77ec

---- TRACE 64 start 60/3 captcha_map.lua:279
---- TRACE 64 IR
---- TRACE 64 mcode 320
ffff8d5c76a4 mov x7, #0x3a98
.........
ffff8d5c787c b   0xffff8d5c77ec

---- TRACE 2235 start 64/3 captcha_map.lua:279
0005  TGETS    9   8   1  ; "x"       (captcha_map.lua:279)
...
0022  JITERL   7  60       (captcha_map.lua:278)
---- TRACE 2235 IR
.....
---- TRACE 2235 mcode 320
ffff8d4072e8 mov x27, x26
.........
ffff8d407424 b 0x8d5c8118
---- TRACE 2235 stop -&amp;gt; 60
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Combining the emulated PC sequence with &lt;code&gt;jit.dump&lt;/code&gt;, the core dump, and GDB, we identified this execution chain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;TRACE 60  →  TRACE 64  →  TRACE 2235
    ↑                         |
    └─────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The PC kept jumping between the machine code of traces 60, 64, and 2235. The infinite loop was therefore inside this trace chain.&lt;/p&gt;

&lt;p&gt;The exported JIT logs linked these traces to the following Lua code: the &lt;code&gt;for&lt;/code&gt; loop at line 278.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi2ts2e09htb59wihnd4p.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi2ts2e09htb59wihnd4p.png" alt="The Lua loop corresponding to traces 60, 64, and 2235" width="800" height="201"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;From the code, we inferred that the &lt;code&gt;coordinates&lt;/code&gt; array had exactly three elements. &lt;code&gt;idx&lt;/code&gt; should have advanced through 1, 2, and 3, then terminated the loop. Instead, it repeated &lt;code&gt;1, 2, 3, 1, 2, 3, ...&lt;/code&gt; indefinitely.&lt;/p&gt;

&lt;p&gt;The repeating sequence had to be 1, 2, and 3, rather than 1, 2, 3, 4, and so on: an index of 4 would be beyond this three-element array and would cause an error or terminate the iteration.&lt;/p&gt;

&lt;p&gt;We had located the affected worker, yet the source code still looked correct. Next, we compared its JIT log with the log produced by a healthy worker executing the same code.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3igbpmgbn5lldoe2mmce.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3igbpmgbn5lldoe2mmce.png" alt="Side-by-side JIT output from an affected worker and a healthy worker" width="800" height="261"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The affected worker is on the left; the healthy worker is on the right. The JIT logs were almost identical. The key difference was at the end of the generated assembly: the affected worker had &lt;strong&gt;two &lt;code&gt;str&lt;/code&gt; instructions&lt;/strong&gt;, while the healthy worker had &lt;strong&gt;three&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The bytecode was identical in both workers.&lt;/li&gt;
&lt;li&gt;The IR was identical, including the snapshot.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;mcode&lt;/code&gt; sizes differed: &lt;strong&gt;trace 2235 was 320 bytes, while trace 3014 was 324 bytes&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;ARM64 instructions are four bytes long. The difference was exactly one instruction. Looking at the ends of the two machine-code sequences:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Both workers ended with a &lt;code&gt;b&lt;/code&gt; instruction that jumped to another address: trace 60.&lt;/li&gt;
&lt;li&gt;The affected worker's penultimate instruction matched the healthy worker's third-to-last instruction: an &lt;code&gt;add&lt;/code&gt; that performed a register calculation.&lt;/li&gt;
&lt;li&gt;The healthy worker's penultimate instruction was a &lt;code&gt;str&lt;/code&gt;, which stored a register value to memory. That instruction was missing from the affected worker.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;; Healthy trace 3014
add  x30, x28, x1, lsl #47
str  x30, [x19, #64]      ; slot 10
add  x30, x0, w26, uxtw
str  x30, [x19, #56]      ; slot 9
add  x30, x0, w26, uxtw
str  x30, [x19, #48]      ; slot 8   &amp;lt;-- Present in the healthy worker
b    &amp;lt;jump back to trace 60&amp;gt;

; Infinite-loop trace 2235
add  x30, x28, x1, lsl #47
str  x30, [x19, #64]      ; slot 10
add  x30, x0, w26, uxtw
str  x30, [x19, #56]      ; slot 9
add  x30, x0, w26, uxtw   ; The value has been calculated
b    &amp;lt;jump back to trace 60&amp;gt;      ; But the str for slot 8 is missing
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The corresponding IR snapshot included:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;0023 x26 int ADD 0001 +1 ...
SNAP #3   [ ---- ---- ---- ---- ---- ---- ---- ---- 0023 0023 0028 ]
                                                    slot8 slot9 slot10
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first line is straightforward: &lt;strong&gt;increment the loop index by one&lt;/strong&gt;. &lt;code&gt;SNAP&lt;/code&gt; records the Lua state that must be restored when a trace exits and in related situations. It is not a one-to-one list of machine instructions, but it helps us check whether computed values are propagated correctly.&lt;/p&gt;

&lt;p&gt;Slot 8 holds the loop-control variable used by &lt;code&gt;ipairs&lt;/code&gt;. Reference &lt;code&gt;0023&lt;/code&gt; is the incremented index, computed by &lt;code&gt;ADD 0001 +1&lt;/code&gt;. With the store missing, the next iteration—after jumping back to trace 60—reads the old index through &lt;code&gt;SLOAD #8&lt;/code&gt;, and the loop keeps repeating the same elements.&lt;/p&gt;

&lt;p&gt;To see why losing a single &lt;code&gt;STR&lt;/code&gt; can cause an infinite loop, consider how traces 60, 64, and 2235 execute together.&lt;/p&gt;

&lt;p&gt;First, two instructions are worth spelling out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;SLOAD #8&lt;/code&gt; reads the loop index &lt;code&gt;idx&lt;/code&gt; from slot 8 of the Lua stack.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;STR x30, [x19, #48]&lt;/code&gt; writes the newly calculated index, encoded as a Lua value, back to slot 8.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Normal execution:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;TRACE 60
    Read idx from slot 8; assume its current value is 1
        ↓
TRACE 64
    Continue the loop body and advance the index
        ↓
TRACE 2235
    Assume the newly calculated idx is 4
    Execute STR, writing the new index 4 back to slot 8
        ↓
Jump back to TRACE 60
    Read idx = 4 from slot 8
    Continue or terminate iteration using the updated index
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Faulty execution, with one &lt;code&gt;STR&lt;/code&gt; missing from trace 2235:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;TRACE 60
    Read idx = 1 from slot 8
        ↓
TRACE 64
    Continue the loop body and advance the index
        ↓
TRACE 2235
    Calculate the new idx = 4
    STR is missing, so slot 8 is not updated
        ↓
Jump back to TRACE 60
    Read idx from slot 8; it is still 1
        ↓
TRACE 64 → TRACE 2235 → TRACE 60
    Repeat indefinitely
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The essential problem was that the new loop index had been calculated in a register but had not been written back to the Lua stack.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Trace 2235 eventually jumps back to trace 60. Trace 60 reloads the index from slot 8, sees the old value, and repeats the same execution path. The result is an infinite loop.&lt;/p&gt;

&lt;p&gt;For the first time, we had narrowed “a Lua iteration gets stuck” down to a specific observation:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;LuaJIT had computed the new loop index, but the generated ARM64 machine code was missing a necessary &lt;code&gt;STR&lt;/code&gt; instruction to store it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Why had that instruction disappeared?&lt;/p&gt;

&lt;p&gt;Had the assembler failed to emit it, or had it been generated and then overwritten later during code generation?&lt;/p&gt;

&lt;p&gt;To answer that question, we began reading LuaJIT's ARM64 assembler, focusing on &lt;code&gt;lj_asm_arm64.h&lt;/code&gt; and &lt;code&gt;lj_emit_arm64.h&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Our conclusion at this point was:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The affected worker was missing one assembly instruction. The problem was in code generation, rather than the IR or the business logic.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  4. Reading the source
&lt;/h2&gt;

&lt;p&gt;While reading the source, we also consulted Kong's ARM64 investigation, &lt;a href="https://konghq.com/blog/engineering/troubleshooting-an-intermittent-failure-on-arm64" rel="noopener noreferrer"&gt;Troubleshooting an Intermittent Failure on ARM64&lt;/a&gt;. It drew our attention to the optimization that merges memory-access instructions on ARM64.&lt;/p&gt;

&lt;p&gt;We also noticed a comment in &lt;code&gt;emit_lso()&lt;/code&gt; explaining that &lt;code&gt;str&lt;/code&gt; instructions can be merged.&lt;/p&gt;

&lt;p&gt;Our initial hypothesis was therefore that the missing &lt;code&gt;str&lt;/code&gt; might have been generated, then lost as a consequence of instruction merging.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgayyqlt56w6ob0txjc3o.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgayyqlt56w6ob0txjc3o.png" alt="Source comment describing load/store instruction merging" width="800" height="546"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  4.1 Some background
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;First: the machine code for multiple traces can occupy adjacent regions of JIT memory.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We examined three consecutive traces in the &lt;code&gt;jit.dump&lt;/code&gt; output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;---- TRACE 2233 mcode 128
ffff8d40747c ldr x30, [x22, #368]
......
ffff8d4074f8 b   0x8c6bcd28
---- TRACE 2233 stop -&amp;gt; interpreter

---- TRACE 2234 mcode 24
ffff8d40744c mov x0, #0
......
ffff8d407460 b   0x8c6bcd28
---- TRACE 2234 stop -&amp;gt; interpreter

---- TRACE 2235 mcode 320
ffff8d4072e8 mov x27, x26
......
ffff8d407424 b   0x8d5c8118
---- TRACE 2235 stop -&amp;gt; 60
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;TRACE 2233: ffff8d40747c ... ffff8d4074f8
TRACE 2234: ffff8d40744c ... ffff8d407460
TRACE 2235: ffff8d4072e8 ... ffff8d407424
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The code addresses of these traces become progressively lower. LuaJIT's ARM64 assembler emits instructions from higher addresses toward lower addresses. This raised a question: could different compilation attempts reuse memory that had previously been occupied?&lt;/p&gt;

&lt;p&gt;The later traces in this region clearly used lower addresses. Reading the machine-code allocator and ARM64 assembler confirmed that LuaJIT places multiple traces within allocated machine-code regions and that the ARM64 assembler generally &lt;strong&gt;emits instructions from high addresses to low addresses&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That suggested another possibility: &lt;strong&gt;if a previous compilation failed partway through, could the machine-code bytes already written remain in memory and be encountered by a later compilation?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Second: trace compilation can abort, leaving machine-code bytes behind.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The production logs contained many entries like these:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;---- TRACE 2235 abort sbw_preload_up_rewrite_pre.lua:47
---- TRACE 2235 start config.lua:569
---- TRACE 2235 abort config.lua:590
---- TRACE 2235 start ssl_cert_by.lua:108
---- TRACE 2235 abort ssl_cert_by.lua:140
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An &lt;code&gt;abort&lt;/code&gt; means that the trace compilation did not succeed. The corresponding machine-code space can be reused by later compilations, but the previously written bytes are not cleared.&lt;/p&gt;

&lt;p&gt;The relevant code included the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;trace_abort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jit_State&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;J&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="p"&gt;....&lt;/span&gt;
  &lt;span class="n"&gt;lj_mcode_abort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;J&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="cm"&gt;/* Abort the reservation. */&lt;/span&gt;
&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;lj_mcode_abort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jit_State&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;J&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;J&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;mcarea&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;mcode_protect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;J&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;MCPROT_RUN&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;mcode_protect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jit_State&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;J&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;prot&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;J&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;mcprot&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;prot&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LJ_UNLIKELY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mcode_setprot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;J&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;mcarea&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;J&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;szmcarea&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prot&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
      &lt;span class="n"&gt;mcode_protfail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;J&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;J&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;mcprot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;prot&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On this path, &lt;code&gt;lj_mcode_abort()&lt;/code&gt; mainly calls &lt;code&gt;mcode_protect()&lt;/code&gt; to restore the machine-code region's runtime memory protection.&lt;/p&gt;

&lt;p&gt;It neither clears the machine code already written nor commits the space reserved by the failed compilation.&lt;/p&gt;

&lt;p&gt;Consequently, if a trace has already reached machine-code generation and written some instructions before aborting, those bytes can remain at their original addresses until a later compilation reuses the space.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Third: ARM64's &lt;code&gt;emit_lso()&lt;/code&gt; tries to merge adjacent memory instructions.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In &lt;code&gt;lj_emit_arm64.h&lt;/code&gt;, we focused on the optimization in &lt;code&gt;emit_lso()&lt;/code&gt; that combines adjacent &lt;code&gt;LDR&lt;/code&gt;/&lt;code&gt;STR&lt;/code&gt; instructions into &lt;code&gt;LDP&lt;/code&gt;/&lt;code&gt;STP&lt;/code&gt;. For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;str x4,  [x19, #48]
str x30, [x19, #56]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the pairing conditions are satisfied, these can be combined into one &lt;code&gt;STP&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;stp x4, x30, [x19, #48]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is normally a valid optimization. But &lt;strong&gt;what if the instruction selected as a pairing candidate was not generated by the current compilation at all, but was stale content left in memory?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fourth: trace compilation reserves one or two instruction slots at the tail.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In the older &lt;code&gt;asm_tail_prep()&lt;/code&gt; in &lt;code&gt;lj_asm_arm64.h&lt;/code&gt;, the non-loop branch reserves two slots: one for a later branch and another for stack-pointer adjustment.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="n"&gt;MCode&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;as&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;mctop&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;as&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;mcp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;as&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;invmcp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only &lt;code&gt;*p&lt;/code&gt; is cleared here; &lt;code&gt;p[-1]&lt;/code&gt; is not. Meanwhile, &lt;code&gt;as-&amp;gt;mcp&lt;/code&gt; points exactly to &lt;code&gt;p[-1]&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This led to a more specific hypothesis: &lt;strong&gt;if an old &lt;code&gt;STR&lt;/code&gt; remained in the reserved slot &lt;code&gt;p[-1]&lt;/code&gt;, could &lt;code&gt;emit_lso()&lt;/code&gt; mistakenly treat it as an instruction belonging to the current trace and merge it with a new instruction?&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  4.2 Our hypothesis, step by step
&lt;/h3&gt;

&lt;p&gt;Combining these observations, we modeled the following scenario:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A trace compilation aborts, leaving a &lt;code&gt;STR&lt;/code&gt; in machine-code memory.&lt;/li&gt;
&lt;li&gt;A shorter trace then compiles successfully and overwrites only part of those old instructions.&lt;/li&gt;
&lt;li&gt;During the next trace compilation, the stale &lt;code&gt;STR&lt;/code&gt; is mistaken for a current instruction and used in a merge.&lt;/li&gt;
&lt;/ol&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Initial state: J-&amp;gt;mctop = T
(T-1 and T-2 are adjacent instruction slots, each four bytes wide)

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Step 1: a trace starts assembly, then aborts
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

asm_tail_prep:
p = T-1
mcp = T-2 (two reserved slots: T-1 is zeroed; T-2 is not)

*--mcp = ... mcp = T-3 ← instruction 1
*--mcp = ... mcp = T-4 ← instruction 2
*--mcp = ... mcp = T-5 ← instruction 3
*--mcp = ... mcp = T-6 ← instruction 4 (str x30, [x19, #56]) ← target!
*--mcp = ... mcp = T-7 ← instruction 5

Abort! This compilation is not committed.
Later traces can reuse the space, but the bytes are not cleared.

Memory now:
T-1: 0 ← cleared by asm_tail_prep
T-2: [not cleared; may contain old data]
T-3: instruction left behind by the aborted compilation
T-4: instruction left behind by the aborted compilation
T-5: instruction left behind by the aborted compilation
T-6: str x30, [x19, #56] ← left behind by the aborted compilation ★
T-7: instruction left behind by the aborted compilation


━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Step 2: another, shorter trace compiles successfully
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

This trace emits only two ordinary instructions.
asm_tail_prep: mcp = T-2
*--mcp = ... mcp = T-3 ← overwrites the old instruction at T-3
*--mcp = ... mcp = T-4 ← overwrites the old instruction at T-4
asm_tail_fixup: T-1 = NOP, T-2 = branch ← writes the tail branch

Compilation succeeds; J-&amp;gt;mctop = T-4
(Assume a non-loop trace with spadj = 0.)

Memory now:
T-1: NOP ← written by the successful trace
T-2: branch ← written by the successful trace
T-3: instruction from the successful trace ← overwrites the old bytes
T-4: instruction from the successful trace ← overwrites the old bytes
T-5: instruction from the aborted compilation ← untouched
T-6: str x30, [x19, #56] ← stale instruction, still untouched! ★
T-7: instruction from the aborted compilation


━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Step 3: the faulty trace starts compiling from T-4
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

asm_tail_prep:
p = T-5
mcp = T-6 ← still contains the stale str x30, [x19, #56]! ★

The first instruction to emit is: str x30, [x19, #48]
emit_lso reads *mcp: the old str x30, [x19, #56] at T-6.
Offsets #48 and #56 differ by 8 bytes and satisfy the STR → STP pairing conditions.

*mcp = stp x30, x30, [x19, #48]
    ← the merged STP is written at T-6
    ← pairing returns immediately; mcp does not move to a lower address

*--mcp = ... mcp = T-7
    ← the second instruction: add x30, x0, w26, uxtw

asm_tail_fixup:
T-5 = NOP
T-6 = branch ← overwrites the STP just produced by merging

Memory now:

T-1: NOP ← written by the previous successful trace
T-2: branch ← written by the previous successful trace
T-3: instruction from the previous successful trace
T-4: instruction from the previous successful trace
T-5: NOP ← written by the faulty trace
T-6: branch ← written by the faulty trace, overwriting the STP
T-7: add x30, x0, w26, uxtw ← written by the faulty trace
     The required str x30, [x19, #48] has disappeared! ★
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the production JIT log, trace 2235 was the one that went wrong. Before it, we observed 32 failed attempts at trace 2234, one successful trace 2234, and 55 failed attempts at trace 2235. These frequent aborts and retries strengthened our suspicion that stale instructions in reused machine-code memory were participating in a merge.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.3 Trying to reproduce it offline
&lt;/h3&gt;

&lt;p&gt;To test this hypothesis, we wrote a simplified Lua program that reproduced the loop structure in the production &lt;code&gt;captcha_map.lua&lt;/code&gt; as closely as possible.&lt;/p&gt;

&lt;p&gt;The test data included both integers and floating-point numbers so that LuaJIT would generate IR and machine code structurally similar to production trace 2235.&lt;/p&gt;

&lt;p&gt;Without fault injection, the test program terminated normally. Its ARM traces had a loop-tail structure similar to the one in the affected production trace.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="c1"&gt;--LUAJIT_REPRO_FORCE_TAILPAIR_TRACE=9 LUAJIT_REPRO_DUMP=/tmp/jit_repro_force.dump ./luajit /root/zengjinji/luajit_ipairs_trace_simple.lua&lt;/span&gt;

&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;jit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"jit"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;jit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;off&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;dump_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;os.getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"LUAJIT_REPRO_DUMP"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;dump_flags&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"tbirsm"&lt;/span&gt;
&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;warmup_iters&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;

&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;target_iters&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;

&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;force_number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;is_in_radius_range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;coordinates&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;radius&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;idx&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;ipairs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;coordinates&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;radius&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;radius&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
        &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;int_coordinates&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;135&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;119&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;202&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;202&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;326&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;148&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;198&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;mixed_coordinates&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;135&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;119&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;202&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;202&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;force_number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;326&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;force_number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;148&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;198&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;int_x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;231&lt;/span&gt;
&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;int_y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;106&lt;/span&gt;
&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;num_x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;force_number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;231&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;num_y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;force_number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;106&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;radius_num&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;force_number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;jit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;jit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;opt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"hotloop=1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"hotexit=1"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;jit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;flush&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="nb"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"jit.dump"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dump_flags&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dump_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;warmup_iters&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;is_in_radius_range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;int_coordinates&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;int_x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;int_y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;radius_num&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;warmup_iters&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;is_in_radius_range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;int_coordinates&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;num_x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;num_y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;radius_num&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;warmup_iters&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;is_in_radius_range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mixed_coordinates&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;num_x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;num_y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;radius_num&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;target_iters&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;is_in_radius_range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mixed_coordinates&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;num_x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;num_y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;radius_num&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We exported the generated JIT output and compared it with the production log. The IR and assembly produced for this test were largely consistent with the faulty production trace 2235:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpopeds7zv095ckodrozl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpopeds7zv095ckodrozl.png" alt="Comparison of the test trace and production trace IR" width="799" height="235"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F479jgbh23hlqxy5p7h24.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F479jgbh23hlqxy5p7h24.png" alt="Comparison of the test trace and production machine code" width="800" height="148"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Running the test script alone did not reliably reproduce the stale instructions observed in production. We therefore modified LuaJIT, based on &lt;code&gt;v2.1-20201027&lt;/code&gt;, to add a fault-injection switch used only for testing.&lt;/p&gt;

&lt;p&gt;When compiling a designated trace—trace 9—we deliberately wrote &lt;code&gt;STR x30, [x19, #56]&lt;/code&gt; into the reserved slot &lt;code&gt;p[-1]&lt;/code&gt; in &lt;code&gt;asm_tail_prep()&lt;/code&gt;, simulating an old instruction left in that memory.&lt;/p&gt;

&lt;p&gt;We then checked whether &lt;code&gt;emit_lso()&lt;/code&gt; would incorrectly merge that injected instruction with the newly requested &lt;code&gt;STR x30, [x19, #48]&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;--- a/src/lj_asm_arm64.h
&lt;/span&gt;&lt;span class="gi"&gt;+++ b/src/lj_asm_arm64.h
&lt;/span&gt;&lt;span class="p"&gt;@@ -1987,8 +1987,28 @@&lt;/span&gt; static void asm_tail_prep(ASMState *as)
   if (as-&amp;gt;loopref) {
     as-&amp;gt;invmcp = as-&amp;gt;mcp = p;
   } else {
&lt;span class="gi"&gt;+    const char *force_tailpair_trace = getenv("LUAJIT_REPRO_FORCE_TAILPAIR_TRACE");
+    int force = 0;
&lt;/span&gt;     as-&amp;gt;mcp = p-1;  /* Leave room for stack pointer adjustment. */
     as-&amp;gt;invmcp = NULL;
&lt;span class="gi"&gt;+    if (force_tailpair_trace)
+      force = as-&amp;gt;T-&amp;gt;traceno == (TraceNo)atoi(force_tailpair_trace);
+    if (force) {
+      /* Proof knob: leave a fake stale STRx [BASE,#56] where emit_lso() peeks.
+      ** A following STRx [BASE,#48] can be paired here without decrementing mcp;
+      ** asm_tail_fixup() later overwrites this reserved slot with the tail branch.
+      */
+      p[-1] = A64I_STRx | A64F_D(RID_TMP) | A64F_N(RID_BASE) | A64F_U12(56 &amp;gt;&amp;gt; 3);
+    }
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After rebuilding LuaJIT and running the test, the process stayed at 100% CPU and the program failed to terminate.&lt;/p&gt;

&lt;p&gt;Comparing &lt;code&gt;jit.dump&lt;/code&gt; output with fault injection enabled and disabled showed that, with injection enabled, trace 9 was also missing the &lt;code&gt;STR&lt;/code&gt; that writes back the loop index. This matched the faulty behavior of production trace 2235.&lt;/p&gt;

&lt;p&gt;The experiment demonstrated that placing a suitable stale &lt;code&gt;STR&lt;/code&gt; in the reserved slot could trigger the same kind of incorrect code generation and produce an infinite loop.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fulmd4dq1v8l9nc9r2mph.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fulmd4dq1v8l9nc9r2mph.png" alt="Fault injection reproducing the missing store and infinite loop" width="798" height="156"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Fixing production
&lt;/h2&gt;

&lt;h3&gt;
  
  
  5.1 The code change
&lt;/h3&gt;

&lt;p&gt;Once we understood the issue, we revisited the older &lt;code&gt;asm_tail_prep()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;asm_tail_prep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ASMState&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;as&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;MCode&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;as&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;mctop&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;as&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;loopref&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;as&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;invmcp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;as&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;mcp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;as&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;mcp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;as&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;invmcp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the non-loop branch, &lt;code&gt;as-&amp;gt;mcp&lt;/code&gt; points to &lt;code&gt;p[-1]&lt;/code&gt;, but only &lt;code&gt;*p&lt;/code&gt; is cleared.&lt;/p&gt;

&lt;p&gt;If &lt;code&gt;p[-1]&lt;/code&gt; contains a stale instruction, &lt;code&gt;emit_lso()&lt;/code&gt; can therefore mistake it for an instruction already generated by the current trace.&lt;/p&gt;

&lt;p&gt;We chose a very small fix:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Add &lt;code&gt;p[-1] = 0&lt;/code&gt; to the non-loop branch of &lt;code&gt;asm_tail_prep()&lt;/code&gt; so that no old instruction remains in the reserved slot.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The modified code is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;asm_tail_prep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ASMState&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;as&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;MCode&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;as&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;mctop&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;as&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;loopref&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;as&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;invmcp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;as&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;mcp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;as&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;mcp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;as&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;invmcp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="cm"&gt;/* Prevent load/store merging on reserved tail slot. */&lt;/span&gt;
    &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="cm"&gt;/* Added */&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before the change:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;asm_tail_prep:
    p = T-1
    mcp = T-2

Memory:
T-1: 0
T-2: str x30, [x19, #56] ← may be a stale instruction
T-3: ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After the change:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;asm_tail_prep:
    p = T-1
    mcp = T-2
    p[-1] = 0

Memory:
T-1: 0
T-2: 0 ← reserved slot has been cleared
T-3: ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even if another trace previously used this memory, the compiler will no longer begin the new trace with an old &lt;code&gt;STR&lt;/code&gt; in the reserved slot.&lt;/p&gt;

&lt;p&gt;When &lt;code&gt;emit_lso()&lt;/code&gt; reads &lt;code&gt;*as-&amp;gt;mcp&lt;/code&gt;, the location has already been zeroed. A stale &lt;code&gt;STR&lt;/code&gt; cannot trigger the incorrect merge, so the new &lt;code&gt;STR&lt;/code&gt; is emitted normally.&lt;/p&gt;

&lt;h3&gt;
  
  
  5.2 Validation in production
&lt;/h3&gt;

&lt;p&gt;After deploying the fix across the fleet, we continued monitoring workers on the ARM machines. The intermittent &lt;strong&gt;100% CPU infinite loops previously observed across more than 2,000 ARM machines no longer occurred&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;We also stopped observing some of the earlier LuaJIT symptoms involving disappearing values and unexpected variable types.&lt;/p&gt;

&lt;p&gt;Adding &lt;code&gt;p[-1] = 0&lt;/code&gt; to &lt;code&gt;asm_tail_prep()&lt;/code&gt; was effective for this ARM64 infinite-loop problem, consistent with our source analysis and fault-injection experiment.&lt;/p&gt;

&lt;p&gt;However, we did not analyze the IR and machine code for every separate “disappearing value” incident. Those symptoms may also have involved incorrect JIT-generated memory-access instructions, but we cannot establish that all of them were caused by this particular incorrect &lt;code&gt;STR → STP&lt;/code&gt; merge.&lt;/p&gt;

&lt;h3&gt;
  
  
  5.3 The upstream change
&lt;/h3&gt;

&lt;p&gt;We originally used &lt;strong&gt;OpenResty 1.19.3.1&lt;/strong&gt;, which bundled OpenResty's &lt;a href="https://github.com/openresty/luajit2" rel="noopener noreferrer"&gt;luajit2&lt;/a&gt; at &lt;code&gt;v2.1-20201027&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;During the investigation, we independently upgraded luajit2 first to &lt;code&gt;v2.1-20241203&lt;/code&gt; and then to &lt;code&gt;v2.1-20250529&lt;/code&gt;. Neither upgrade resolved the problem.&lt;/p&gt;

&lt;p&gt;Later, by comparing the upstream source, we found that &lt;strong&gt;a LuaJIT refactoring committed on November 6, 2025 had already addressed this problem indirectly&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The related issue and commit are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/LuaJIT/LuaJIT/issues/285" rel="noopener noreferrer"&gt;LuaJIT issue #285: Increase available allocation range in mcode_alloc()&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/LuaJIT/LuaJIT/commit/68354f444728ef99bb51bb4d86e8f1b40853a898" rel="noopener noreferrer"&gt;LuaJIT commit 68354f4: Allow mcode allocations outside of the jump range to the support code&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The main purpose of that change was to expand the address range available for machine-code allocations and reduce constraints imposed by machine-code branch distances on architectures such as ARM64. It was not written specifically for the infinite loop we encountered.&lt;/p&gt;

&lt;p&gt;However, the change also refactored &lt;code&gt;asm_tail_prep()&lt;/code&gt; and &lt;code&gt;asm_tail_fixup()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The older implementation:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;asm_tail_prep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ASMState&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;as&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;MCode&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;as&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;mctop&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;as&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;loopref&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;as&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;invmcp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;as&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;mcp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;as&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;mcp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;as&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;invmcp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The problem in the older implementation is that the non-loop branch sets &lt;code&gt;as-&amp;gt;mcp&lt;/code&gt; to &lt;code&gt;p[-1]&lt;/code&gt;, but the final zeroing operation clears &lt;code&gt;*p&lt;/code&gt; instead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The relevant part of the newer implementation:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;asm_tail_prep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ASMState&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;as&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TraceNo&lt;/span&gt; &lt;span class="n"&gt;lnk&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;MCode&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;as&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;mctop&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;as&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;loopref&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;as&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;invmcp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;as&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;mcp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;lnk&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;MCode&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MCode&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;lj_vm_exit_interp&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;A64F_S_OK&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;26&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
          &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;A64F_S_OK&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;26&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="cm"&gt;/* Leave room for stack pointer adjustment. */&lt;/span&gt;
    &lt;span class="n"&gt;as&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;mcp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;as&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;invmcp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="n"&gt;as&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;mctail&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="cm"&gt;/* Prevent load/store merging. */&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two changes stand out:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The reserved slot is initialized differently.&lt;/strong&gt; The newer implementation first determines the location of &lt;code&gt;as-&amp;gt;mcp&lt;/code&gt;, then executes &lt;code&gt;*p = 0&lt;/code&gt;. Since &lt;code&gt;as-&amp;gt;mcp = p&lt;/code&gt; at that point, it clears exactly the location that &lt;code&gt;emit_lso()&lt;/code&gt; may inspect. An old &lt;code&gt;STR&lt;/code&gt; at that location cannot be mistaken for a pairing candidate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tail code generation is restructured.&lt;/strong&gt; A new &lt;code&gt;as-&amp;gt;mctail&lt;/code&gt; records the trace-tail location. &lt;code&gt;asm_tail_fixup()&lt;/code&gt; uses it to generate stack-pointer adjustments and branch instructions instead of relying entirely on the older fixed reserved-slot arrangement.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The most important part is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="n"&gt;as&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;mcp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="cm"&gt;/* Prevent load/store merging. */&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The newer implementation determines the location first, then clears that location, preventing stale instructions from taking part in an incorrect merge.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Although the upstream change primarily supported more flexible machine-code allocation, its restructuring of ARM64 tail generation also eliminated the problem we found.&lt;/p&gt;

&lt;p&gt;The upstream commit does not explicitly state that it fixes this particular infinite-loop defect. That connection is our conclusion from comparing the older and newer source.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Corresponding OpenResty versions&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Checking the OpenResty release history showed that the following versions include the relevant change:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;luajit2:&lt;/strong&gt; &lt;code&gt;v2.1-20260311&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OpenResty:&lt;/strong&gt; &lt;code&gt;1.29.2.2&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The preceding OpenResty &lt;code&gt;1.29.2.1&lt;/code&gt; used luajit2 &lt;code&gt;v2.1-20251022&lt;/code&gt;, which did not yet include the upstream change from November 6, 2025.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;November 6, 2025 is the date of the upstream LuaJIT commit; OpenResty incorporated the change in a later release.&lt;/strong&gt; It would be incorrect to assume that every OpenResty release published in November 2025 already included the fix.&lt;/p&gt;

&lt;p&gt;Readers running an older OpenResty on ARM64 and encountering similar worker loops or abnormal JIT-generated code can check their LuaJIT version and consider upgrading to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;OpenResty &lt;code&gt;1.29.2.2&lt;/code&gt; or a later stable release.&lt;/li&gt;
&lt;li&gt;luajit2 &lt;code&gt;v2.1-20260311&lt;/code&gt; or later, if maintaining luajit2 separately.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Of course, not every infinite loop in an ARM64 worker has this cause. JIT logs and machine code are still needed to identify the actual problem.&lt;/p&gt;

&lt;p&gt;Related release and source records:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://openresty.org/cn/changelog-1029002.html" rel="noopener noreferrer"&gt;Official OpenResty 1.29.2.x changelog&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/openresty/luajit2/blob/v2.1-20260311/src/lj_asm_arm64.h" rel="noopener noreferrer"&gt;OpenResty luajit2 v2.1-20260311 source&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the older LuaJIT version we maintained, we ultimately chose the minimal &lt;code&gt;p[-1] = 0&lt;/code&gt; change in &lt;code&gt;asm_tail_prep()&lt;/code&gt;, without backporting the entire larger upstream refactoring.&lt;/p&gt;

&lt;p&gt;Note: The technical investigation, debugging, and conclusions in this article are my original work. AI tools were used to assist with English translation and language polishing.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. References &amp;amp; About the Author
&lt;/h2&gt;

&lt;p&gt;This article is also available on my personal technical blog:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Original article:&lt;/strong&gt; &lt;a href="https://zengjinji.github.io/posts/luajit-arm64-infinite-loop/" rel="noopener noreferrer"&gt;An OpenResty Worker Stuck in an Infinite Loop: How We Found a Missing LuaJIT ARM64 Instruction&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Disclosure: The technical investigation, debugging, and conclusions presented in this article are my original work. AI tools were used to assist with English translation and language polishing.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>luajit</category>
      <category>openrest</category>
      <category>nginx</category>
      <category>debugging</category>
    </item>
  </channel>
</rss>
