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