[{"data":1,"prerenderedAt":30},["ShallowReactive",2],{"note-plugins-that-change-a-callee-saved-register-en":3},{"slug":4,"order":5,"title":6,"summary":7,"measured":8,"topic":9,"toc":10,"html":29},"plugins-that-change-a-callee-saved-register",20,"Plugins that change a callee-saved register","A plugin that returns with a callee-saved register changed breaks its host later and somewhere else. The host core now guards every plugin call, records the first change, and the qualification offers such a plugin isolated only.","Raspberry Pi 5 (Debian 13, arm64) and an x86-64 desktop, October 2026; omx-clap-core 0.26.2.","qualify",[11,14,17,20,23,26],{"id":12,"title":13},"what-happens","What happens",{"id":15,"title":16},"how-it-was-found","How it was found",{"id":18,"title":19},"the-guard","The guard",{"id":21,"title":22},"what-is-done-with-such-a-plugin","What is done with such a plugin",{"id":24,"title":25},"what-the-guard-does-not-show","What the guard does not show",{"id":27,"title":28},"how-to-see-it-again","How to see it again","\u003Ch2 id=\"what-happens\">What happens\u003C\u002Fh2>\n\u003Cp>Every calling convention gives a callee registers it must hand back as it found them:\u003C\u002Fp>\n\u003Cul>\n\u003Cli>aarch64: x19 to x28, d8 to d15, FPCR\u003C\u002Fli>\n\u003Cli>x86-64: rbx, rbp, r12 to r15, MXCSR's control bits, the x87 control word\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>A caller may keep a value in one of them across the call. A plugin that returns with one changed replaces that value\nwithout anything crashing at the time. The damage shows later, somewhere else.\u003C\u002Fp>\n\u003Ch2 id=\"how-it-was-found\">How it was found\u003C\u002Fh2>\n\u003Cp>Four limiter plugins of one LV2 collection, measured on a Raspberry Pi 5, returned from \u003Ccode>run()\u003C\u002Fcode> with d14 and d15 changed,\nfrom block 227 of the measurer's control sweep on, in 386 of its calls. The measurer kept its sample rate in d15. Its next\ninstance was opened at 0 Hz, the plugin sized a buffer from that rate and wrote through a NULL pointer: each of the four was\nrecorded as a crash (SIGSEGV).\u003C\u002Fp>\n\u003Cp>Built at -O0, where the compiler keeps its locals in memory rather than in those registers, the measurer ran them to the\nend. At -O2 it crashed every time. That difference is the mark of a clobbered register.\u003C\u002Fp>\n\u003Ch2 id=\"the-guard\">The guard\u003C\u002Fh2>\n\u003Cp>The host core calls every plugin function through one guard, \u003Ccode>omx_abi_call6\u003C\u002Fcode> (\u003Ccode>abi_guard.S\u003C\u002Fcode>):\u003C\u002Fp>\n\u003Cul>\n\u003Cli>it saves the callee-saved registers, calls, compares, and puts them back;\u003C\u002Fli>\n\u003Cli>it records the first call that changed any: the registers, the plugin function and the run block.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>Its cost is 14 to 15 ns per call on a Raspberry Pi 5 and 5.6 ns on an x86-64 desktop. For 24 plugins at 48 kHz in\n256-frame blocks that is 0.36 µs of each 5,333 µs block.\u003C\u002Fp>\n\u003Cp>The LV2 measurer makes every plugin call through the same guard, \u003Ccode>lilv_plugin_instantiate\u003C\u002Fcode> and \u003Ccode>lilv_instance_free\u003C\u002Fcode>\nincluded. Its record carries \u003Ccode>abi\u003C\u002Fcode>, for example:\u003C\u002Fp>\n\u003Cpre>\u003Ccode>{&quot;clobbered&quot;:true,&quot;registers&quot;:[&quot;d14&quot;,&quot;d15&quot;],&quot;call&quot;:&quot;run&quot;,&quot;block&quot;:227,&quot;calls&quot;:386}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>A plugin that never clobbered reads \u003Ccode>{&quot;clobbered&quot;:false}\u003C\u002Fcode>.\u003C\u002Fp>\n\u003Ch2 id=\"what-is-done-with-such-a-plugin\">What is done with such a plugin\u003C\u002Fh2>\n\u003Cp>The verdict is \u003Ccode>hosting.stability.callee-saved-clobbered\u003C\u002Fcode>, rated conditional: the plugin is offered isolated only, in a\nworker of its own, never in-process in the console and never in a shared chain. The guard keeps the host's own frame\nwhole; it does not protect the state of other plugins in the same process.\u003C\u002Fp>\n\u003Cp>A worker reports \u003Ccode>abi_clobber &lt;instance&gt; &lt;mask&gt; &lt;registers&gt; &lt;call&gt; &lt;block&gt;\u003C\u002Fcode> on its feedback port, once per instance, and\nplugin-hostd then moves the instance, with its state, to a worker of its own.\u003C\u002Fp>\n\u003Cp>Under the guard the four limiters measure to the end: no allocation in \u003Ccode>run()\u003C\u002Fcode>, and every rate sounds. Their figures from\nbefore the guard may have been corrupted and are not used.\u003C\u002Fp>\n\u003Ch2 id=\"what-the-guard-does-not-show\">What the guard does not show\u003C\u002Fh2>\n\u003Cp>A plugin that crashed without the guard and runs to the end with it, recording no clobber, is not proven clean: the guard\nchanges the measurer's stack frame and may hide a memory fault. And a plugin that still crashes under the guard and at -O0\nhas its cause elsewhere.\u003C\u002Fp>\n\u003Ch2 id=\"how-to-see-it-again\">How to see it again\u003C\u002Fh2>\n\u003Cp>The test fixture \u003Ccode>native\u002Ffixtures\u002Flv2-gain.c\u003C\u002Fcode> of plugin-qualify, built with \u003Ccode>-DFIXTURE_ABI_BREAKER\u003C\u002Fcode>, returns from its third\n\u003Ccode>run()\u003C\u002Fcode> with x19 and d15 changed (rbx and r15 on x86-64), in assembly, after the C \u003Ccode>run()\u003C\u002Fcode> has returned. The test\n\u003Ccode>src\u002Flv2-measure-native.test.ts\u003C\u002Fcode> measures it to the end and checks the record.\u003C\u002Fp>\n",1793954995776]