On Mon, Jun 8, 2026 at 8:03 AM <[email protected]> wrote:

>
> Am 08.06.2026 um 04:54 schrieb Damjan Jovanovic:
> > On Sun, Jun 7, 2026 at 10:43 PM Arrigo Marchiori<[email protected]>
> wrote:
> >
> >> Hello Damjan, All,
> >>
> >> On Sun, Jun 07, 2026 at 10:00:37PM +0200, Damjan Jovanovic wrote:
> >>
> >>> Could this be the problem? I think it must be, OpenGrok shows there's
> >>> nowhere else in OpenOffice we use PROT_EXEC.
> >>>
> >>> At least on lines 105 and 111 it's granting read, write and execute
> >>> permissions together:
> >>>
> >>> File main/bridges/source/cpp_uno/shared/vtablefactory.cxx:
> >>>
> >>>       77 extern "C" void * SAL_CALL allocExec(rtl_arena_type *,
> sal_Size *
> >>> size) {
> >>> ...
> >>>       96     sal_Size n = (*size + (pagesize - 1)) & ~(pagesize - 1);
> >>>       97     void * p;
> >>>       98 #if defined SAL_UNX
> >>>       99     p = mmap(
> >>>      100         0, n, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON,
> -1,
> >>>      101         0);
> >>>      102     if (p == MAP_FAILED) {
> >>>      103         p = 0;
> >>>      104     }
> >>>      105     else if (mprotect (static_cast<char*>(p), n, PROT_READ |
> >>> PROT_WRITE | PROT_EXEC) == -1)
> >>>      106     {
> >>>      107         munmap (static_cast<char*>(p), n);
> >>>      108         p = 0;
> >>>      109     }
> >>>      110 #elif defined SAL_W32
> >>>      111     p = VirtualAlloc(0, n, MEM_COMMIT,
> PAGE_EXECUTE_READWRITE);
> >>>      112 #elif defined(SAL_OS2)
> >>>      113     p = 0;
> >>>      114     DosAllocMem( &p, n, PAG_COMMIT | PAG_READ | PAG_WRITE |
> >>> OBJ_ANY);
> >>>      115 #endif
> >>>      116     if (p != 0) {
> >>>      117         *size = n;
> >>>      118     }
> >>>      119     return p;
> >>>      120 }
> >>>
> >>> There are already some workarounds for SELinux in this directory, but
> we
> >>> probably need more.
> >> I don't know SELinux well, nor those syscalls. So I cannot confirm,
> >> unfortunately.
> >>
> >> Do you think we should put a ``workaround for SELinux'' in place,
> >> rather than running execstack as I was suggesting in PR #483?  If so,
> >> could you please point me to some documentation? I have access both to
> >> an ``old'' build VM and a ``new'' Linux-based system, so I can easily
> >> do tests.
> >>
> >>
> > Let's understand this better first.
> >
> > This is an excellent blog post on the topic:
> > https://linuxvox.com/blog/stacks-are-executable-even-with-noexecstack/
> >
> > Now as per that article, note how both FreeBSD and Linux already link
> with
> > -Wl,-z,noexecstack for libraries, at least with gbuild:
> >
> > main/solenv]$ grep execstack * -R
> > gbuild/platform/freebsd.mk:gb_Library_TARGETTYPEFLAGS := -shared
> > -Wl,-z,noexecstack
> > gbuild/platform/linux.mk:gb_Library_TARGETTYPEFLAGS := -shared
> > -Wl,-z,noexecstack
> >
> > but even this seems unnecessary, as that should be the default with GCC
> >=
> > 4.1.
> >
> > A quick audit on my build tree
> >
> (main/instsetoo_native/unxfbsdx/Apache_OpenOffice/installed/install/en-US/openoffice4/program)
> > shows how nothing has executable stacks:
> >
> > find . -type f | while read i; do readelf -l "$i" 2>/dev/null| grep
> > GNU_STACK|grep E; done
> > (no output. You can remove the "grep E" to see every GNU_STACK entry has
> > only "RW" permissions.)
> >
> > It's of course possible that the stack is made executable at run-time,
> > because when libraries needing execstack are loaded, they poison the
> stack
> > of the entire application. Even this can be checked:
> >
> > procstat vm PID |grep rwx
> > (or on Linux, use "cat /proc/PID/map")
> >
> >    PID              START                END PRT  RES PRES REF SHD FLAG
> TP
> > PATH
> >   6460        0x89b9a5000        0x89bc15000 rwx  134  134   1   0 -----
> sw
> >   6460        0x89bf35000        0x89c1a5000 rwx  186  186   1   0 -----
> sw
> >   6460        0x8a346d000        0x8a36dd000 rwx   43   43   1   0 -----
> sw
> >
> > The "sw" node type means swap.
> >
> > Each of those 3 memory mappings is 2.4375 MiB in size, and probably made
> by
> > the bridges code from my earlier post, but isn't a stack.
> >
> > A stack would have flag "D" (grows Down), but none of the mappings with
> > flag "D" have permission "x".
> >
> > So it looks like on FreeBSD noexecstack already works like it should, at
> > build time and run time.
> >
> > Now why is it broken on Linux? I don't have time to check right now,
> please
> > try the above on Linux and post what you get?
>
> maybe this AI code analysis helps (take it with a grain of salt):
>
> SELinux fights AOO because AOO does something SELinux is /specifically
> built to stop/ — and it's not the markings, it's the runtime behavior.
> vtablefactory.cxx:99-109
>
> <vscode-webview://0and1hd7healr0tlpsb02psleiicthqlvde48hdaraebfrgihgpa/main/bridges/source/cpp_uno/shared/vtablefactory.cxx#L99-L109>:
>
> |p = mmap(0, n, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0);
> ... else if (mprotect(p, n, PROT_READ | PROT_WRITE | PROT_EXEC) == -1)
> // W+X anon |
>
> That's the smoking gun. The UNO C++↔UNO bridge *generates machine code
> at runtime* — the vtable trampolines / code snippets (cpp2uno.cxx:412
> |codeSnippet|
> <vscode-webview://0and1hd7healr0tlpsb02psleiicthqlvde48hdaraebfrgihgpa/main/bridges/source/cpp_uno/gcc3_linux_x86-64/cpp2uno.cxx#L412>)
>
> — into anonymous memory it then marks executable. Those are exactly the
> 2.44 MiB |rwx| pools you saw on FreeBSD. AOO is, in effect, a JIT.
>
>
>     Mapping AOO's behavior onto SELinux's permission classes
>
> Vanilla Linux DAC just allows W+X memory. SELinux doesn't — it gates
> executable memory through distinct permissions, and AOO trips four of them:
>
> SELinux perm    What it guards  Where AOO triggers it
> *|execmem|*     anonymous W+X / making writable anon memory executable
> |allocExec|mmap+mprotect to RWX
> <vscode-webview://0and1hd7healr0tlpsb02psleiicthqlvde48hdaraebfrgihgpa/main/bridges/source/cpp_uno/shared/vtablefactory.cxx#L100-L105>—
>
> the bridge snippet pools
> *|execstack|*   |mprotect|-ing the process's own stack to executable
> glibc|_dl_make_stack_executable|when an exec-stack DSO is|dlopen|'d (our
> earlier discussion)
> *|execmod|*     making a/file-backed, relocation-modified/page executable
> *text relocations (TEXTREL)*in shipped|.so|s from non-PIC asm — the
> loader writes into|.text|, so the page is W+X
> *|execheap|*    executable heap (|brk|/|sbrk|region)    if exec memory is
> satisfied from the heap rather than a fresh|mmap|
>
> So everything we've discussed lands here: the unmarked/legacy asm →
> |execstack|/|execmod|; the runtime codegen → |execmem|.
>
>
>     Why it's/structural/, not just a markings bug
>
> This is the key distinction from the previous two turns. You can fix
> markings (|.note.GNU-stack|, |-z noexecstack|) and you can fix TEXTRELs
> (build everything |-fPIC|). But |execmem| is *unavoidable by design*: a
> UNO bridge /must/ synthesize trampolines at runtime to glue the C++
> vtable ABI to UNO's dispatch. Even a perfectly noexecstack-clean,
> fully-PIC AOO still calls |allocExec|. So an SELinux-confined domain
> will deny it regardless — the only resolutions are a policy allow rule,
> the |allow_execmem|/|deny_execmem| boolean, or a labeled exception for
> the soffice domain. That's why SELinux singles AOO out where most
> desktop apps sail through: most apps never allocate executable memory;
> AOO does it on every component load.
>
> (There's a partial escape hatch in the same file: the MAP_SHARED fd path
> at line 287
> <vscode-webview://0and1hd7healr0tlpsb02psleiicthqlvde48hdaraebfrgihgpa/main/bridges/source/cpp_uno/shared/vtablefactory.cxx#L287>
>
> uses a *double mapping* — one |RW| view and a separate |RX| view of the
> same shm object — which is W^X-clean and dodges |execmem|-on-anon. But
> it's conditional/fallback, not the default |allocExec| path, so the W+X
> |mprotect| is what actually runs.)
>
>
>     And why FreeBSD didn't fight it
>
> FreeBSD has no SELinux/Type-Enforcement layer in the default install —
> its MAC framework isn't enforcing |execmem|/|execstack|/|execmod|
> equivalents. Combined with rtld not poisoning the stack (prior turn),
> the /same/ JIT-ing bridge code that draws AVC denials on an enforcing
> SELinux box just runs silently on FreeBSD. The behavior is identical;
> only the enforcement regime differs.
>
> So the one-line answer: *SELinux fights AOO because AOO is a JIT* — the
> UNO bridges allocate W+X memory (|execmem|) on every C++ component
> bridge, on top of any residual exec-stack/TEXTREL
> (|execstack|/|execmod|) from legacy asm — and SELinux's whole purpose is
> to deny exactly those operations unless explicitly permitted.
>
> Here is the recommended action by claude (dont know if this is rubbish,
> but maybe it gives more insight):
>
> can we redisign uno in a SELinux save way?
>
> Short answer: yes, and the cleanest fix is well-trodden ground — but
> it's a /source/ redesign of the bridges, which puts it in the same
> bucket as your VCL-lifetime track: a post-migration design effort,
> explicitly outside the "don't touch source" Bazel scope. Let me lay out
> the design space honestly, because there are three very different tiers
> with very different cost/payoff.
>
>
>     What exactly has to go
>
> Recall the SELinux triggers ranked by how fundamental they are:
>
>   * *|execmem|*— the W+X|mprotect|in|allocExec|
>
> <vscode-webview://0and1hd7healr0tlpsb02psleiicthqlvde48hdaraebfrgihgpa/main/bridges/source/cpp_uno/shared/vtablefactory.cxx#L100-L105>./This
>     is the architectural one./Every C++ component load synthesizes
>     machine code.
>   * |execstack|/|execmod|— legacy-asm side effects (unmarked|.s|,
>     TEXTRELs). Fixable with notes/|-fPIC|,*independent*of the redesign.
>
> So "SELinux-safe UNO" = "eliminate runtime-generated executable code
> from the C++ bridge." Everything else is hygiene.
>
>
>     Why the codegen exists (the constraint you have to design around)
>
> The C++↔UNO bridge fakes a C++ vtable. At a virtual call site the /only/
> thing distinguishing slot N from slot M is *the address jumped to* — the
> callee gets no "which slot am I" parameter. So the bridge emits N tiny
> thunks, each baking its own |(vtableOffset, functionOffset)| as an
> immediate, then jumping to the shared |cpp_vtable_call| (cpp2uno.cxx:412
> <vscode-webview://0and1hd7healr0tlpsb02psleiicthqlvde48hdaraebfrgihgpa/main/bridges/source/cpp_uno/gcc3_linux_x86-64/cpp2uno.cxx#L412>).
>
> N distinct entry addresses ⇒ N distinct code blobs. That's the whole
> reason for the JIT.
>
> The key insight: *those thunks are pure functions of their index.*
> Nothing in them depends on runtime data except an integer that's
> bounded. That's exactly the property that lets you precompute them.
>
>
>     Tier 1 — static trampoline table (recommended pragmatic fix)
>
> Generate the thunks at *compile time*: a fixed array of trampolines
> |thunk[0..N_MAX]|, each hardcoded with its own index, living in the
> bridge |.so|'s normal read-only |.text|. |vtablefactory| stops calling
> |mmap|/|mprotect| and just hands out |&thunk[i]|. No anonymous memory,
> no |mprotect|, *no |execmem|* — the code executes from a file-backed,
> never-writable segment, which is precisely what SELinux is happy with.
>
>   * *Precedent:*this is exactly what libffi did in 2021
>     (|FFI_EXEC_STATIC_TRAMP|) to become W^X / SELinux / PaX-clean — same
>     problem, same solution. You're not inventing anything.
>   * *Cost:*pick an|N_MAX|upper bound on slots-per-proxy (UNO interfaces
>     are large but bounded; even 4096 thunks × ~24 B ≈ 100 KB, trivial).
>     The per-slot variable data (|this|-relative routing) is already
>     recoverable from the proxy object, so the thunk only needs to inject
>     the index — which is what makes static precompute work.
>   * *Catch:*the thunks are hand asm/per architecture/(x86, x86-64, arm,
>     …). Each|gcc3_*|bridge needs its static table. That's mechanical but
>     it's N ports.
>
> This is the highest payoff-per-effort: it kills the structural |execmem|
> denial without changing the UNO API at all.
>
>
>     Tier 2 — make the dual-mapping the default (weak for SELinux)
>
> The code already has a W^X path — the |MAP_SHARED| fd double-mapping at
> line 287
> <vscode-webview://0and1hd7healr0tlpsb02psleiicthqlvde48hdaraebfrgihgpa/main/bridges/source/cpp_uno/shared/vtablefactory.cxx#L287>:
>
> write through an |RW| view, execute through a separate |RX| view, never
> W+X simultaneously. Promoting it to the default fixes plain-kernel W^X
> and PaX/grsec. *But it only partially helps SELinux* — mapping a
> writable-backed object |PROT_EXEC| still draws |execmod|/|execmem|
> checks depending on labeling and |memfd| exec seals. So this satisfies
> hardened /kernels/ but not necessarily an /enforcing SELinux policy/.
> Lower priority than Tier 1.
>
>
>     Tier 3 — remove the C++ bridge entirely (the "real" redesign)
>
> The bridge exists only because |cppu|/|cppuhelper| expose UNO objects as
> native C++ objects with real vtables, so C++ code can call them with
> ordinary virtual-call syntax across an ABI boundary. If, at *IDL-compile
> time*, you generate a concrete C++ proxy /implementation/ per interface
> (extend |cppumaker| to emit method bodies that forward to the C
> |uno_Interface::pDispatcher|), there are no synthetic vtables, no
> thunks, no executable memory — the marshalling boundary moves from
> runtime machine code to compile-time-generated C++. All UNO interfaces
> are known via IDL, so nothing is lost by requiring compile-time knowledge.
>
>   * *Result:*the cleanest possible outcome —|execmem|gone by
>     construction, and the same approach generalizes the Java/.NET
>     bridges' rationale.
>   * *Cost:*large. Touches|cppumaker|, the cppu calling convention, every
>     component's build, binary-compat of the C++ UNO ABI. This is a
>     multi-quarter effort and a genuine ABI event.
>
>
>     My recommendation
>
> Do *Tier 1* for the SELinux win and *the asm-hygiene fixes*
> (|.note.GNU-stack| everywhere + |-fPIC|/no-TEXTREL + extend |-z
> noexecstack| to executables) for |execstack|/|execmod|. Together those
> let AOO run under enforcing SELinux with /no/ custom policy module or
> boolean — which is the actual goal. Tier 3 is the "if you're rewriting
> UNO anyway" answer; don't gate the SELinux fix on it.
>
>
> HTH
> Peter
>
> >
> >
> >> Best regards,
> >> --
> >> Arrigo
> >>
> >>
> >>
> > Regards
> > Damjan
> >



As usual, that AI code analysis does get a few things right and comes up
with an interesting idea, but also misses things and gets them wrong.

There are 2 related ideas:
* noexecstack, don't allow executable code on the stack.
* W^X (W XOR X), don't allow any memory page anywhere to be both writable
and executable. This is a superset of noexecstack, and offers greater
protection, as even heap or static memory cannot be used to inject
malicious code.

AFAICT, the problem with rwx permissions for those JIT memory pools is W^X,
not noexecstack.

Let's begin with "Tier 2". The way out of rwx permissions isn't necessarily
a double memory-map, one writable, one executable. As per this article (
https://freebsdfoundation.org/wp-content/uploads/2019/11/Improving-Memory-Permissions-in-FreeBSD.pdf),
"it is usually a simple matter to map pages writable, write generated code
to them, and then make them executable." In other words, a single mapping,
that is first only writable, then only readable and executable, would be
enough - and is simpler and safer than the double memory mapping the AI
proposed, where the same memory is still both writable and executable, but
in different mapped regions.

Tier 3 is a very interesting idea, that could vastly improve portability of
OpenOffice to other architectures, without needing to write any assembly.
Since we have full knowledge of the UNO interface at IDL compile time, yes,
we genuinely could generate the C++ proxy then. So why don't we? Maybe the
answer is performance. With compile-time proxies, we would have to call C++
-> UNO -> C++. With run-time proxies, and both components built with
compatible compilers and ABIs (something we don't know ahead of time), it
can short-circuit to C++ -> C++. But surely the choice of whether to use
the proxy or link objects directly also exists at runtime, so maybe
performance could be equally good. Memory usage could be higher, as each
interface would need its own marshalling code, instead of the
reusable generic marshalling we use now.

I don't really understand tier 1.

Regards
Damjan

Reply via email to