Percio Castelo.
PT Resume
When a shell redirect uncovers an ELF architecture mismatch
← Back to blog

When a shell redirect uncovers an ELF architecture mismatch

11/1/2026 · 4 min · Linux · 476 views

When a shell redirect uncovers an ELF architecture mismatch#

Sometimes a straightforward desktop task reveals a much deeper architectural problem. I recently ran into an unexpected issue that started as a minor tweak to an application launcher on Linux Mint and ended in a technical investigation involving file descriptors, system calls, and ELF binary headers.

The initial goal was simple: pass a Chromium remote debugging flag (--remote-debugging-port) to an Electron application named antigravity, while silencing stdout and stderr in the background:

--remote-debugging-port=31905 > /dev/null 2>&1 &

The command appeared standard, but the resulting behavior defied basic shell expectations.


Desktop launchers and the ghost tabs issue#

The initial attempt involved modifying the desktop launcher file in ~/.local/share/applications/.

The original Exec directive was:

/usr/share/antigravity/antigravity %F

Under the FreeDesktop XDG desktop entry specification, desktop environments dispatch commands using execvp() without invoking a shell. This means redirection operators (>, &, |) are not interpreted natively. When written directly into the Exec line, the launcher passes them as literal arguments in argv[].

To work around this limitation, I wrapped the invocation in a subshell:

sh -c "/usr/share/antigravity/antigravity --remote-debugging-port=31905 %F > /dev/null 2>&1"

The application launched, but produced a bizarre side effect: it opened empty editor tabs named %F, >, null, and 2>&1.

I tried adding the POSIX option terminator (--), which instructs argument parsers to stop processing flags:

/usr/share/antigravity/antigravity --remote-debugging-port=31905 -- %F > /dev/null 2>&1

The tabs continued to appear. I then attempted to replace the shell process using exec:

sh -c 'exec /usr/share/antigravity/antigravity --remote-debugging-port=31905 > /dev/null 2>&1'

The tabs still showed up. The application seemed to bypass shell redirection rules entirely, treating operators as filenames to open in the UI.


Isolating file descriptors with Bash and C#

To determine whether the desktop environment was leaking arguments, I created a clean wrapper script that redirected file descriptor 1 (stdout) and 2 (stderr) in the shell before invoking the binary:

#!/bin/bash
exec > /dev/null
exec 2>&1

/usr/share/antigravity/antigravity --remote-debugging-port=31905 "$@"

Even with this wrapper, the application opened the same ghost tabs.

To eliminate shell interpreters completely, I wrote a C wrapper using low-level system calls (open, dup2, and execv), ensuring a clean argv array without shell parsing:

#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>

int main(int argc, char *argv[]) {
    int devnull = open("/dev/null", O_WRONLY);
    if (devnull != -1) {
        dup2(devnull, 1);
        dup2(devnull, 2);
        close(devnull);
    }

    char *new_argv[argc + 2];
    new_argv[0] = "/usr/share/antigravity/antigravity";
    new_argv[1] = "--remote-debugging-port=31905";

    for (int i = 1; i < argc; i++) {
        new_argv[i+1] = argv[i];
    }
    new_argv[argc + 1] = NULL;

    execv(new_argv[0], new_argv);
    perror("Error executing execv");
    return 1;
}

I compiled and ran the wrapper. The program still opened the exact same ghost tabs. A compiled binary executing execv without shell characters should not pass literal redirection symbols into the process. Something inside the application binary was failing.


Terminal execution and the Exec format error#

I ran the application directly from the console using the system alias script (ragy):

user@host:~$ ragy
🚀 Launching Antigravity with CDP on port 31905...
Warning: 'remote-debugging-port' is not in the list of known options, but still passed to Electron/Chromium.

Immediately following that line, the console output reported:

/usr/bin/antigravity: 62: /usr/share/antigravity/bin/../antigravity: Exec format error

The Exec format error message corresponds to the ENOEXEC error code, returned by the Linux kernel when load_elf_binary() in fs/binfmt_elf.c rejects an executable. This occurs when the magic number is invalid, execute bits are missing, or the ELF architecture header does not match the host CPU.

This explained why the binary failed to launch directly. But why did the graphical interface open with ghost tabs?

The application's wrapper script had an unhandled fallback routine: when the main binary failed with ENOEXEC, it forwarded the original command arguments to a secondary helper binary (language_server), which parsed each redirection symbol (>, &, null) as an open file request.


Inspecting ELF headers with readelf and file#

With the format error exposed, I inspected the binary header on the x86-64 host:

readelf -h /usr/share/antigravity/antigravity

The output revealed the root cause:

  Class: ELF64
  Type: DYN (Position-Independent Executable file)
  Machine: AArch64
  Version: 0x1

The host was running an Intel/AMD 64-bit CPU (x86-64), but the installed package was built for 64-bit ARM (AArch64).

Scanning all binaries in the application folder:

find /usr/share/antigravity/ -executable -type f -exec file {} + | grep ELF

Confirmed the problem across the directory:

/usr/share/antigravity/antigravity: ELF 64-bit LSB pie executable, ARM aarch64...
/usr/share/antigravity/libvulkan.so.1: ELF 64-bit LSB shared object, ARM aarch64...
/usr/share/antigravity/chrome-sandbox: setuid ELF 64-bit LSB pie executable, ARM aarch64...
/usr/share/antigravity/resources/app/extensions/antigravity/bin/language_server_linux_x64: ELF 64-bit LSB pie executable, x86-64...

Nearly all files were ARM binaries. The only x86-64 executable was the secondary language server. When the main binary crashed with ENOEXEC, the wrapper passed the command string into that secondary process, triggering the unexpected tabs.

Purging the mismatched package (sudo apt purge antigravity) and installing the correct amd64/x86_64 build resolved the issue. The standard launcher command worked immediately without opening ghost tabs.


From command lines to ELF headers: practical takeaways#

This troubleshooting experience highlights three practical habits when working with Linux desktop environments:

  1. Watch for unexpected fallback logic in wrapper scripts: when an application fails to respect standard shell redirects or low-level dup2 calls, the main binary may be silently failing and falling back to secondary helpers.
  2. Test directly in a terminal before writing complex wrappers: launching the command in a raw console would have surfaced the Exec format error long before writing C code or custom shell wrappers.
  3. Inspect binary architecture headers using readelf or file: when manually installed packages behave erratically, checking ELF headers confirms whether the binary matches the host CPU architecture.

Was this article helpful?

Leave a quick reaction to help prioritize future technical guides:

476 reads
Creative Commons Attribution-NonCommercial

This post is licensed under CC BY-NC.

Comments

Join the discussion below.

0 comments