Skip to content

setup_linux: Use LDT fallback directly#113

Open
AlgebraManiacABC wants to merge 3 commits intodecompals:mainfrom
AlgebraManiacABC:patch-1
Open

setup_linux: Use LDT fallback directly#113
AlgebraManiacABC wants to merge 3 commits intodecompals:mainfrom
AlgebraManiacABC:patch-1

Conversation

@AlgebraManiacABC
Copy link

Problem

On Linux environments where the seccomp filter blocks set_thread_area
(i386 syscall 243), wibo is killed by SIGSYS during thread setup. The signal
arrives before the existing LDT fallback can execute, so the process never
recovers.

Fix

Initialize g_threadAreaEntry to -2 instead of -1 in
src/setup_linux.cpp. The value -2 is already the sentinel meaning "skip
set_thread_area and use modify_ldt"; this change simply makes that the
default rather than a fallback reached only after a failed attempt.

-int g_threadAreaEntry = -1;
+int g_threadAreaEntry = -2;

Agentic Coding

This fixes wibo to work under Claude's Cowork feature, since modify_ldt is
permitted by the seccomp policies there. As an alternative, I wonder if
there is a way to determine the seccomp policies beforehand?
The answer is left as an exercise for the interested reader.

@encounter
Copy link
Member

This is quite a hack, it disables the entire set_thread_area codepath without even leaving a comment. It'd be better if we could detect the lack of set_thread_area or handle and recover from SIGSYS if possible

@AlgebraManiacABC
Copy link
Author

I turned out to be the "interested reader." I added SIGSYS handling for a preliminary 64-bit syscall, and on fail it uses LDT. If the 64-bit syscall works, it continues on to using the 32-bit version.

I have tried this with success in Claude Cowork in version 1.1.5749 (ecf3d9).

@encounter
Copy link
Member

encounter commented Mar 10, 2026

The "preliminary 64-bit syscall" is not actually probing set_thread_area. In the x86-64 syscall table, set_thread_area is syscall 205, while 243 is mq_timedreceive. So this line:

syscall(243, desc)

does not hit the compat set_thread_area path at all. It is probing the wrong syscall number in the wrong table. On my machine, syscall(243, &desc) returns EBADF, and native syscall(SYS_set_thread_area, &desc) returns ENOSYS, which also matches the documented x86-64 behavior for native set_thread_area.

There is also a seccomp ABI mismatch here. The existing asm path uses int 0x80, which enters the ia32 compat syscall path via do_int80_emulation(). That path sets TS_COMPAT, and seccomp then reports the syscall architecture as AUDIT_ARCH_I386 when TS_COMPAT is set, otherwise AUDIT_ARCH_X86_64. So even if a 64-bit probe "worked", it still would not prove that the real compat set_thread_area call is allowed by the filter. They are different codepaths from seccomp’s point of view.

I think the better approach is to guard the real compat syscall directly instead of trying to probe it indirectly first.

The note in the patch saying the SIGSYS handler "must NOT" be used for the asm path is misleading here. wibo is still a normal x86-64 process, even if it temporarily far-jumps into compat code and executes int 0x80. On x86-64, Linux chooses the signal frame ABI based on the handler flags, not just on "you happened to be in compat code at the time": signal.c checks SA_IA32_ABI to decide whether to build an ia32 frame, and otherwise setup_rt_frame() uses the normal x64 frame path. The x64 signal setup code also explicitly says it will run the handler in 64-bit mode even if the signal interrupted 32-bit or 16-bit code.

What I would suggest instead is:

  1. Keep the real compat set_thread_area path (setThreadArea64() in asm).
  2. Install a narrow SIGSYS handler with SA_SIGINFO.
  3. In the handler, only intercept the exact seccomp case you expect:
    • info->si_code == SYS_SECCOMP
    • info->si_arch == AUDIT_ARCH_I386
    • info->si_syscall == 243
  4. For that case, patch the interrupted context’s return register:
    • uc->uc_mcontext.gregs[REG_RAX] = -ENOSYS;
  5. Return from the handler. Execution should resume after int 0x80, and the existing asm/C++ path can fall back to LDT.

Something along these lines:

static void sigsysHandler(int, siginfo_t *info, void *ctx) {
    auto *uc = static_cast<ucontext_t *>(ctx);

    if (info->si_code != SYS_SECCOMP ||
        info->si_arch != AUDIT_ARCH_I386 ||
        info->si_syscall != 243) {
        return;
    }

    uc->uc_mcontext.gregs[REG_RAX] = -ENOSYS;
}

and then:

struct sigaction sa = {}, oldSa = {};
sa.sa_sigaction = sigsysHandler;
sa.sa_flags = SA_SIGINFO;
sigemptyset(&sa.sa_mask);
sigaction(SIGSYS, &sa, &oldSa);

int ret = setThreadArea64(g_threadAreaEntry, teb);

sigaction(SIGSYS, &oldSa, nullptr);

One caveat: this only helps if the seccomp policy uses a recoverable action that delivers SIGSYS, i.e. SECCOMP_RET_TRAP. If the environment uses KILL_PROCESS / KILL_THREAD, there is nothing to catch.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants