Why does Firefox keep creating ~/Desktop?

For as long as I've used Linux, a Desktop folder has plagued my home directory. Despite my efforts to remove it, it has continued to reappear for no discernible reason. At some point I had the vague idea that it might be Firefox making it. A few days ago I finally broke, and decided that it was time to get to the bottom of it. I documented the process here.

Mozilla has a convenient tool that lets you search through the mozilla-unified codebase, which contains all of the Firefox code.

Since I had no idea what was even triggering the bug, I naively searched "Desktop" to see if there were any obvious matches. There were over a thousand results, and nothing relevant came up. However, I realized that a lot of the matches were "Desktop" as part of some sort of symbol name. Being a file path in my home directory, it might be more likely to be in a string. Searchfox supports regexp, so I used the expression ".*Desktop.*" and found something interesting:

I've noticed that Firefox doesn't always create a Desktop folder. It seems to appear at random times, but it would always appear eventually. This is consistent with it only being created when I opened preferences, which is what the search result seems to indicate. To test my hypothesis, I deleted the Desktop folder and opened the preferences page. I rechecked my home directory and my Desktop folder had indeed reappeared. This seemed like a promising starting point. Specifically, the this._getDownloadsFolder was interesting because it looked like it might create the folder if it fails to find it on the filesystem.

So I set some breakpoints (using Firefox's debugging feature) near where this function is called with the "Desktop" argument.

Deleting the Desktop folder again and rerunning with breakpoints, I found that the folder appears after this._getDownloadsFolder("Desktop"); runs.

But why does this function need to run? It seems like it gets the display name for the folder in the preferences pane. So on a Unix-like system, if I set my downloads folder to be "$HOME/Downloads", it will instead display "Downloads", to be "user-friendly". If I set my downloads folder to be "$HOME/downloads", it will just display "$HOME/downloads". While mangling the directory name to make it less clear where it actually is seems kind of silly, UX people seem to like doing it, and it means that it actually does make sense to read the Download and Desktop directory names at the beginning of the function.

However, it does not make sense for these functions to be creating the directories. We need to go deeper. Let's see what _getDownloadsFolder looks like:

  async _getDownloadsFolder(aFolder) {
    switch (aFolder) {
      case "Desktop":
        return Services.dirsvc.get("Desk", Ci.nsIFile);
      case "Downloads":
        let downloadsDir = await Downloads.getSystemDownloadsDirectory();
        return new FileUtils.File(downloadsDir);
    }
    throw new Error(
      "ASSERTION FAILED: folder type should be 'Desktop' or 'Downloads'"
    );
  },

Since only the Desktop folder gets created, presumably whatever is going wrong happens on the Services.dirsvc.get("Desk", Ci.nsIFile); call. Searching Services.dirsvc didn't turn up anything promising, but "Desk" seems like some sort of index, so I searched for that.

This gives us the symbol DirectoryService_OS_DesktopDirectory that looks like it might appear in other places. Indeed, it is used in xpcom/io/nsDirectoryService.cpp to get a file path, which makes sense. It has different branches for Mac, Windows and Unix. In the Unix branch, it performs the call GetSpecialSystemDirectory(Unix_XDG_Desktop, getter_AddRefs(localFile)). This function is in xpcom/io/SpecialSystemDirectory.cpp. With the given parameters, GetSpecialSystemDirectory eventually performs the call GetUnixXDGUserDirectory(aSystemDirectory, aFile) which contains the following code when aSystemDirectory == Unix_XDG_Desktop:

// for the XDG desktop dir, fall back to HOME/Desktop
// (for historical compatibility)
rv = GetUnixHomeDir(getter_AddRefs(file));
if (NS_FAILED(rv)) {
  return rv;
}

rv = file->AppendNative("Desktop"_ns);
...
rv = file->Exists(&exists);
if (NS_FAILED(rv)) {
  return rv;
}
if (!exists) {
  rv = file->Create(nsIFile::DIRECTORY_TYPE, 0755);
  if (NS_FAILED(rv)) {
    return rv;
  }
}

While we could go deeper, this code is pretty obviously creating $HOME/Desktop as a fallback if XDG_DESKTOP is not specified elsewhere. In my case it isn't, so the fallback is created. Thus the titular question is answered.

As it turns out, this bug was filed 15(!) years ago, discussed, and then ignored. And, as Gijs, a friendly Firefox developer pointed out, it is actually a regression(!!!) introduced by the addition of XDG user directory support, also 15 years ago.

What's the fix?

While there were many potential ways to fix this, the simplest fix with the least potential for disruption was to revert to the behavior before the patch that introduced the regression: fall back to $HOME if $HOME/Desktop doesn't exist. Thanks to Gijs for helping me fix a long standing annoyance and getting my first Firefox patch landed!