Visitar URL original
fix: honour the documented opt-out, keep the machine name out and the Linux queue private by Jen-Uno · Pull Request #35 · unoplatform/uno.devtools.telemetry · GitHub
Skip to content

fix: honour the documented opt-out, keep the machine name out and the Linux queue private - #35

Merged
Jen-Uno merged 9 commits into
mainfrom
dev/jen/telemetry-privacy-fixes
Nov 4, 2026
Merged

Jen-Uno merged 9 commits into
mainfrom
dev/jen/telemetry-privacy-fixes

Conversation

@Jen-Uno

@Jen-Uno Jen-Uno commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

GitHub Issue (If applicable): n/a; internal issues below.

PR Type

  • Bugfix

What is the current behavior?

Three privacy problems in the telemetry library:

  • The documented opt-out value 1 is ignored. UNO_PLATFORM_TELEMETRY_OPTOUT is read with bool.TryParse, so only true opts out. Setting it to 1, as the public telemetry page says, leaves telemetry on in every product that doesn't pass its own provider.
  • Desktop items carry the computer's name. The Application Insights SDK fills an empty cloud role instance with the machine name, plus its domain when there is one, and then fills the internal node name the same way. The library never set either, so every desktop item carried the hostname.
  • On Linux the queue sits in the shared /tmp. The desktop queue lives under /tmp/.uno/telemetry/outbox, which every user on the machine shares, and the folder gets default permissions. Each queued file also stores the address it is sent to, and the sender used that stored address. So another local user could read queued items, block a second user's queue, or plant a file that is POSTed to an address of their choosing.

What is the new behavior?

Nine commits:

  1. fix: accept 1, yes and on in the telemetry opt-out. 1, true, yes and on opt out, in any case and with surrounding whitespace. Only the literal false still forces telemetry on over a product's own opt-out, as before. 0, no, off and anything unrecognised read as unset, so the product decides. This covers the Telemetry constructor, used on desktop and WASM, and the library's environment-selected file lanes. docs/usage.md lists the accepted values. Commit 9 drops the false override.
  2. fix: keep the Linux telemetry queue per user and send only to the configured endpoint. On every platform, a queued file is now sent only to the channel's configured endpoint; the stored address is still written, so other versions can read the format, but it is ignored. This commit also moved the Linux queue, but how it did that is replaced by commit 4, following the review panel's comment below.
  3. fix: keep the machine name out of desktop telemetry. The role instance and node name are set to anonymous on the client context, which is copied onto every item. A test reads queued items back and checks them for the machine and host names.
  4. fix: make the queue folder private once at startup, in every build, without native calls. Outside Windows, the queue folder is settled once, when telemetry starts, in every build of the package, netstandard2.0 included:
    • Where: only Windows, and macOS with TMPDIR set, queue in the temp folder. macOS without it (sudo, cron, launchd) uses ~/Library/Caches/uno/telemetry/outbox. Linux, FreeBSD and other systems use $XDG_CACHE_HOME/uno/telemetry/outbox, or ~/.cache/uno/telemetry/outbox.
    • The folder above it (~/.cache, $XDG_CACHE_HOME, ~/Library/Caches or $TMPDIR) must not be writable by users outside its owner and group (commit 7 also refuses it with the sticky bit, and requires it to be the user's). Group-writable is accepted: where each user has a group of their own, as on Ubuntu, ~/.cache is usually 0775.
    • The folders the package names (uno or .uno, telemetry, outbox) must not be symbolic links, are created at 0700, then set to 0700 and read back. Only a folder's owner, or root, may change its mode, so this also shows they are the user's. The netstandard2.0 build makes the same calls through reflection.
    • Root is refused only under sudo or doas (SUDO_UID, SUDO_USER or DOAS_USER set) with a home that isn't root's; commit 7 judges this by the queue's folder instead. Root in a container queues in its home like any user. Commit 9 replaces this rule.
    • When no folder is usable, the instance sends nothing and writes one Trace line naming the reason. Without a home the machine id isn't stored either. A queue folder deleted while telemetry runs isn't recreated. Commit 9 sends from memory instead.
    • Removed: FileOwner.cs and the per-file owner check. No P/Invoke remains. Paths are built from single folder names, so nothing appended can replace the base.
    • CI: two Linux unit-test jobs, Unit Tests (Linux, glibc) as the runner's ordinary user and Unit Tests (Linux, musl) in an Alpine container as root. UNO_TELEMETRY_TESTS_REQUIRE makes the tests each job exists to run fail instead of skipping, and a test checks the job runs as the expected user.
  5. test: give the sudo check its own folder permissions so it runs on Windows. Test only. The sudo test faked root but used the runtime's own folder-mode calls, which Windows doesn't have, so on Windows it stopped one check early and failed. It now passes fake modes. The library is unchanged.
  6. refactor: catch only expected exceptions in test cleanups, and use Select in IsHomeOfRoot. From the code-quality review of commit 4. The test cleanups this PR adds catch only IOException and UnauthorizedAccessException, the sudo test asserts its failure reason isn't null, and IsHomeOfRoot uses Select. No change in behaviour.
  7. fix: refuse queue folders another user controls, and say why telemetry is off. Findings 1, 2, 4 and 5 of the second review panel, below:
    • The folder above the queue is refused when other users can write to it even with the sticky bit, since the owner of a sticky folder such as /tmp may still rename entries in it. It must also be the user's own: setting its mode to the one it has succeeds only for its owner, or root. The folders the package names are checked for links again at the end.
    • The reason telemetry is off also goes to the console when UNO_TELEMETRY_ENABLE_PERSISTENCE_CHANNEL_DEBUG_OUTPUT is set, since no host listens to Trace.
    • Under sudo or doas, root creates nothing outside root's home, judged by the folder that would hold the queue folder rather than by HOME, so sudo keeping XDG_CACHE_HOME is caught. Root running sudo itself (SUDO_UID=0, SUDO_USER=root) no longer counts.
    • A queued file is read only if it is seekable and no longer than the storage's 10 MB capacity, and never past its own length. A link to /dev/zero or a pipe is set aside as corrupt.
  8. test: check the root test's failure reason without dereferencing it. Test only, from the code-quality review: the root test asserts on failure ?? string.Empty. No change in behaviour.
  9. fix: send from memory without a queue folder, keep root in its own home, and let the opt-out only opt out. From rajamatt's review:
    • No usable queue folder: the instance sends from memory instead of turning off: every 5 seconds, and on Flush, FlushAsync and Dispose (waiting at most 2 seconds). Items still waiting when the process stops are lost. Each item carries a Queue Refused property with a fixed code, never a path: no-home, no-unix-modes, root-outside-home, shared-parent, not-owned, no-unix-permissions, symbolic-link or io-error. The machine id isn't stored then either. docs/usage.md lists the codes and adds UNO_TELEMETRY_ENABLE_PERSISTENCE_CHANNEL_DEBUG_OUTPUT to the env table.
    • Root uses no folder outside root's home, as /etc/passwd lists it, whether or not sudo or doas started it. That covers a container that mounts a user's home and sets HOME to it, and su -m. The SUDO_*/DOAS_USER check is removed.
    • Opt-out: UNO_PLATFORM_TELEMETRY_OPTOUT can only opt out. false reads as unset, like 0, so the product decides. The docs list 1 and true, as the public telemetry page does; yes and on still opt out.

PR Checklist

  • Docs updated (docs/usage.md).
  • Unit tests added: 291 cases in all, against 122 on main. 289 passed and 2 skipped here, net8.0, macOS arm64, in both Debug and Release. The two skipped need root; the musl job runs them.
  • Contains NO breaking changes. Behaviour changes, listed below.
  • Associated with an internal issue.

Other information

Behaviour changes for reviewers:

  • Machine names: cloud_RoleInstance and the internal node name now read anonymous on every desktop item. Nothing that grouped by role instance will still see one value per machine.

  • Queued file addresses: a queued file's stored address is ignored on every platform. In practice every version uses the same default endpoint.

  • Queue location: items queued by older versions under /tmp/.uno/telemetry/outbox aren't migrated, since anyone could have written them. The protection holds only once every Uno tool on a machine has the new package: older versions keep queueing in /tmp.

  • Opt-out: UNO_PLATFORM_TELEMETRY_OPTOUT=false no longer forces telemetry on over a product's own opt-out; it reads as unset.

  • Telemetry now sends from memory, with no disk queue, tagged Queue Refused, and with one Trace line (and a console line with UNO_TELEMETRY_ENABLE_PERSISTENCE_CHANNEL_DEBUG_OUTPUT set), where the released version queued on disk:

    • a home that is missing, relative or / (a container run as a uid with no passwd entry), or read-only;
    • a file system that keeps no Unix permissions (some NFS, CIFS and vfat mounts);
    • a folder above the queue that users outside its group can write to, even with the sticky bit: so always with TMPDIR=/tmp, and with HOME=/tmp when another user created /tmp/.cache;
    • a folder above the queue that isn't the user's;
    • root, when the queue's folder would be outside root's home: under sudo -E, with sudo keeping XDG_CACHE_HOME, or in a container whose HOME isn't root's (GitHub Actions container jobs use /github/home);
    • the netstandard2.0 build on a runtime older than .NET 7 (Mono, .NET 6), which can't set a folder's mode.

    In those cases items not sent before the process stops are lost, and the machine id isn't stored: a machine with no hardware address to hash gets a new id each run.

  • Accepted risk: a group-writable folder above the queue is trusted. On a machine where several users share the group of someone's cache folder, they could replace that person's queue folder.

  • Not detected:

    • access control lists that survive the mode change (macOS, ZFS, NFSv4), and mounts that report 0700 without enforcing it.

Not verified:

  • Linux only in CI. This was developed on macOS. Both new Linux jobs have passed on every push since f77026c; no Linux machine was tried by hand.
  • The root rule of commit 9 is checked for real only by the musl job, which runs as root; locally its two tests skip.
  • WASM runtime tests weren't run locally; CI's WASM job covers them. No WASM code path changed.

For the repo owners:

  • sign needs only build, so signing and publishing wait for neither the two Linux jobs nor the WASM tests. Whether they should is your call.

Follow-ups outside this repository: each product needs to take the new package version, and Studio and the dev server have their own copies of the opt-out check.

Internal Issue (If applicable):

🤖 Generated with Claude Code

Jen-Uno and others added 3 commits November 2, 2026 15:36
UNO_PLATFORM_TELEMETRY_OPTOUT was read with bool.TryParse, so the
documented value 1 left telemetry on. 1, true, yes and on now opt out,
ignoring case and surrounding whitespace. Only the literal false still
forces telemetry on over a product's own opt-out; 0, no, off and any
other value read as unset, so the product decides.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…figured endpoint

On Linux the desktop queue lived in the shared /tmp with default
permissions, and a queued file was sent to the address stored in it, so
another local user could read queued items, block the queue, or plant a
file that would be posted elsewhere. On Linux the queue now lives under
$XDG_CACHE_HOME/uno/telemetry/outbox (or ~/.cache/uno/telemetry/outbox),
created with mode 0700 where the framework allows it, and files owned by
another user are skipped. On every platform a queued file is sent only to
the channel's configured endpoint. Windows and macOS keep their per-user
temp folders.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Application Insights SDK fills an empty cloud role instance with the
machine's name, with its domain when there is one, and then fills the
internal node name the same way, so every desktop item carried the
hostname. Both are now set to a constant on the client context, which is
copied onto every item. A test reads queued items back and checks them
for the machine and host names.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Jen-Uno

Jen-Uno commented Nov 2, 2026

Copy link
Copy Markdown
Contributor Author

Review panel: commit 2c03326 (per-user Linux queue and endpoint pinning)

This review covers only 2c03326 (fix: keep the Linux telemetry queue per user and send only to the configured endpoint). The opt-out (69bc1e3) and machine-name (a2f9b92) commits are out of scope. Architect, security and skeptic reviewers read the code independently. None of them ran anything on Linux.

Verdict: fix-first. The verdicts were architect approve-with-changes, security fix-before-merge and skeptic fix-first. Endpoint pinning is complete and sound, and the statx layout looks right by inspection. But the private folder this design relies on often never becomes private, and no Linux code path has run yet.

High

  1. The queue folder's mode isn't enforced (security, from code), and was flagged by all three reviewers. See PersistenceChannel/StorageService.cs:103-106 and :127-137.

    • What: 0700 is applied only when the folder is created, and only by the net8.0/net9.0 builds.
    • Why it matters:
      • The netstandard2.0 build uses the same path. That's the build MSBuild tasks and the XAML source generator load, and it's often the first Uno code to run on a fresh machine, inside dotnet build. It creates ~/.cache/uno/telemetry/outbox with the default 0755 (files 0644), and later processes keep it.
      • A folder that already exists is never checked, including one someone else created first under a shared HOME or XDG_CACHE_HOME, such as /tmp/.cache in some images.
      • Where home folders can be entered by others, queued items stay readable by other users.
    • Direction: on Unix, verify the folder once on first use, on every target framework:
      • It must be a real directory (no-follow) owned by geteuid().
      • If it has group or other bits, chmod it to 0700: File.SetUnixFileMode on net8+, a libc chmod P/Invoke on netstandard2.0. A successful chmod also shows the folder is ours.
      • If any step fails, queue nothing.

    This also removes most of the reason for the per-file owner check (finding 3).

  2. The Linux code has never run (testing), flagged by the skeptic and security. See Uno.DevTools.Telemetry.Tests/PersistenceChannelSharedFolderTests.cs:186, :244, :252, and .github/workflows/ci.yml.

    • What: CI runs the unit tests only on windows-2022, and the test project targets net8.0 only. So the 0700 test and the root-owner test come out Inconclusive. The "own file" test passes without testing anything, because off Linux the check returns true without asking.
    • Why it matters: CI proves only the endpoint pinning, the path function and the wiring. A wrong statx offset would fail closed: every file would be skipped and Linux telemetry would stop silently.
    • Direction: add an Ubuntu unit-test job; CI already has an Ubuntu job, for the WASM tests. Run the Linux tests on glibc (x64) and musl (Alpine) before merging.

Medium

  1. The per-file owner check is the wrong level, and it can fail closed (design and availability), flagged by all three. See StorageService.cs:252, :186, :432 and :631-655; PersistenceChannel/FileOwner.cs:50.

    • What:
      • Once sends are pinned, a planted file can only reach the endpoint an attacker could reach directly.
      • Skipped files still count toward the 100-file cap, and Peek reads only the first 50 .trn files, so 50 to 100 planted files can block the queue for good.
      • The check compares the owner statx reports with geteuid(). On NFS with mismatched user mapping, all_squash, sshfs, CIFS uid= or fakeroot, the user's own files look foreign, so everything is skipped and dropped silently. Shared dev boxes typically have home folders on such mounts.
    • Direction: rely on the verified private folder (finding 1). Then either drop the per-file check, or treat a mismatch as "can't tell". Leave files that aren't ours out of the limits, or delete them once the folder is verified private.
  2. "Is the temp folder per user?" is decided by "is this Linux?" (coupling), flagged by all three. See Telemetry.cs:252, FileOwner.cs:23 and :34, StorageService.cs:130, QueueDirectory.cs:34-37.

    • What: three places use three different platform checks.
    • Why it matters: FreeBSD, and macOS without TMPDIR (sudo, cron, launchd), still queue in the shared /tmp, and the owner check doesn't run there. The commit message says macOS is per-user, and FileOwner.cs:28-30 contradicts the macOS caveat in docs/usage.md.
    • Direction: put one predicate in PlatformDetection.cs. Only Windows, and macOS with TMPDIR, use the temp folder. Everything else uses the XDG cache, or ~/Library/Caches on macOS.
  3. An unusable home has two outcomes, and one of them is silent (lifecycle), flagged by the architect and skeptic. See QueueDirectory.cs:40-44; Telemetry.cs:259-271; StorageService.cs:108-111, :161-166, :424.

    • What:
      • A missing home disables telemetry and traces once.
      • HOME=/, which Docker sets for --user 1001 without a passwd entry, counts as valid, as do /nonexistent and read-only homes. The folder then can't be created, and EnqueueAsync returns quietly with no trace.
      • StorageService reads a null path as "use the default folder".
      • With a channel override and no home, the settings path stays relative, so .machinehash is written under the current directory.
    • Direction: settle the folder once at startup with one fallback: trace, then send without a queue, as WasmHttpSender does, or send nothing. Make the settings path absolute or skip it.

Low

  1. Link swap and size (denial of service; security). See StorageService.cs:252-257 and :518. The owner check reads the name without following links, but the open that follows does follow them. Nothing checks the file's type or size, and the whole file is read into memory. Anyone who can write to the folder, which is only possible through finding 1, can swap in a link to /dev/zero or a FIFO and exhaust memory or hang the sender. Direction: check the opened handle with statx and AT_EMPTY_PATH. Require a regular file with the same device and inode as the first check, and cap the size before reading.
  2. sudo that keeps HOME (architect, skeptic). See StorageService.cs:132. Under sudo -E, or plain sudo on older Ubuntu, root creates a missing ~/.cache and outbox as root-owned 0700. That breaks the user's cache folder for every app, along with their own telemetry queue. Direction: don't create missing parents when the effective user isn't the owner of HOME; queue nothing instead.
  3. The endpoint is stored twice (architect). See FlushManager.cs:45 and BaseStorageService.cs:47. They are kept in step only by the setter at PersistenceChannel.cs:90-94, and storage takes its default from PersistenceChannel, a layer above it. Pinning where a file is read back is the right layer, and the new parameter on CreateFromStreamAsync is clean. Direction: give the endpoint a single owner.
  4. Two conventions for per-user folders (architect). See Telemetry.cs:268-271. Settings go in ~/.uno/telemetry and the queue in ~/.cache/uno. QueueDirectory could choose both.
  5. The old /tmp folder and older versions (all three). Not migrating is right, because those files can't be trusted. But deleting the current user's own files in the old Linux folder would be cheap. The PR should also say the protection holds only once every Uno tool on the machine has the new package: older versions, such as a generator pinned by an old SDK, keep queueing in /tmp and keep using the stored address.

Info

  • statx P/Invoke: correct by inspection (security, skeptic). The struct is 256 bytes, the mask is at offset 0 and stx_uid at 0x14, AT_FDCWD is -100, AT_SYMLINK_NOFOLLOW is 0x100 and STATX_UID is 0x8. "libc" resolves on both glibc and musl. Because the buffer starts zeroed and the mask is checked, the code can't act on garbage. Failing open is acceptable once the endpoint is pinned.
  • Endpoint pinning: complete (all three). The only place the stored address is read is StorageTransmission.cs:55, and the value is discarded. The endpoint can't be configured anywhere else (PersistenceChannel.cs:50), so no setup breaks.
  • Logging: nothing sensitive is newly logged. The debug output, off by default, can print one line of a malformed file to the user's own console (StorageTransmission.cs:95).
  • Small things:
    • StorageService.cs:129 uses NET7_0_OR_GREATER, where the repo usually uses NET8_0_OR_GREATER.
    • docs/usage.md:137 says "queues nothing" where the code sends nothing.

Suggested order

  1. Verify the folder on every target (1).
  2. Drop or relax the per-file check (3).
  3. Use one per-user predicate (4).
  4. Settle the no-home and unusable-home path (5).
  5. Get a Linux test run on glibc and musl (2).

Findings 6 to 10 can follow in the same PR or a later one.

…ithout native calls

The previous commit's private queue folder was often never private:
only the net8.0 and net9.0 builds set mode 0700, and only when they
created the folder, so a folder the netstandard2.0 build (MSBuild tasks,
the XAML generator) created first stayed 0755. Its per-file owner check
called statx and geteuid through P/Invoke, could skip every file on NFS
or sshfs, and never ran in CI.

Outside Windows the queue folder is now settled once, when telemetry
starts, in every build:

- The folder that holds the ones the library names (~/.cache,
  $XDG_CACHE_HOME, ~/Library/Caches or $TMPDIR) must not be writable by
  users outside its owner and group, unless it has the sticky bit.
  Group-writable is accepted: where each user has a group of their own,
  as on Ubuntu, ~/.cache is usually 0775.
- uno (or .uno), telemetry and outbox must not be symbolic links, are
  created at 0700, then set to 0700 and read back. Only a folder's owner,
  or root, may change its mode, so this also shows they are the user's.
  The netstandard2.0 build makes the same calls through reflection.
- Only Windows, and macOS with TMPDIR set, queue in the temp folder.
  macOS without it uses ~/Library/Caches; other systems the XDG cache.
- Root is refused only under sudo or doas with a home that isn't root's.
- Without a usable folder the instance sends nothing and writes one
  Trace line naming the reason, and the machine id isn't stored without
  a home. A queue folder deleted while telemetry runs isn't recreated.

FileOwner.cs and the per-file owner check are removed, so no P/Invoke
remains. Paths are built from single folder names, so nothing appended
can replace the base. CI gains two Linux unit-test jobs, glibc as an
ordinary user and Alpine (musl) as root, which fail rather than skip the
tests they exist to run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Jen-Uno and others added 2 commits November 3, 2026 11:27
…ndows

The test faked root under sudo but used the runtime's own folder-mode
calls, which Windows doesn't have, so TryPrepare stopped at "this runtime
can't set a folder's Unix permissions" before reaching the sudo check.
It now passes fake modes, as the same case in TelemetryQueueFolderTests
does. The library is unchanged: on Windows it returns before either check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lect in IsHomeOfRoot

Addresses the code-quality review of f77026c:

- The four test classes this PR adds clean up their folders catching
  only IOException and UnauthorizedAccessException, instead of every
  exception; anything else now fails the test.
- The sudo test asserts the failure reason isn't null before reading it.
- IsHomeOfRoot maps each /etc/passwd line to its fields with Select. It
  behaves as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Jen-Uno

Jen-Uno commented Nov 3, 2026

Copy link
Copy Markdown
Contributor Author

Review panel: a2f9b92..4a86b71 (the queue-folder rework)

This review covers f77026c (make the queue folder private once at startup, in every build, without native calls), c50d30b and 4a86b71. The opt-out (69bc1e3) and machine-name (a2f9b92) commits are out of scope. Architect, security and skeptic reviewers read the code at 4a86b71 independently and compared it with the earlier panel's findings on 2c03326. None of them ran anything. CI on 4a86b71 is green on Windows, Linux glibc (ordinary user), Linux musl (root) and WASM.

Verdict: fix-first. The verdicts were architect approve-with-changes, security fix-before-merge and skeptic fix-first. The layering is right, nothing fails open, and every reviewer found the netstandard2.0 reflection path sound. But a base folder another user owns is still trusted, and every reason telemetry turns off is invisible on a real machine.

Earlier findings

# Status Notes
1. Mode not enforced Partly closed Every build and every existing folder is now checked. A base folder owned by someone else is still accepted (finding 1 below).
2. Linux code never ran Closed Both Linux jobs run the real mode calls and fail rather than skip. Sudo detection from the environment is still untested (finding 6).
3. Per-file owner check Closed FileOwner.cs is gone.
4. Three platform checks Closed One predicate, PlatformDetection.cs:37-38.
5. Two outcomes for an unusable home Closed One decision, QueueDirectory.cs:37-47, with one fallback. But see finding 2.
6. Link swap and unbounded read Partly closed Reaching the folder is harder; the read path is unchanged (finding 5).
7. sudo keeping HOME Partly closed Only HOME is compared (finding 4).
8. Endpoint stored twice Open Not touched; fine to defer.
9. Two folder conventions Partly closed Same helpers, but the settings folder is chosen elsewhere and gets no checks (finding 7).
10. Old /tmp folder Docs only Nothing deletes the user's own files left there.

Medium

  1. Whoever owns the base folder is trusted (security, with hand-offs from architect and skeptic). See QueueDirectory.cs:147 and :153-163.

    • What: a sticky, other-writable base is accepted on the basis that only an entry's owner may rename an entry. The base's own owner may too, and nothing checks who that is. Suppose another user creates /tmp/.cache at 1777 first (HOME=/tmp, or XDG_CACHE_HOME under /tmp). After the victim verifies uno, that user can rename it and put their own folder in its place. StorageService keeps using the path, so the rest of the run queues there.
    • Also: in a sticky base without fs.protected_symlinks (macOS with TMPDIR=/tmp, FreeBSD), a link planted between the symlink check (:155) and the create and chmod (:161-163) is followed. Under root, that can chmod any folder.
    • Direction:
      • Refuse mode bit 0002 even with the sticky bit.
      • Prove the base is the user's with a no-op SetMode(base, GetMode(base)), refusing on UnauthorizedAccessException.
      • Check IsSymbolicLink again after the loop.
      • On macOS, consider ~/Library/Caches when TMPDIR isn't per user, rather than refusing.
    • Accepted, not a finding: a group-writable base lets members of a shared group do the same. The PR accepts this on purpose for Ubuntu's private-group 0775 ~/.cache, and documents it.
  2. No real host can see why telemetry turned off (skeptic; the architect raised the same point under fallbacks). See Telemetry.cs:266 and TelemetryDiagnostics.cs:23-24.

    • What: the reason goes only to Trace, which no Uno host listens to, so each case is a silent drop on the machine and in the data. Cases that worked with the released /tmp queue and now send nothing:
      • docker run -u <uid> with no passwd entry, so HOME=/;
      • devcontainers whose ~/.cache Docker created as root;
      • images that ran chmod -R 777 $HOME;
      • NFS all_squash, CIFS without Unix extensions, WSL DrvFs without metadata;
      • MSBuild on Mono or .NET 6;
      • plain sudo on macOS, which keeps HOME.
    • Direction: also write the reason through PersistenceChannelDebugLog, the existing UNO_TELEMETRY_ENABLE_PERSISTENCE_CHANNEL_DEBUG_OUTPUT switch. After release, compare event volume by OS and runtime.
  3. The Linux jobs don't gate release (architect). See .github/workflows/ci.yml:214-215.

    • What: sign needs only build. The two Linux jobs are the only place this change runs on Linux.
    • Direction: add both jobs to sign's needs. This is the repo owners' call.

Low

  1. The sudo guard compares HOME but the queue path can come from elsewhere (security, skeptic). See QueueDirectory.cs:129 against :61-79.
    • With sudo -E and XDG_CACHE_HOME exported (Fedora's always_set_home, or env_keep), HOME is /root, the guard passes, and root creates a root-owned uno in the user's cache. The user's own runs then fail.
    • su -m and su -p set none of the variables checked.
    • SUDO_UID=0 (root running sudo itself) is refused although no other user is involved.
    • Direction: under sudo or doas, compare the chosen base folder with root's home. Ignore SUDO_UID=0.
  2. Queued files are still opened through links and read without a cap (security). See StorageService.cs:220 and :479-481. A link to /dev/zero or a FIFO, planted through finding 1, an NFS mount or before the folder was made private, can allocate up to 2 GB or block Peek. Direction: reject a file when !claim.CanSeek, and read at most min(claim.Length, cap) bytes.
  3. Untested and misleading paths (skeptic).
    • Sudo detection from the environment (UserFolders.cs:61-62) is never tested: every test injects the flag.
    • Every refused chmod is reported as "belongs to another user" (UnixFolder.cs:159-162). EPERM also comes from NFS squashing, chattr +i and ZFS aclmode=restricted.
  4. The settings folder gets none of these checks (security, architect). See Telemetry.cs:337-347. With HOME=/tmp it is /tmp/.uno/telemetry, the old shared queue folder, and .machinehash there can be planted or read; it is read without a size limit (TelemetryCommonProperties.cs:150). Its choice also lives apart from QueueDirectory and splits on IsLinux rather than Windows, macOS and the rest. Direction: choose and prepare it in QueueDirectory, keeping the existing folder so machine ids don't change.
  5. A queue folder deleted mid-run (skeptic). See StorageService.cs:92 and :427-430. This isn't a regression. Items queued in the gap are lost silently, and once any Uno process recreates the folder the long-running one writes into it without checking it. No test covers it.
  6. Structure (architect).
    • UserFolders mixes facts about the machine it describes with the real host's mode calls and root check (UserFolders.cs:45-49). A Linux machine described on Windows gets no mode calls, which is what c50d30b worked around. Set those defaults only in Current().
    • StorageService relies on a prepared folder only by a comment (StorageService.cs:77-81), and docs/usage.md:172-173 still says the storage location is configurable.
    • UNO_TELEMETRY_TESTS_REQUIRE only bites for tests that call TestRequirements.Skip, which nothing enforces, and the macOS paths have no CI job.

Info

  • The chmod "proves ownership" claim has limits worth documenting (security):
    • root and CAP_FOWNER can chmod anything;
    • NFS all_squash, CIFS noperm, and FUSE allow_other without default_permissions read back 0700 without enforcing it;
    • ACLs on macOS, ZFS and NFSv4 survive the chmod, and the read-back sees only mode bits. Linux POSIX ACLs are masked, so they are fine.
  • Nothing fails open (security). Every catch returns a failure, and anything else reaches the catch at Telemetry.cs:315. The Trace line holds paths, so the login name, but never reaches telemetry items.
  • Concurrency (skeptic): no defect found. Every step is idempotent.
  • Reflection (architect, skeptic): sound for MSBuild and generator hosts, and TelemetryQueueFolderTests.cs:271-296 exercises the real netstandard2.0 build. The lookups also run at type initialization on net8 and net9, where only the tests use them; making them lazy would avoid that. The user-name fallback in IsRootByReflection (UnixFolder.cs:94) runs nowhere.
  • Tests that build the storage directly (architect hand-off, not checked by the others): PersistenceChannelResiliencyTests.cs:64 and following, and TelemetryMachineNameTests.cs:64, store nothing unless they create the folder first. Check that each asserts something was stored.

Suggested order

  1. Refuse other-writable bases, prove the base is the user's, and re-check links (finding 1).
  2. Surface the reason through the debug switch (finding 2).
  3. Base the sudo guard on the chosen folder (finding 4).
  4. Cap and type-check the read (finding 5).
  5. Leave gating release on the Linux jobs (finding 3) to the owners.

Findings 6 to 9 can follow in this PR or a later one.

…y is off

Addresses findings 1, 2, 4 and 5 of the review panel on a2f9b92..4a86b71:

- The folder that holds the queue folder is refused when other users
  can write to it even with the sticky bit, since the owner of a sticky
  folder such as /tmp may still rename entries in it. It must also be
  the user's own: setting its mode to the one it has succeeds only for
  its owner, or root. The folders the library names are checked for
  symbolic links again at the end, for one swapped in meanwhile.
- The reason telemetry is disabled also goes to the console when
  UNO_TELEMETRY_ENABLE_PERSISTENCE_CHANNEL_DEBUG_OUTPUT is set: no host
  listens to Trace.
- Under sudo or doas, root creates nothing outside root's home, judged
  by the folder that would hold the queue folder rather than HOME, so
  sudo keeping XDG_CACHE_HOME is caught too. SUDO_UID=0 and
  SUDO_USER=root, as when root runs sudo itself, no longer count.
- A queued file is read only up to its own length, and only if it is
  seekable and no longer than the storage's capacity: a link to a pipe
  or to /dev/zero is set aside as corrupt instead of being read without
  end.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Jen-Uno

Jen-Uno commented Nov 3, 2026

Copy link
Copy Markdown
Contributor Author

Findings 1, 2, 4 and 5 of the review panel above are addressed in 604ad66:

  • Finding 1, base folder another user owns. Refused when other users can write to it, even with the sticky bit. It must also be the user's own: the library sets its mode to the mode it already has, which only its owner, or root, may do. The folders the library names are checked for links again at the end. So telemetry now turns off with TMPDIR=/tmp, and with HOME=/tmp when another user created /tmp/.cache.
  • Finding 2, invisible reason. The "Telemetry is disabled for this instance" line also goes to the console when UNO_TELEMETRY_ENABLE_PERSISTENCE_CHANNEL_DEBUG_OUTPUT is set. Checked by hand with HOME=/ and with TMPDIR=/tmp on macOS.
  • Finding 4, sudo guard. Root under sudo or doas creates nothing outside root's home, judged by the folder that would hold the queue folder rather than by HOME, so sudo -E keeping XDG_CACHE_HOME is caught. Root running sudo itself (SUDO_UID=0, SUDO_USER=root) no longer counts. The variables are now parsed by a tested function. su -m and su -p set none of them and are documented as not detected.
  • Finding 5, unbounded read. A queued file is read only if it is seekable and no longer than the storage's 10 MB capacity, and never past its own length. A link to /dev/zero or a pipe is set aside as corrupt.

Tests: 299 cases, 297 passed and 2 skipped (both need root), macOS arm64, Debug and Release. CI covers the Linux paths.

Not addressed here: 3 (whether sign should wait for the Linux jobs) is the repo owners' call; 6 to 9 can follow.

The code-quality check still read the failure reason as possibly null:
the same test null-checks it earlier. It now asserts on the reason or
an empty string, so a missing reason still fails the test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Jen-Uno
Jen-Uno marked this pull request as ready for review November 3, 2026 17:25
Comment thread src/Uno.DevTools.Telemetry/Telemetry.cs Resolved
…me, and let the opt-out only opt out

From the review of this PR:

- When no queue folder of the user's own is usable, the instance sends from
  memory instead of turning off: every 5 seconds, and on Flush, FlushAsync and
  Dispose (at most 2 seconds). Each item carries a "Queue Refused" property with
  a fixed code, never a path, so the loss shows instead of looking like fewer
  users. The machine id isn't stored then either. docs/usage.md lists the codes
  and UNO_TELEMETRY_ENABLE_PERSISTENCE_CHANNEL_DEBUG_OUTPUT.
- As root, no folder outside root's home is used, whether or not sudo or doas
  started the process. The SUDO_*/DOAS_USER check is removed.
- UNO_PLATFORM_TELEMETRY_OPTOUT can only opt out: false reads as unset, so the
  product decides. The docs list 1 and true, as the public telemetry page does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comment thread src/Uno.DevTools.Telemetry/Telemetry.cs Dismissed
@mergify

mergify Bot commented Nov 4, 2026 •

Copy link
Copy Markdown

This pull request does not currently match the merge queue conditions, so it cannot be queued from here. The box comes back if it matches again.

@Jen-Uno
Jen-Uno merged commit cc8f7e6 into main Nov 4, 2026
13 checks passed
@Jen-Uno
Jen-Uno deleted the dev/jen/telemetry-privacy-fixes branch November 4, 2026 20:19
agneszitte added a commit that referenced this pull request Nov 12, 2026
…y-privacy-1.4

fix: honour the documented opt-out, keep the machine name out and the Linux queue private (backport #35)
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