Skip to content

many: reproduce inconsistent common id bug#16634

Open
olivercalder wants to merge 1 commit into
canonical:masterfrom
olivercalder:reproduce-inconsistent-common-id-bug
Open

many: reproduce inconsistent common id bug#16634
olivercalder wants to merge 1 commit into
canonical:masterfrom
olivercalder:reproduce-inconsistent-common-id-bug

Conversation

@olivercalder
Copy link
Copy Markdown
Member

@olivercalder olivercalder commented Feb 19, 2026

@3v1n0 is seeing the common ID flip between the one defined in the snap desktop-files plug and the default <snap>_... one. One way this manifests is that snap installs occasionally fail due to a mismatch. This PR adds a spread test to reproduce it and record the stacktraces at time of failure.

Marco said:

with 2.74.1 we return always the wrong ID, while with 2.73+ubuntu24.04 the dancing behavior happens all the times to me

in resolute, the desktop file is called: /var/lib/snapd/desktop/applications/ashpd-demo_com.belmoussaoui.ashpd.demo.desktop

In my experience on my local system (25.10 with snapd 2.74.1 from latest/beta) I see the flip-flopping too. So it's host-system dependent.

This PR should not be merged.

This issue is tracked internally by https://warthogs.atlassian.net/browse/SNAPDENG-36476

Signed-off-by: Oliver Calder <oliver.calder@canonical.com>
@3v1n0
Copy link
Copy Markdown
Contributor

3v1n0 commented Feb 20, 2026

FYI all started by playing with the rest API to avoid using snap routine myself, with this test snap that defines desktop-file-ids, if I use this client:

snapd-info-test.c
/*
Compile with:
 gcc snapd-info-test.c  -o snapd-test $(jhbuild run pkg-config --cflags --libs libsoup-3.0 json-glib-1.0 gio-unix-2.0) -g3
 */
#define HAVE_SNAPD_SUPPORT 1
#define SNAP_SECURITY_LABEL_PREFIX "snap."

#include <gio/gdesktopappinfo.h>
#include <libsoup/soup.h>
#include <json-glib/json-glib.h>
#include <stdint.h>
#include <inttypes.h>

#define SNAPD_SOCKET_PATH "/var/run/snapd.socket"

#if HAVE_SNAPD_SUPPORT
static SoupMessage *
make_snapd_message (const gchar *method, const gchar *path)
{
  g_autofree gchar *uri = NULL;
  SoupMessage *msg;
  SoupMessageHeaders *request_headers;

  uri = g_strconcat ("http://localhost", path, NULL);
  msg = soup_message_new (method, uri);
  request_headers = soup_message_get_request_headers (msg);

  /* Disallow authentication via polkit. */
  soup_message_headers_append (request_headers, "X-Allow-Interaction", "false");

  return msg;
}

static JsonObject *
process_snapd_response_body (SoupMessage   *msg,
                             GBytes        *body,
                             GError      **error)
{
  const gchar *content_type;
  g_autoptr(JsonParser) parser = NULL;
  const gchar *body_data;
  size_t body_length;
  JsonNode *root;
  JsonObject *response;
  gint64 status_code;
  g_autoptr(GError) internal_error = NULL;

  content_type = soup_message_headers_get_one (
    soup_message_get_response_headers (msg), "Content-Type");
  if (g_strcmp0 (content_type, "application/json") != 0)
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
                   "Invalid content type %s returned", content_type);
      return NULL;
    }

  parser = json_parser_new ();
  body_data = g_bytes_get_data (body, &body_length);
  if (!json_parser_load_from_data (parser, body_data, body_length, &internal_error))
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
                   "Failed to decode JSON content: %s", internal_error->message);
      return NULL;
    }

  root = json_parser_get_root (parser);
  if (!JSON_NODE_HOLDS_OBJECT (root))
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, "Returned JSON not an object");
      return NULL;
    }

  response = json_node_get_object (root);
  status_code = json_object_get_int_member (response, "status-code");
  if (status_code != SOUP_STATUS_OK && status_code != SOUP_STATUS_ACCEPTED)
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, "Invalid status code %"
                   G_GINT64_FORMAT, status_code);
      return NULL;
    }

  return json_object_ref (response);
}

typedef struct {
  GMainLoop *loop;
  GBytes *response_body;
  GError **error;
} SendMessageData;

static void
on_send_message_finished (GObject *source_object,
                          GAsyncResult *res,
                          gpointer user_data)
{
  SendMessageData *data = user_data;
  SoupSession *session = SOUP_SESSION (source_object);

  data->response_body = soup_session_send_and_read_finish (session, res,
                                                           data->error);
  g_main_loop_quit (data->loop);
}

static JsonObject *
call_snapd_sync (const gchar *method, const gchar *path, GError **error)
{
  g_autoptr(SoupMessage) msg = NULL;
  g_autoptr(GBytes) response_body = NULL;
  g_autoptr(GSocketAddress) address = g_unix_socket_address_new (SNAPD_SOCKET_PATH);
  g_autoptr(GMainLoop) loop = g_main_loop_new (NULL, FALSE);
  g_autoptr(GCancellable) cancellable = g_cancellable_new ();
  SendMessageData send_message_data = { .loop = loop, .error = error };

  msg = make_snapd_message (method, path);

  SoupSession *session = soup_session_new_with_options ("remote-connectable", address, NULL);
  soup_session_set_timeout (session, 1);

  g_timeout_add_once (20, (GSourceOnceFunc) g_cancellable_cancel, cancellable);
  soup_session_send_and_read_async (session, msg, G_PRIORITY_HIGH,
                                    cancellable, on_send_message_finished,
                                    &send_message_data);
  g_main_loop_run (loop);

  if (!(response_body = g_steal_pointer (&send_message_data.response_body)))
    return NULL;

  return process_snapd_response_body (msg, response_body, error);
}

JsonObject *
snapd_client_get_snap_sync (const gchar *name, GError **error)
{
  g_autofree gchar *path = NULL;
  g_autoptr(JsonObject) response = NULL;
  JsonObject *result;

  path = g_strconcat ("/v2/snaps/", name, NULL);
  response = call_snapd_sync ("GET", path, error);
  if (response == NULL)
    return NULL;
  result = json_object_get_object_member (response, "result");

  if (result == NULL)
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, "Invalid response to %s", path);
      return NULL;
    }

  return json_object_ref (result);
}

static char *
snapd_get_application_app_id_sync (const gchar  *snap_name,
                                   const gchar  *app_name,
                                   GError      **error)
{
  g_print("Looking for snap name: '%s' app '%s'\n", snap_name, app_name);
  g_autoptr (JsonObject) snap = NULL;

  if (!(snap = snapd_client_get_snap_sync(snap_name, error)))
      return NULL;

  g_print("Got json object for snap '%p'\n", snap);
  // {
  //   g_autofree gchar *snap_text = NULL;
  //   gsize snap_len = 0;
  //   JsonNode *node = NULL;

  //   node = json_node_new (JSON_NODE_OBJECT);
  //   json_node_init_object (node, snap);
  //   snap_text = json_to_string(node, TRUE);
  //   g_print ("%s\n", snap_text);
  //   json_node_free (node);
  // }

  JsonNode *apps_node = json_object_get_member (snap, "apps");
  if (!apps_node || !JSON_NODE_HOLDS_ARRAY (apps_node))
    {
      g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_FAILED,
                           "Failed parsing returned JSON");
      return NULL;
    }

  JsonArray *apps_array = json_node_get_array (apps_node);
  g_autoptr(GList) apps = json_array_get_elements (apps_array);
  guint n_apps = json_array_get_length (apps_array);

  for (guint i = 0; i < n_apps; ++i)
    {
      g_autofree char *app_id = NULL;
      JsonNode *app_node;
      JsonObject *app;
      const char *name;
      const char *desktop_file;
      char *dot;

      app_node = json_array_get_element (apps_array, i);
      if (!JSON_NODE_HOLDS_OBJECT (app_node))
        {
          g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_FAILED,
                              "Failed parsing returned JSON");
          return NULL;
        }

      app = json_node_get_object (app_node);
      name = json_object_get_string_member (app, "name");
      desktop_file = json_object_get_string_member (app, "desktop-file");

      g_print("Processing app '%s' with desktop file '%s'\n", name, desktop_file);

      if (!name || !desktop_file)
        continue;

      if (g_strcmp0 (name, app_name) != 0)
        continue;

      app_id = g_path_get_basename (desktop_file);

      dot = strrchr (app_id, '.');
      if (dot)
        *dot = '\0';

      g_print("Snap '%s' has app '%s' with desktop file '%s'\n", snap_name, name, desktop_file);
      return g_steal_pointer (&app_id);
    }

  return NULL;
}

#endif

typedef struct _MetaWindow
{
  char *sandboxed_app_id;
} MetaWindow;

static gboolean
meta_window_update_snap_id (MetaWindow *window,
                            uint32_t    pid)
{
  g_autoptr (GError) error = NULL;
  g_autoptr (GKeyFile) portal_info_key_file = NULL;
  g_autofree char *security_label_filename = NULL;
  g_autofree char *security_label_contents = NULL;
  g_autofree char *desktop_file = NULL;
  g_autofree char *portal_info = NULL;
  g_autofree char *pid_str = NULL;
  gsize i, security_label_contents_size = 0;
  char *contents_start;
  char *contents_end;
  char *sandboxed_app_id;
  char *sandboxed_app_name;
  int portal_wait_status;

  g_return_val_if_fail (pid != 0, FALSE);
  g_return_val_if_fail (window->sandboxed_app_id == NULL, FALSE);

  security_label_filename = g_strdup_printf ("/proc/%u/attr/current", pid);

  if (!g_file_get_contents (security_label_filename,
                            &security_label_contents,
                            &security_label_contents_size,
                            NULL))
    return FALSE;

  if (!g_str_has_prefix (security_label_contents, SNAP_SECURITY_LABEL_PREFIX))
    return FALSE;

  /* We need to translate the security profile into the desktop-id.
   * The profile is in the form of 'snap.name-space.binary-name (current)'
   * while the desktop id will be name-space_binary-name.
   */
  security_label_contents_size -= sizeof (SNAP_SECURITY_LABEL_PREFIX) - 1;
  contents_start = security_label_contents + sizeof (SNAP_SECURITY_LABEL_PREFIX) - 1;
  contents_end = strchr (contents_start, ' ');

  if (contents_end)
    security_label_contents_size = contents_end - contents_start;

  for (i = 0; i < security_label_contents_size; ++i)
    {
      if (contents_start[i] == '.')
        contents_start[i] = '_';
    }

  sandboxed_app_id = g_malloc0 (security_label_contents_size + 1);
  memcpy (sandboxed_app_id, contents_start, security_label_contents_size);

  window->sandboxed_app_id = g_steal_pointer (&sandboxed_app_id);


#ifdef HAVE_SNAPD_SUPPORT
  g_autoptr (GDesktopAppInfo) app_info = NULL;
  g_autofree char *desktop_id = NULL;
  g_autofree char *app_id = NULL;

  g_autofree char *snap_name = g_strdup (window->sandboxed_app_id);
  char *snap_app = strchr (snap_name, '_');

  desktop_id = g_strconcat (window->sandboxed_app_id, ".desktop", NULL);
  g_print("Desktop ID: %s\n", desktop_id);
  if (!snap_name || !snap_app ||
      /* (app_info = g_desktop_app_info_new (desktop_id)) != NULL */ FALSE)
    return TRUE;

  *snap_app = '\0';
  snap_app++;

  g_clear_pointer (&desktop_id, g_free);
  app_id = snapd_get_application_app_id_sync (snap_name, snap_app, &error);

  if (app_id)
    {
      window->sandboxed_app_id = g_steal_pointer (&app_id);
    }
  else if (error)
    {
      g_warning ("Failed to get desktop file for snap '%s' app '%s': %s",
                 snap_name, snap_app, error->message);
    }

  return TRUE;
#else
  return TRUE;
#endif

  return FALSE;
}

int
main (int argc, char *argv[])
{
  MetaWindow window = {0};

  pid_t pid = argc > 1 ? (pid_t) atoi (argv[1]) : 0;

  if (!meta_window_update_snap_id (&window, pid))
    g_print ("Failed to get snap id\n");
  else
    g_print ("Got snap id: %s\n", window.sandboxed_app_id);

  g_clear_pointer (&window.sandboxed_app_id, g_free);

  return 0;
}

And we do the nice dance:

for i in $(seq 0 20); do ./snapd-test $(pidof ashpd-demo); done |grep "snap id"
Got snap id: ashpd-demo_ashpd-demo
Got snap id: ashpd-demo_ashpd-demo
Got snap id: com.belmoussaoui.ashpd.demo
Got snap id: com.belmoussaoui.ashpd.demo
Got snap id: com.belmoussaoui.ashpd.demo
Got snap id: com.belmoussaoui.ashpd.demo
Got snap id: ashpd-demo_ashpd-demo
Got snap id: com.belmoussaoui.ashpd.demo
Got snap id: com.belmoussaoui.ashpd.demo
Got snap id: ashpd-demo_ashpd-demo
Got snap id: com.belmoussaoui.ashpd.demo
Got snap id: ashpd-demo_ashpd-demo
Got snap id: com.belmoussaoui.ashpd.demo
Got snap id: com.belmoussaoui.ashpd.demo
Got snap id: com.belmoussaoui.ashpd.demo
Got snap id: com.belmoussaoui.ashpd.demo
Got snap id: com.belmoussaoui.ashpd.demo
Got snap id: ashpd-demo_ashpd-demo
Got snap id: ashpd-demo_ashpd-demo
Got snap id: com.belmoussaoui.ashpd.demo
Got snap id: ashpd-demo_ashpd-demo

No different from using snap routine:

for i in $(seq 0 20); do snap routine portal-info $(pidof ashpd-demo); done |grep "DesktopFile" 
DesktopFile=com.belmoussaoui.ashpd.demo.desktop
DesktopFile=com.belmoussaoui.ashpd.demo.desktop
DesktopFile=ashpd-demo_ashpd-demo.desktop
DesktopFile=com.belmoussaoui.ashpd.demo.desktop
DesktopFile=ashpd-demo_ashpd-demo.desktop
DesktopFile=ashpd-demo_ashpd-demo.desktop
DesktopFile=com.belmoussaoui.ashpd.demo.desktop
DesktopFile=com.belmoussaoui.ashpd.demo.desktop
DesktopFile=ashpd-demo_ashpd-demo.desktop
DesktopFile=ashpd-demo_ashpd-demo.desktop
DesktopFile=com.belmoussaoui.ashpd.demo.desktop
DesktopFile=com.belmoussaoui.ashpd.demo.desktop
DesktopFile=com.belmoussaoui.ashpd.demo.desktop
DesktopFile=com.belmoussaoui.ashpd.demo.desktop
DesktopFile=ashpd-demo_ashpd-demo.desktop
DesktopFile=ashpd-demo_ashpd-demo.desktop
DesktopFile=com.belmoussaoui.ashpd.demo.desktop
DesktopFile=com.belmoussaoui.ashpd.demo.desktop
DesktopFile=ashpd-demo_ashpd-demo.desktop
DesktopFile=com.belmoussaoui.ashpd.demo.desktop
DesktopFile=ashpd-demo_ashpd-demo.desktop

The desktop file switches between the two defined.


Note that this was not happening in 2.74.1, but because the file was named wrongly, after fixing it it show the issue:

sudo mv /var/lib/snapd/desktop/applications/ashpd-demo_com.belmoussaoui.ashpd.demo.desktop \
  /var/lib/snapd/desktop/applications/com.belmoussaoui.ashpd.demo.desktop

@3v1n0
Copy link
Copy Markdown
Contributor

3v1n0 commented Feb 20, 2026

BTW technically #15813 should help with this if the CommonID is defined.

But in the other cases, I feel that it could be due to the fact that this loop isn't always following the same order or what?

snapd/snap/info.go

Lines 1486 to 1487 in 05579fc

for _, desktopFileID := range desktopFileIDs {
desktopFile := filepath.Join(dirs.SnapDesktopFilesDir, desktopFileID)

@github-actions
Copy link
Copy Markdown

Fri Feb 20 01:40:16 UTC 2026
The following results are from: https://github.com/canonical/snapd/actions/runs/22205091722

Failures:

Preparing:

  • openstack:ubuntu-20.04-64:tests/main/inconsistent-common-id-reproducer
  • openstack:ubuntu-25.10-64:tests/main/inconsistent-common-id-reproducer
  • openstack:ubuntu-24.04-64:tests/main/inconsistent-common-id-reproducer

Executing:

  • openstack:arch-linux-64:tests/main/snap-quota
  • openstack:ubuntu-core-20-64:tests/core/kernel-base-gadget-pair-single-reboot-failover:kernel_gadget
  • openstack:ubuntu-core-20-64:tests/main/systemd-success-exit-status
  • openstack:ubuntu-core-24-64:tests/core/kernel-base-gadget-pair-single-reboot-failover:kernel_base
  • openstack:ubuntu-core-24-64:tests/main/dbus-activation-system
  • openstack:ubuntu-20.04-64:tests/main/degraded
  • openstack:ubuntu-25.10-64:tests/main/systemd-success-exit-status
  • openstack:ubuntu-25.10-64:tests/main/apparmor-prompting-integration-tests:download_file_defaults
  • openstack:ubuntu-25.10-64:tests/main/apparmor-prompting-snapd-startup

Restoring:

  • openstack:ubuntu-core-20-64:tests/core/kernel-base-gadget-pair-single-reboot-failover:kernel_gadget
  • openstack:ubuntu-core-20-64:tests/core/
  • openstack:ubuntu-core-20-64:
  • openstack:ubuntu-core-24-64:tests/core/kernel-base-gadget-pair-single-reboot-failover:kernel_base
  • openstack:ubuntu-core-24-64:tests/core/
  • openstack:ubuntu-core-24-64:
  • openstack:ubuntu-25.10-64:tests/main/apparmor-prompting-snapd-startup
  • openstack:ubuntu-25.10-64:tests/main/
  • openstack:ubuntu-25.10-64:

@3v1n0
Copy link
Copy Markdown
Contributor

3v1n0 commented Feb 20, 2026

BTW technically #15813 should help with this if the CommonID is defined.

But in the other cases, I feel that it could be due to the fact that this loop isn't always following the same order or what?

snapd/snap/info.go

Lines 1486 to 1487 in 05579fc

for _, desktopFileID := range desktopFileIDs {
desktopFile := filepath.Join(dirs.SnapDesktopFilesDir, desktopFileID)

Oh. Of course, we're looping through Map values, they aren't ever orderd!

@codecov
Copy link
Copy Markdown

codecov Bot commented Feb 20, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 77.52%. Comparing base (05579fc) to head (bb24226).

Additional details and impacted files
@@            Coverage Diff             @@
##           master   #16634      +/-   ##
==========================================
+ Coverage   77.50%   77.52%   +0.02%     
==========================================
  Files        1355     1353       -2     
  Lines      186018   186090      +72     
  Branches     2449     2449              
==========================================
+ Hits       144165   144268     +103     
+ Misses      33130    33113      -17     
+ Partials     8723     8709      -14     
Flag Coverage Δ
unittests 77.52% <100.00%> (+0.02%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@olivercalder
Copy link
Copy Markdown
Member Author

BTW technically #15813 should help with this if the CommonID is defined.
But in the other cases, I feel that it could be due to the fact that this loop isn't always following the same order or what?

snapd/snap/info.go

Lines 1486 to 1487 in 05579fc

for _, desktopFileID := range desktopFileIDs {
desktopFile := filepath.Join(dirs.SnapDesktopFilesDir, desktopFileID)

Oh. Of course, we're looping through Map values, they aren't ever orderd!

Thanks for digging into this!

In this case the app only has one desktop file ID specified in the plug, so I don't think that is the issue (although we should do something to make that more deterministic). But I think the real problem here is a race between the desktop file being available on the filesystem. Not sure how to mitigate that, but will think on it with the team

@bboozzoo
Copy link
Copy Markdown
Contributor

@olivercalder can you clarify whether we still need it? If so, please rebase and ask for reviews.

@olivercalder
Copy link
Copy Markdown
Member Author

I forgot about this PR, but a fix for the issue is in #17016. In short, the test snap has two desktop plugs, and if the one with no desktop-file-ids attribute is chosen, then the fallback is used, otherwise the explicit ID is used.

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.

3 participants