This is an automated email from the git hooks/post-receive script.

git pushed a commit to branch wl/browser-all
in repository enlightenment.

View the commit online.

commit f5b6bdf2ac7cd1fb3c0729efd0793137ef478ce7
Author: Cedric BAIL <[email protected]>
AuthorDate: Thu Aug 20 12:10:55 2026 -0600

    e_comp_wl - only named programs may be an input method
    
    Two of the protocols E now speaks are not like the others.
    zwp_input_method_v2 reads every keystroke typed into a text field;
    zwp_virtual_keyboard_v1 types into whatever window is focused. Both are
    exactly what an input method needs and exactly what a keylogger needs,
    and a bare Wayland connection gives the compositor nothing to tell those
    apart.
    
    The protocol says so itself - virtual-keyboard asks a compositor to
    "present an error when an untrusted client requests a new keyboard" and
    defines an unauthorized error for it. Everyone advertises both to
    everyone anyway, because there is nothing to test a client against. So
    this asks the only party who knows: the person running the machine, via
    a file naming the programs they mean to trust.
    
        <prefix>/share/enlightenment/wayland-privileges/   packagers
        ~/.e/e/wayland-privileges/                         the user
    
    .desktop files, granting through a key rather than through the
    directory - so one copied there by accident grants nothing, and the next
    privileged protocol needs a name rather than a new directory:
    
        X-Enlightenment-Wayland-Privileges=virtual_keyboard;input_method;
    
    Exec's first word is resolved the way a shell would and compared against
    the running program's /proc/pid/exe, so Exec=fcitx5 matches whichever
    fcitx5 is on PATH.
    
    **A refused client never sees the global.** wl_display_set_global_filter
    is consulted before a global is advertised, so a program not on the list
    finds a compositor that appears not to implement the protocol - a case
    every input method already handles, having run on compositors that
    genuinely do not. Advertising and then refusing the request would
    produce a protocol error and a dead connection, which is a far worse
    thing to hand someone whose input method stopped working.
    
    Deny is the default, so fcitx5.desktop and ibus.desktop are installed
    with the compositor. Without them an upgrade would silently stop a
    working input method and leave no way to find out why. Refusals are
    logged with the program's path for the same reason.
    
    **Two things this is not.** The trust boundary is the config directory:
    anyone who can write ~/.e/e can add themselves, but they could equally
    add a startup application, so it is the same hole rather than a new one.
    What it buys is that a program you merely *ran* cannot quietly become
    your keylogger. And sandboxed clients cannot be matched at all - a
    Flatpak's /proc/pid/exe is bwrap - which is what wp_security_context_v1
    exists for; it is in the wayland-protocols this tree builds against and
    is not implemented here.
    
    The tests use the real mechanism rather than an escape hatch.
    run-nested.sh writes an actual allow-file into the throwaway HOME for
    the tests that play an input method, and e_wlcs.c does the same for
    itself - otherwise the eleven TextInputV3WithInputMethodV2 tests would
    go back to being skipped, which reads as coverage vanishing rather than
    as a policy working. Every other test is an ordinary client, which is
    what gives wl-privileges' deny assertion its meaning.
    
    The strongest check is the fcitx5 one, and it needed no new test: it is
    not on the harness allowlist, so it passes only because the installed
    fcitx5.desktop matches the fcitx5 on PATH. Pointing that file at a
    different binary makes it fail with "fcitx5 never took the seat's input
    method" - checked.
    
    Measured. Full wlcs 780 passed / 14 failed, failure set identical to
    abe50aad2, and the E-13 filter is still 9 of 11. In-tree 39 -> 40.
    globals.expected loses the two globals, because the client that writes
    it is an ordinary one - which is the visible half of the whole change.
---
 data/wayland-privileges/README         |  42 ++++
 data/wayland-privileges/fcitx5.desktop |   7 +
 data/wayland-privileges/ibus.desktop   |   7 +
 data/wayland-privileges/meson.build    |  11 +
 meson.build                            |   1 +
 src/bin/e_comp_wl.c                    |   1 +
 src/bin/e_comp_wl_extensions.c         |   5 +
 src/bin/e_comp_wl_privileges.c         | 357 +++++++++++++++++++++++++++++++++
 src/bin/e_comp_wl_privileges.h         |  22 ++
 src/bin/e_includes.h                   |   1 +
 src/bin/meson.build                    |   2 +
 src/tests/wayland/globals.expected     |   2 -
 src/tests/wayland/meson.build          |  31 ++-
 src/tests/wayland/run-nested.sh        |  18 ++
 src/tests/wayland/test_fcitx5.c        |   8 +-
 src/tests/wayland/test_privileges.c    |  71 +++++++
 src/tests/wlcs/e_wlcs.c                |  87 ++++++--
 17 files changed, 649 insertions(+), 24 deletions(-)

diff --git a/data/wayland-privileges/README b/data/wayland-privileges/README
new file mode 100644
index 000000000..1092e070a
--- /dev/null
+++ b/data/wayland-privileges/README
@@ -0,0 +1,42 @@
+Programs allowed to be an input method
+======================================
+
+A .desktop file here lets one program use the two Wayland protocols that are
+not safe to hand to anything that asks:
+
+    zwp_input_method_v2       reads every keystroke typed into a text field
+    zwp_virtual_keyboard_v1   types into whatever window is focused
+
+Both are what an input method needs, and both are what a keylogger needs. The
+wire gives the compositor no way to tell those apart, so the decision is made
+here instead. A program not listed does not see the globals at all - to it, E
+looks like a compositor without input method support, which is a case every
+input method already handles.
+
+The key is what grants; the directory alone does not:
+
+    X-Enlightenment-Wayland-Privileges=virtual_keyboard;input_method;
+
+Exec names the program. Its first word is resolved the way a shell would
+resolve it and compared against the running program, so `Exec=fcitx5` matches
+whichever fcitx5 is on PATH.
+
+Two directories are read, system first and then the user's:
+
+    <prefix>/share/enlightenment/wayland-privileges/   this one, for packagers
+    ~/.e/e/wayland-privileges/                         for the person at the
+                                                       keyboard
+
+Nothing here removes a privilege - it is an allowlist. To stop trusting a
+program, delete or edit the file that granted it.
+
+Two limits worth knowing.
+
+Anyone who can write your config directory can add a file here, so this does
+not defend against someone who already has that. It is not meant to: what it
+buys is that a program you merely *ran* cannot quietly become your keylogger.
+
+Flatpak and Snap applications cannot be matched this way, because the program
+behind the connection is bwrap or snap-confine rather than the application.
+The answer for those is wp_security_context_v1, where the sandbox tags the
+connection when it creates it. E does not implement it yet.
diff --git a/data/wayland-privileges/fcitx5.desktop b/data/wayland-privileges/fcitx5.desktop
new file mode 100644
index 000000000..918115a25
--- /dev/null
+++ b/data/wayland-privileges/fcitx5.desktop
@@ -0,0 +1,7 @@
+[Desktop Entry]
+Type=Application
+Name=Fcitx 5
+Comment=Allowed to act as this session's input method
+Exec=fcitx5
+NoDisplay=true
+X-Enlightenment-Wayland-Privileges=virtual_keyboard;input_method;
diff --git a/data/wayland-privileges/ibus.desktop b/data/wayland-privileges/ibus.desktop
new file mode 100644
index 000000000..534ec3a99
--- /dev/null
+++ b/data/wayland-privileges/ibus.desktop
@@ -0,0 +1,7 @@
+[Desktop Entry]
+Type=Application
+Name=IBus
+Comment=Allowed to act as this session's input method
+Exec=ibus-daemon
+NoDisplay=true
+X-Enlightenment-Wayland-Privileges=virtual_keyboard;input_method;
diff --git a/data/wayland-privileges/meson.build b/data/wayland-privileges/meson.build
new file mode 100644
index 000000000..4d8fcde5e
--- /dev/null
+++ b/data/wayland-privileges/meson.build
@@ -0,0 +1,11 @@
+# Programs allowed to be an input method. See the README next to this file.
+#
+# Installed rather than optional: the default is deny, so without these an
+# upgrade would silently stop a working fcitx5 from being an input method at
+# all, and the user would have no way to know why.
+install_data([ 'fcitx5.desktop',
+               'ibus.desktop',
+               'README'
+             ],
+             install_dir: join_paths(dir_data, 'enlightenment', 'wayland-privileges')
+            )
diff --git a/meson.build b/meson.build
index a0bee8aff..ed769eb25 100644
--- a/meson.build
+++ b/meson.build
@@ -407,6 +407,7 @@ endif
 subdir('data/backgrounds')
 subdir('data/config')
 subdir('data/desktop')
+subdir('data/wayland-privileges')
 subdir('data/etc')
 subdir('data/favorites')
 subdir('data/flags')
diff --git a/src/bin/e_comp_wl.c b/src/bin/e_comp_wl.c
index 795e6cbfd..6fb078af8 100644
--- a/src/bin/e_comp_wl.c
+++ b/src/bin/e_comp_wl.c
@@ -4633,6 +4633,7 @@ e_comp_wl_shutdown(void)
    E_Comp_Wl_Output *output;
 
    e_comp_wl_text_input_shutdown();
+   e_comp_wl_privileges_shutdown();
 
    EINA_LIST_FREE(e_comp_wl->outputs, output)
      {
diff --git a/src/bin/e_comp_wl_extensions.c b/src/bin/e_comp_wl_extensions.c
index 5680c9e30..b75b313fc 100644
--- a/src/bin/e_comp_wl_extensions.c
+++ b/src/bin/e_comp_wl_extensions.c
@@ -2417,6 +2417,11 @@ e_comp_wl_extensions_init(void)
    GLOBAL_CREATE_OR_RETURN(zwp_idle_inhibit_manager_v1, zwp_idle_inhibit_manager_v1_interface, 1);
    GLOBAL_CREATE_OR_RETURN(zwp_keyboard_shortcuts_inhibit_manager_v1, zwp_keyboard_shortcuts_inhibit_manager_v1_interface, 1);
 
+   /* Before the globals below, because the filter has to be in place when
+    * they are created - a client that binds in the window between would get
+    * one it should not have. */
+   e_comp_wl_privileges_init();
+
    /* text-input v3 and input-method v2 create their own globals, in
     * e_comp_wl_text_input.c. They are one state machine spread over two
     * globals, and the macro above assumes one global per feature with its
diff --git a/src/bin/e_comp_wl_privileges.c b/src/bin/e_comp_wl_privileges.c
new file mode 100644
index 000000000..3f590674a
--- /dev/null
+++ b/src/bin/e_comp_wl_privileges.c
@@ -0,0 +1,357 @@
+/* Which programs may act as your input method, or type on your behalf.
+ *
+ * Two of the protocols E speaks are not like the others.
+ * zwp_virtual_keyboard_v1 lets a client synthesise keystrokes into whatever
+ * window is focused; zwp_input_method_v2 lets a client read every keystroke
+ * that goes into a text field. Both are exactly what an input method needs and
+ * exactly what a keylogger needs, and the wire gives the compositor no way to
+ * tell those apart.
+ *
+ * The protocol says so itself. virtual-keyboard: "If the compositor enables a
+ * keyboard to perform arbitrary actions, it should present an error when an
+ * untrusted client requests a new keyboard", and it defines an `unauthorized`
+ * error for saying so. Every compositor that supports input methods today
+ * advertises both globals to everyone, because there is nothing in a bare
+ * Wayland connection to test a client against.
+ *
+ * So this asks the only party that knows: the person running the machine, by
+ * way of a file naming the programs they mean to trust.
+ *
+ * **A refused client never sees the global.** wl_display_set_global_filter is
+ * consulted before a global is advertised, so a program that is not on the
+ * list finds a compositor that appears not to implement the protocol - which
+ * is a situation every input method already handles, having run on compositors
+ * that genuinely do not. The alternative, advertising it and then refusing the
+ * request, produces a protocol error and a killed connection, which is a much
+ * worse thing to hand a user whose input method just stopped.
+ *
+ * The list is .desktop files, in two places:
+ *
+ *   $prefix/share/enlightenment/wayland-privileges/   - packagers
+ *   ~/.e/e/wayland-privileges/                        - the user
+ *
+ * with a key saying what is granted:
+ *
+ *   X-Enlightenment-Wayland-Privileges=virtual_keyboard;input_method;
+ *
+ * The key matters rather than only the directory: a .desktop file copied there
+ * by accident grants nothing, and the next privileged protocol needs a new
+ * name rather than a new directory.
+ *
+ * **What this is not.** The trust boundary is the config directory. Anyone who
+ * can write ~/.e/e/ can add themselves to the list - but they can equally add
+ * a startup application, so this is not a new hole; it is the same hole, and
+ * saying otherwise would be overselling it. What it does buy is that a program
+ * you merely *ran* cannot silently become your keylogger.
+ *
+ * **Sandboxes are not covered.** A Flatpak or Snap client's /proc/pid/exe is
+ * bwrap or snap-confine, not the application, so matching an executable cannot
+ * work for them. That case has a real answer - wp_security_context_v1, which
+ * is in the wayland-protocols this tree builds against - where the sandbox
+ * tags the connection at creation time. It is complementary to this, not a
+ * replacement, and it is not implemented here.
+ */
+#define E_COMP_WL
+#include "e.h"
+#include <Efreet.h>
+
+typedef struct
+{
+   /* The resolved absolute path of the program, when it could be resolved,
+    * and its basename, which is the weaker fallback. */
+   Eina_Stringshare *exe;
+   Eina_Stringshare *base;
+   unsigned int privileges;
+   Eina_Stringshare *source;   /* the file that granted it, for the log */
+} Privileged_App;
+
+static Eina_List *_apps;
+static Eina_Bool _filter_installed;
+
+static const struct
+{
+   const char *name;
+   unsigned int bit;
+} _privilege_names[] =
+{
+   { "virtual_keyboard", E_COMP_WL_PRIVILEGE_VIRTUAL_KEYBOARD },
+   { "input_method",     E_COMP_WL_PRIVILEGE_INPUT_METHOD },
+};
+
+static unsigned int
+_privilege_parse(const char *list)
+{
+   unsigned int got = 0;
+   const char *p = list;
+
+   /* "a;b;" or "a;b" or "a b" - accept the desktop-entry list separator and
+    * whitespace, because a file written by hand will use whichever the author
+    * happened to remember. */
+   while (p && *p)
+     {
+        const char *end = p;
+        size_t len;
+        unsigned int i;
+
+        while (*end && (*end != ';') && (*end != ',') && (*end != ' ')) end++;
+        len = end - p;
+        for (i = 0; i < EINA_C_ARRAY_LENGTH(_privilege_names); i++)
+          {
+             if (strlen(_privilege_names[i].name) != len) continue;
+             if (strncmp(p, _privilege_names[i].name, len)) continue;
+             got |= _privilege_names[i].bit;
+          }
+        p = (*end) ? end + 1 : end;
+     }
+   return got;
+}
+
+/* The program a desktop entry's Exec refers to, as an absolute path.
+ *
+ * Exec is a command line, not an identity: it carries arguments and field
+ * codes, and may name a program on PATH rather than by path. Only the first
+ * token is a program name, and only that is compared. */
+static char *
+_exec_resolve(const char *exec)
+{
+   char first[PATH_MAX];
+   const char *p = exec;
+   size_t n = 0;
+   char *path, *dir, *save;
+
+   if (!exec) return NULL;
+   while (*p == ' ') p++;
+   /* A quoted program name is legal in a desktop entry. */
+   if ((*p == '"') || (*p == '\''))
+     {
+        char q = *p++;
+
+        while (*p && (*p != q) && (n < sizeof(first) - 1)) first[n++] = *p++;
+     }
+   else
+     while (*p && (*p != ' ') && (n < sizeof(first) - 1)) first[n++] = *p++;
+   first[n] = 0;
+   if (!first[0]) return NULL;
+
+   if (first[0] == '/') return strdup(first);
+
+   /* Look it up the way a shell would. */
+   path = getenv("PATH") ? strdup(getenv("PATH")) : NULL;
+   if (!path) return strdup(first);
+   for (dir = strtok_r(path, ":", &save); dir; dir = strtok_r(NULL, ":", &save))
+     {
+        char buf[PATH_MAX];
+
+        if (snprintf(buf, sizeof(buf), "%s/%s", dir, first) >= (int)sizeof(buf))
+          continue;
+        if (!access(buf, X_OK))
+          {
+             char *real = realpath(buf, NULL);
+
+             free(path);
+             return real ?: strdup(buf);
+          }
+     }
+   free(path);
+   return strdup(first);
+}
+
+static void
+_app_add(const char *file)
+{
+   Efreet_Desktop *desktop;
+   Privileged_App *app;
+   const char *priv;
+   unsigned int bits;
+   char *exe;
+
+   desktop = efreet_desktop_new(file);
+   if (!desktop) return;
+
+   priv = eina_hash_find(desktop->x, "X-Enlightenment-Wayland-Privileges");
+   bits = priv ? _privilege_parse(priv) : 0;
+   if (!bits)
+     {
+        /* A desktop file with no privilege key grants nothing. Said out loud,
+         * because a file placed here deliberately and ignored silently is a
+         * user wondering why their input method does not work. */
+        if (!priv)
+          INF("wayland-privileges: %s has no "
+              "X-Enlightenment-Wayland-Privileges key, ignoring", file);
+        else
+          INF("wayland-privileges: %s grants nothing recognised ('%s')",
+              file, priv);
+        efreet_desktop_free(desktop);
+        return;
+     }
+
+   exe = _exec_resolve(desktop->exec);
+   if (!exe)
+     {
+        INF("wayland-privileges: %s has no usable Exec", file);
+        efreet_desktop_free(desktop);
+        return;
+     }
+
+   app = E_NEW(Privileged_App, 1);
+   if (app)
+     {
+        const char *slash = strrchr(exe, '/');
+
+        app->exe = eina_stringshare_add(exe);
+        app->base = eina_stringshare_add(slash ? slash + 1 : exe);
+        app->privileges = bits;
+        app->source = eina_stringshare_add(file);
+        _apps = eina_list_append(_apps, app);
+        INF("wayland-privileges: %s may use %s%s%s", exe,
+            (bits & E_COMP_WL_PRIVILEGE_VIRTUAL_KEYBOARD) ? "virtual_keyboard" : "",
+            ((bits & E_COMP_WL_PRIVILEGE_VIRTUAL_KEYBOARD) &&
+             (bits & E_COMP_WL_PRIVILEGE_INPUT_METHOD)) ? ", " : "",
+            (bits & E_COMP_WL_PRIVILEGE_INPUT_METHOD) ? "input_method" : "");
+     }
+   free(exe);
+   efreet_desktop_free(desktop);
+}
+
+static void
+_dir_scan(const char *dir)
+{
+   Eina_List *files;
+   char *file;
+
+   if (!ecore_file_is_dir(dir)) return;
+   files = ecore_file_ls(dir);
+   EINA_LIST_FREE(files, file)
+     {
+        char path[PATH_MAX];
+
+        if (eina_str_has_extension(file, ".desktop") &&
+            (snprintf(path, sizeof(path), "%s/%s", dir, file) < (int)sizeof(path)))
+          _app_add(path);
+        free(file);
+     }
+}
+
+/* The program behind a connection, or NULL if it cannot be established.
+ *
+ * Read at the moment the question is asked rather than remembered from
+ * connect: /proc/<pid>/exe follows the process, so this is what is running
+ * now. A client could in principle exec something else after connecting and
+ * keep the socket, but the Wayland fd is close-on-exec for anyone using the
+ * ordinary library, so that takes deliberate effort - and a program willing to
+ * go to that trouble has easier ways to attack a session it is already inside.
+ */
+static char *
+_client_exe_get(struct wl_client *client)
+{
+   pid_t pid = 0;
+   uid_t uid = 0;
+   gid_t gid = 0;
+   char link[64];
+
+   wl_client_get_credentials(client, &pid, &uid, &gid);
+   if (pid <= 0) return NULL;
+   snprintf(link, sizeof(link), "/proc/%d/exe", (int)pid);
+   return realpath(link, NULL);
+}
+
+EINTERN Eina_Bool
+e_comp_wl_privileges_client_allowed(struct wl_client *client, unsigned int privilege)
+{
+   Privileged_App *app;
+   Eina_List *l;
+   char *exe;
+   const char *base;
+   Eina_Bool ok = EINA_FALSE;
+
+   if (!client) return EINA_FALSE;
+   exe = _client_exe_get(client);
+   if (!exe) return EINA_FALSE;
+   base = strrchr(exe, '/');
+   base = base ? base + 1 : exe;
+
+   EINA_LIST_FOREACH(_apps, l, app)
+     {
+        if (!(app->privileges & privilege)) continue;
+        /* The resolved path is the real check. The basename is a fallback for
+         * the case the path could not be resolved from Exec - a desktop entry
+         * whose Exec is a shell wrapper, most often - and it is weaker on
+         * purpose: two programs can share a name. */
+        if (eina_streq(app->exe, exe) ||
+            ((app->exe[0] != '/') && eina_streq(app->base, base)))
+          {
+             ok = EINA_TRUE;
+             break;
+          }
+     }
+
+   if (!ok)
+     INF("wayland-privileges: refused %s - no file in "
+         "share/enlightenment/wayland-privileges or ~/.e/e/wayland-privileges "
+         "grants it", exe);
+
+   free(exe);
+   return ok;
+}
+
+/* Consulted before any global is advertised to any client.
+ *
+ * Everything not named here is public, and returning true early for those is
+ * what keeps this cheap: the filter runs once per global per registry, so the
+ * roughly thirty globals E advertises must not each cost a /proc read. */
+static bool
+_global_filter(const struct wl_client *client, const struct wl_global *global, void *data EINA_UNUSED)
+{
+   const struct wl_interface *iface = wl_global_get_interface(global);
+   unsigned int need;
+
+   if (!iface) return true;
+   if (!strcmp(iface->name, "zwp_virtual_keyboard_manager_v1"))
+     need = E_COMP_WL_PRIVILEGE_VIRTUAL_KEYBOARD;
+   else if (!strcmp(iface->name, "zwp_input_method_manager_v2"))
+     need = E_COMP_WL_PRIVILEGE_INPUT_METHOD;
+   else
+     return true;
+
+   /* The const is libwayland's; the credentials call does not modify it. */
+   return !!e_comp_wl_privileges_client_allowed((struct wl_client *)client, need);
+}
+
+EINTERN void
+e_comp_wl_privileges_init(void)
+{
+   char buf[PATH_MAX];
+
+   /* System first, then the user - so a user file can add to what a packager
+    * shipped. Nothing removes: this is an allowlist, and a user who wants to
+    * take a program off it edits or deletes the file that granted it. */
+   e_prefix_data_snprintf(buf, sizeof(buf), "wayland-privileges");
+   _dir_scan(buf);
+   e_user_dir_snprintf(buf, sizeof(buf), "wayland-privileges");
+   _dir_scan(buf);
+
+   if (!_apps)
+     INF("wayland-privileges: nothing is allowed to be an input method or a "
+         "virtual keyboard; those globals will not be advertised to anyone");
+
+   wl_display_set_global_filter(e_comp_wl->wl.disp, _global_filter, NULL);
+   _filter_installed = EINA_TRUE;
+}
+
+EINTERN void
+e_comp_wl_privileges_shutdown(void)
+{
+   Privileged_App *app;
+
+   if (_filter_installed && e_comp_wl && e_comp_wl->wl.disp)
+     wl_display_set_global_filter(e_comp_wl->wl.disp, NULL, NULL);
+   _filter_installed = EINA_FALSE;
+
+   EINA_LIST_FREE(_apps, app)
+     {
+        eina_stringshare_del(app->exe);
+        eina_stringshare_del(app->base);
+        eina_stringshare_del(app->source);
+        free(app);
+     }
+}
diff --git a/src/bin/e_comp_wl_privileges.h b/src/bin/e_comp_wl_privileges.h
new file mode 100644
index 000000000..a9ba8f961
--- /dev/null
+++ b/src/bin/e_comp_wl_privileges.h
@@ -0,0 +1,22 @@
+#ifdef E_TYPEDEFS
+#else
+# ifndef E_COMP_WL_PRIVILEGES_H
+#  define E_COMP_WL_PRIVILEGES_H
+
+/* Protocols that are not safe to hand to any client that asks. See the top of
+ * e_comp_wl_privileges.c for why these two and not others. */
+#  define E_COMP_WL_PRIVILEGE_VIRTUAL_KEYBOARD (1 << 0)
+#  define E_COMP_WL_PRIVILEGE_INPUT_METHOD     (1 << 1)
+
+EINTERN void e_comp_wl_privileges_init(void);
+EINTERN void e_comp_wl_privileges_shutdown(void);
+
+/* Whether this connection's program is on the list for a privilege.
+ *
+ * Normally not called directly: the global filter installed by
+ * e_comp_wl_privileges_init hides the globals from clients that would be
+ * refused, so a disallowed client never reaches the protocol at all. */
+EINTERN Eina_Bool e_comp_wl_privileges_client_allowed(struct wl_client *client, unsigned int privilege);
+
+# endif
+#endif
diff --git a/src/bin/e_includes.h b/src/bin/e_includes.h
index e888afb2a..e54476f07 100644
--- a/src/bin/e_includes.h
+++ b/src/bin/e_includes.h
@@ -169,4 +169,5 @@
 # include "e_comp_wl_dmabuf.h"
 # include "e_comp_wl_input.h"
 # include "e_comp_wl_text_input.h"
+# include "e_comp_wl_privileges.h"
 #endif
diff --git a/src/bin/meson.build b/src/bin/meson.build
index 14abe851b..b256accd4 100644
--- a/src/bin/meson.build
+++ b/src/bin/meson.build
@@ -424,6 +424,7 @@ if config_h.has('HAVE_WAYLAND') == true
     'e_comp_wl_extensions.c',
     'e_comp_wl_extensions_tizen.c',
     'e_comp_wl_text_input.c',
+    'e_comp_wl_privileges.c',
     wayland_proto_c,
     wayland_proto_h
   ]
@@ -432,6 +433,7 @@ if config_h.has('HAVE_WAYLAND') == true
     'e_comp_wl_dmabuf.h',
     'e_comp_wl_input.h',
     'e_comp_wl_text_input.h',
+    'e_comp_wl_privileges.h',
     'e_comp_wl.h'
   ]
   deps_e += dep_wayland
diff --git a/src/tests/wayland/globals.expected b/src/tests/wayland/globals.expected
index b1a796f70..18349bd2b 100644
--- a/src/tests/wayland/globals.expected
+++ b/src/tests/wayland/globals.expected
@@ -17,13 +17,11 @@ xdg_toplevel_drag_manager_v1	1
 xdg_wm_base	6
 zwp_e_session_recovery	1
 zwp_idle_inhibit_manager_v1	1
-zwp_input_method_manager_v2	1
 zwp_keyboard_shortcuts_inhibit_manager_v1	1
 zwp_pointer_constraints_v1	1
 zwp_primary_selection_device_manager_v1	1
 zwp_relative_pointer_manager_v1	1
 zwp_text_input_manager_v3	1
-zwp_virtual_keyboard_manager_v1	1
 zxdg_decoration_manager_v1	1
 zxdg_exporter_v1	1
 zxdg_exporter_v2	1
diff --git a/src/tests/wayland/meson.build b/src/tests/wayland/meson.build
index 190cffa51..fc7d09049 100644
--- a/src/tests/wayland/meson.build
+++ b/src/tests/wayland/meson.build
@@ -100,6 +100,7 @@ wl_protocol_tests = [
   ['input-method-keyboard', 'test_input_method_keyboard.c'],
   ['input-popup', 'test_input_popup.c'],
   ['no-dialogs', 'test_no_dialogs.c'],
+  ['privileges', 'test_privileges.c'],
 ]
 
 # Shared plumbing: registry binding, toplevel construction, enumeration and a
@@ -114,15 +115,34 @@ tk_src = files('e_wl_testkit.c')
 # browser is a skip (exit 77), not a failure.
 browser_run = meson.current_source_dir() / 'browser-run.sh'
 
+# Tests that play an input method, and so need to be on the allowlist E reads
+# from wayland-privileges. run-nested.sh writes a real .desktop naming the test
+# binary into the throwaway HOME, so these exercise the mechanism rather than
+# stepping around it - and every test *not* named here is an ordinary client,
+# which is what makes wl-privileges' deny assertion mean something.
+wl_privileged_tests = [
+  'text-input',
+  'input-method-keyboard',
+  'input-popup',
+]
+
 foreach t: wl_protocol_tests
   exe = executable('test_wl_' + t[0].underscorify(),
     [t[1], tk_src, test_proto_src],
     dependencies: [dependency('wayland-client')],
   )
+  test_env = wl_test_env
+  if wl_privileged_tests.contains(t[0])
+    test_env = environment()
+    test_env.set('E_TEST_BIN', e_test_bin)
+    test_env.set('E_TEST_MODULE_SO', wl_test_module_so)
+    test_env.set('E_TEST_MODULE_ARCH', module_arch)
+    test_env.set('E_TEST_WL_PRIVILEGED', '1')
+  endif
   test('wl-' + t[0],
     find_program('run-nested.sh'),
     args: [exe],
-    env: wl_test_env,
+    env: test_env,
     timeout: 120,
     should_fail: t.length() > 2 and t[2] == 'xfail',
   )
@@ -215,6 +235,8 @@ foreach b: ['firefox', 'chromium', 'brave']
       # Chromium's Wayland text-input v3 is off by default. Firefox ignores
       # the variable; browser-run.sh only uses it on the chromium family.
       'E_TEST_BROWSER_FLAGS=--enable-features=WaylandTextInputV3',
+      # This test plays the input method itself, so it needs the allowlist.
+      'E_TEST_WL_PRIVILEGED=1',
       # Which browsers are held to activating an input method at all. Measured
       # 2026-08-19: Brave carries text-input v1 and v3 and turns v3 on with the
       # flag above; the Chromium build here has only v1, and this Firefox has
@@ -248,6 +270,11 @@ test('input-method-fcitx5',
     'E_TEST_MODULE_SO=' + wl_test_module_so,
     'E_TEST_MODULE_ARCH=' + module_arch,
     'E_TEST_APP=' + meson.current_source_dir() / 'fcitx5-run.sh',
+    # The test client probes for fcitx5 by trying to become the input method
+    # and being refused, so it needs the allowlist for the probe. fcitx5 itself
+    # is granted by the fcitx5.desktop this tree installs - so if that shipped
+    # file were wrong, this test would fail with "fcitx5 never took the seat".
+    'E_TEST_WL_PRIVILEGED=1',
   ],
   suite  : 'im',
   timeout: 300,
@@ -285,6 +312,8 @@ test('input-method-pinyin-brave',
       meson.current_source_dir() / 'fcitx5-browser-run.sh',
       meson.current_source_dir() / 'pages' / 'textinput.html'),
     'E_TEST_FCITX_IM=pinyin',
+    # Same as input-method-fcitx5: the probe needs it, fcitx5 does not.
+    'E_TEST_WL_PRIVILEGED=1',
     'E_TEST_BROWSER_FLAGS=--enable-features=WaylandTextInputV3',
   ],
   suite  : 'im-browser',
diff --git a/src/tests/wayland/run-nested.sh b/src/tests/wayland/run-nested.sh
index 2e55bd632..55f520ffe 100755
--- a/src/tests/wayland/run-nested.sh
+++ b/src/tests/wayland/run-nested.sh
@@ -146,7 +146,25 @@ chmod 0700 "$RUNDIR"
 # silently breaks every test that cares where the focus is.
 E_HOME="$RUNDIR/.e"
 MODULE_DIR="$E_HOME/e/modules/wl_test/$MODULE_ARCH"
+PRIV_DIR="$E_HOME/e/wayland-privileges"
 mkdir -p "$MODULE_DIR"
+
+# Programs allowed to be an input method, for the tests that play one.
+#
+# The real mechanism, not a bypass: E reads this directory exactly as it reads
+# a user's, so an opted-in test proves the allowlist grants, and every other
+# test proves it denies. A test-only escape hatch would have proved neither.
+if [ "${E_TEST_WL_PRIVILEGED:-0}" = "1" ]; then
+    mkdir -p "$PRIV_DIR"
+    cat > "$PRIV_DIR/e-test-client.desktop" <<PRIVEOF
+[Desktop Entry]
+Type=Application
+Name=Enlightenment test client
+Exec=$(readlink -f "$1")
+NoDisplay=true
+X-Enlightenment-Wayland-Privileges=virtual_keyboard;input_method;
+PRIVEOF
+fi
 ln -s "$MODULE_SO" "$MODULE_DIR/module.so"
 
 # The x11 backend needs an X server. Inherit one if the caller has it -- that
diff --git a/src/tests/wayland/test_fcitx5.c b/src/tests/wayland/test_fcitx5.c
index 7dcca10f9..e301728e8 100644
--- a/src/tests/wayland/test_fcitx5.c
+++ b/src/tests/wayland/test_fcitx5.c
@@ -232,8 +232,12 @@ main(void)
    if (!_input_method_taken(tk, im_mgr, seat))
      tk_fail(tk, "fcitx5 never took the seat's input method within %d ms. It "
                  "is started as E_TEST_APP and its output is in the app log "
-                 "printed below this; the usual causes are a missing D-Bus "
-                 "session or the waylandim addon being disabled",
+                 "printed below this. Three usual causes: a missing D-Bus "
+                 "session, the waylandim addon being disabled, or the "
+                 "fcitx5.desktop this tree installs into "
+                 "share/enlightenment/wayland-privileges no longer matching "
+                 "the fcitx5 on PATH - without that file E does not advertise "
+                 "the input method globals to it at all",
              IM_APPEAR_MS);
    printf(PROG ": fcitx5 holds the seat's input method\n");
 
diff --git a/src/tests/wayland/test_privileges.c b/src/tests/wayland/test_privileges.c
new file mode 100644
index 000000000..56dcc1348
--- /dev/null
+++ b/src/tests/wayland/test_privileges.c
@@ -0,0 +1,71 @@
+/* An ordinary client cannot be an input method.
+ *
+ * zwp_input_method_v2 reads every keystroke typed into a text field and
+ * zwp_virtual_keyboard_v1 types into whatever window is focused. They are what
+ * an input method needs and what a keylogger needs, and nothing on the wire
+ * distinguishes those, so E only advertises them to programs named in a
+ * .desktop file under wayland-privileges - see e_comp_wl_privileges.c.
+ *
+ * This test is the deny half, and it is the half that matters: the grant half
+ * is proven several times over by the tests that do play an input method, none
+ * of which could work if the allowlist never granted anything. Those run with
+ * E_TEST_WL_PRIVILEGED=1, which makes run-nested.sh write a real allow-file
+ * naming the test binary. This one deliberately does not, so it is an ordinary
+ * client, and the compositor should behave towards it exactly as one that does
+ * not implement the protocols at all.
+ *
+ * That last part is the point of hiding the globals rather than refusing the
+ * requests. A client that is refused mid-protocol gets an error and a dead
+ * connection; a client that never sees the global takes the path it already
+ * has for compositors without input method support. The check below is
+ * therefore that the globals are *absent from the registry*, not that binding
+ * them fails.
+ */
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "e_wl_testkit.h"
+
+#define PROG "test-privileges"
+
+int
+main(void)
+{
+   Tk *tk;
+   uint32_t im, vk, ti;
+
+   tk = tk_connect(PROG);
+
+   im = tk_global_version(tk, "zwp_input_method_manager_v2");
+   vk = tk_global_version(tk, "zwp_virtual_keyboard_manager_v1");
+   ti = tk_global_version(tk, "zwp_text_input_manager_v3");
+
+   if (im)
+     tk_fail(tk, "zwp_input_method_manager_v2 was advertised (version %u) to a "
+                 "client with no entry under wayland-privileges. Any program "
+                 "you happen to run could then read everything you type into a "
+                 "text field", im);
+
+   if (vk)
+     tk_fail(tk, "zwp_virtual_keyboard_manager_v1 was advertised (version %u) "
+                 "to a client with no entry under wayland-privileges. Any "
+                 "program you happen to run could then type into whatever "
+                 "window is focused", vk);
+
+   /* The application side of text input is not privileged and must not become
+    * so by accident - every browser binds it, and a filter that caught it
+    * would take input methods away from all of them. */
+   if (!ti)
+     tk_fail(tk, "zwp_text_input_manager_v3 is missing. That one is the "
+                 "application side, not the input method side: it is what a "
+                 "browser binds for its own text fields and it is not "
+                 "privileged. Hiding it would disable input methods everywhere "
+                 "rather than restrict who can be one");
+
+   printf(PROG ": ok - the two privileged globals are hidden, text-input v3 is "
+          "not (v%u)\n", ti);
+
+   tk_disconnect(tk);
+   return 0;
+}
diff --git a/src/tests/wlcs/e_wlcs.c b/src/tests/wlcs/e_wlcs.c
index 4eaa613f9..af1488eb6 100644
--- a/src/tests/wlcs/e_wlcs.c
+++ b/src/tests/wlcs/e_wlcs.c
@@ -219,11 +219,36 @@ _wait_for_socket(E_Server *s)
  * slow: without the module the compositor comes up fine and every question we
  * ask it goes unanswered, which reads as a hang and not as a missing file.
  */
+/* mkdir -p, in place: walk the string turning each '/' into a NUL, make that
+ * prefix, put it back. */
+static int
+_mkdir_p(char *dir)
+{
+   char *p;
+
+   for (p = dir + 1; *p; p++)
+     {
+        if (*p != '/') continue;
+        *p = '\0';
+        if ((mkdir(dir, 0700) < 0) && (errno != EEXIST))
+          {
+             fprintf(stderr, "e_wlcs: mkdir %s: %s\n", dir, strerror(errno));
+             return -1;
+          }
+        *p = '/';
+     }
+   if ((mkdir(dir, 0700) < 0) && (errno != EEXIST))
+     {
+        fprintf(stderr, "e_wlcs: mkdir %s: %s\n", dir, strerror(errno));
+        return -1;
+     }
+   return 0;
+}
+
 static int
 _stage_test_module(const char *home, char *e_home, size_t e_home_size)
 {
    char dir[PATH_MAX], link[PATH_MAX];
-   char *p;
 
    if (!E_WLCS_TEST_MODULE_SO[0] || !E_WLCS_TEST_MODULE_ARCH[0])
      {
@@ -243,24 +268,7 @@ _stage_test_module(const char *home, char *e_home, size_t e_home_size)
         return -1;
      }
 
-   /* mkdir -p, in place: walk the string turning each '/' into a NUL, make
-    * that prefix, put it back. */
-   for (p = dir + 1; *p; p++)
-     {
-        if (*p != '/') continue;
-        *p = '\0';
-        if ((mkdir(dir, 0700) < 0) && (errno != EEXIST))
-          {
-             fprintf(stderr, "e_wlcs: mkdir %s: %s\n", dir, strerror(errno));
-             return -1;
-          }
-        *p = '/';
-     }
-   if ((mkdir(dir, 0700) < 0) && (errno != EEXIST))
-     {
-        fprintf(stderr, "e_wlcs: mkdir %s: %s\n", dir, strerror(errno));
-        return -1;
-     }
+   if (_mkdir_p(dir) < 0) return -1;
 
    snprintf(link, sizeof(link), "%s/module.so", dir);
    if (symlink(E_WLCS_TEST_MODULE_SO, link) < 0)
@@ -273,6 +281,37 @@ _stage_test_module(const char *home, char *e_home, size_t e_home_size)
    return 0;
 }
 
+/* Let this binary act as an input method. See the call site. */
+static void
+_stage_privileges(const char *e_home)
+{
+   char dir[PATH_MAX], path[PATH_MAX], exe[PATH_MAX];
+   ssize_t n;
+   FILE *f;
+
+   n = readlink("/proc/self/exe", exe, sizeof(exe) - 1);
+   if (n <= 0) return;
+   exe[n] = 0;
+
+   if (snprintf(dir, sizeof(dir), "%s/e/wayland-privileges", e_home) >=
+       (int)sizeof(dir)) return;
+   if (_mkdir_p(dir) < 0) return;
+   if (snprintf(path, sizeof(path), "%s/wlcs.desktop", dir) >=
+       (int)sizeof(path)) return;
+
+   f = fopen(path, "w");
+   if (!f) return;
+   fprintf(f,
+           "[Desktop Entry]\n"
+           "Type=Application\n"
+           "Name=wlcs\n"
+           "Exec=%s\n"
+           "NoDisplay=true\n"
+           "X-Enlightenment-Wayland-Privileges=virtual_keyboard;input_method;\n",
+           exe);
+   fclose(f);
+}
+
 static void
 _server_start(WlcsDisplayServer *server)
 {
@@ -295,6 +334,16 @@ _server_start(WlcsDisplayServer *server)
     * removed with it. */
    if (_stage_test_module(s->runtime_dir, e_home, sizeof(e_home)) < 0) return;
 
+   /* wlcs is the input method as well as the application.
+    *
+    * TextInputV3WithInputMethodV2Test binds zwp_input_method_manager_v2, and E
+    * only advertises that to programs named under wayland-privileges - so
+    * without a file naming this binary those eleven tests would go back to
+    * being skipped, which reads as coverage quietly disappearing rather than
+    * as a policy doing its job. Written into the throwaway E_HOME, so it is
+    * the same mechanism a user's file uses and it dies with the temp dir. */
+   _stage_privileges(e_home);
+
    /* A private XDG_RUNTIME_DIR means the compositor under test owns the only
     * socket in it, so we know its name without having to discover it: E picks
     * its own via ecore_wl2_display_create(NULL) and never exports

-- 
To stop receiving notification emails like this one, please contact
the administrator of this repository.

Reply via email to