File Notification Side Channels: Watch Permission Is a Read Permission You Never Audited
On Linux, an unprivileged process can mount an inotify watch on a directory it can read and receive events for files inside that directory it cannot read. Th...
On Linux, an unprivileged process can mount an inotify watch on a directory it can read and receive events for files inside that directory it cannot read. The kernel checks read permission on the watch target, then reports activity on children the caller has no access to. That single asymmetry — watch permission decoupled from read permission — is the root cause behind a class of side channels that Graz University of Technology researchers formalized in File Notification Attacks, presented at ACM CCS 2026.
The events never carry file contents. They carry path names and timestamps. That is enough. With those two fields, the team reconstructed keystrokes, detected KDE authentication prompts for a clickjacking attack, and identified which websites a second user visited in real time.
This is not a new API bug. It is a twenty-year-old design decision that four operating systems made independently and that nobody threat-modeled as a permission boundary.
The Permission Model Nobody Audited
Every modern OS ships a file-notification subsystem: inotify on Linux (2005), ReadDirectoryChangesW on Windows (2000), FSEvents on macOS (2007), and FileObserver on Android (2008). They exist so tools like file syncers, build watchers, and antivirus engines can react to filesystem changes without polling.
The design assumes notifications are a convenience layer over data the caller can already access. In practice, the notification channel answers a different question than the read path. A read asks "what is in this file?" A notification asks "did something touch this path, and when?" The second question leaks information even when the first is denied.
The critical gap is temporal and spatial. Timing reveals behavior patterns; path names reveal intent. Neither requires content access, so neither trips the permission checks engineers assume are comprehensive.
If your threat model says "the attacker cannot read the file, so the attacker learns nothing," you have not modeled the notification channel.
How a Notification Stream Becomes a Keystroke
Raw timing data is not a keylogger. The attack is a template attack: record labeled traces, then match unknown traces against them. The team's contribution is a method to discover which notification streams carry exploitable leakage, then build the templates automatically.
The pipeline has three stages. First, discovery: watch candidate directories, drive known activity, and measure whether event timing correlates with the activity. Second, templating: capture traces under controlled input to build a labeled reference set. Third, inference: match a live trace against the templates and output the most likely input.
import time, subprocess, collections
def collect_trace(duration_s=10.0):
"""Sample inotify access events on /dev/input into a timestamp list."""
proc = subprocess.Popen(
["inotifywait", "-m", "-e", "access", "--format", "%T", "--timefmt", "%s.%N",
"/dev/input"],
stdout=subprocess.PIPE, text=True)
end, events = time.time() + duration_s, []
try:
while time.time() < end:
line = proc.stdout.readline()
if line:
events.append(float(line.strip()))
finally:
proc.terminate()
return events
def gaps(events):
"""Inter-keystroke intervals are the feature vector, not absolute times."""
return [round(b - a, 4) for a, b in zip(events, events[1:])]
# Cluster gaps into n-gram templates, then nearest-neighbor match live traces
The feature vector is intervals, not absolute timestamps — that is why it survives clock noise and why the SSH variant hit 100% accuracy. A remote attacker never needs the victim's clock; the local event stream already carries the rhythm.
The hard part is not inference. It is discovery: knowing which of thousands of watchable paths correlate with the activity you want. The paper's method treats every path as a hypothesis and the correlation test as the filter.
Who Is Actually Exposed
The threat model is a second principal with local code execution and read access to a shared parent directory. That condition is common on infrastructure engineers routinely operate.
- Shared CI runners. Multiple jobs from untrusted branches run as different users on one host. A job that can read
/devor a shared workspace can watch siblings. - VDI and jump hosts. Many users, one machine, per-user home directories with a readable parent. The keystroke channel does not require physical access.
- Multi-tenant build and test machines. Any box where two principals share a filesystem parent is a candidate.
- Windows workstations with fast user switching. The drive-root watch crosses user boundaries outright, so the classic "same user" caveat does not apply.
The researchers found no evidence of in-the-wild exploitation. That does not reduce the impact for defense planning — it only means the window between publication and abuse is still open. Proof-of-concept code is public on GitHub.
One nuance limits the blast radius on Linux: the caller must have read permission on the parent directory. Lock that parent down — chmod o-rx on sensitive parents — and the direct watch path fails. The Windows drive-root case bypasses the parent check entirely, which is why it is the more serious finding.
The attacker does not need the file. The attacker needs the parent directory, and on Windows not even that.
Linux: Watching a Directory You Can Read, Reading Files You Can't
On Linux the asymmetry is explicit. Mounting a watch directly on an unreadable file fails with EACCES. But if the file's parent directory is readable, watching the directory succeeds and delivers events for every child, including ones the caller cannot open.
# Direct watch on a protected file: denied
$ inotifywait -e access /dev/input/event0
inotifywait: Permission denied
# Watch the parent directory instead: succeeds, leaks children
$ inotifywait -m -e access,modify /dev/input
Setting up watches.
Watches established.
/dev/input/event3 ACCESS
/dev/input/event3 ACCESS
/dev/input/event3 MODIFY
Each ACCESS on /dev/input/event* corresponds to a read from the input device, which fires on every keystroke. The event does not say which key was pressed. It says when. Inter-keystroke timing, combined with a reference model of typing rhythms, is a well-studied attack primitive.
The team measured 93.1% to 100% keystroke-accuracy across seven local users and 100% against a remote SSH session. The remote case matters: file notification events survive network boundaries because they are generated locally on the victim host, not derived from traffic.
A second Linux case turns the same channel into credential theft. Monitoring /usr/bin/pkexec reveals exactly when Polkit spawns an authentication dialog. A malicious process draws a look-alike password window over the real one and collects what the user types. The signal is not the prompt's content — it is the event announcing that a prompt now exists.
The kernel shipped a partial fix in December 2025, tracked as CVE-2025-68788 in the fsnotify subsystem. It stops generating ACCESS and MODIFY events on child special files for parent-directory watchers — closing the /dev/input channel specifically. The fix landed in 5.10.248, 5.15.198, 6.1.160, 6.6.120, 6.12.65, and 6.18.3.
Note the scope. The patch aligns fsnotify behavior with what stat() already exposes: stat() never showed atime or mtime changes on special device files touched by other users, so the notification layer should not have either. The broader filesystem class of leakage — ordinary files under a readable directory — remains by design.
Android: FileObserver Sees Through the FUSE Sandbox
Android wraps inotify in the FileObserver Java class. Since Android 10, apps see their storage through a FUSE layer that presents each app a scoped view of the filesystem, enforcing the per-app sandbox at the storage layer rather than only via Linux DAC.
The flaw is that FileObserver registers watches below that view. It resolves to the underlying kernel path, so an unprivileged app can watch another app's private directory even though the FUSE layer would deny it direct access. The sandbox is a view, and the notification channel bypasses the view.
The researchers demonstrated this against WhatsApp, revealing precisely when photos, videos, and files arrive or get deleted in the app's private storage. Message timing, media receipt, and deletion events leak without any content access and without a dangerous permission.
FileObserver observer = new FileObserver(
"/data/data/com.whatsapp/files/Media",
FileObserver.CREATE | FileObserver.DELETE) {
@Override
public void onEvent(int event, String path) {
Log.d("leak", "event=" + event + " path=" + path + " t=" + System.currentTimeMillis());
}
};
observer.startWatching();
On a correctly sandboxed system this code should fail at watch registration. It does not, because the FUSE view is enforced at the read boundary, not the watch boundary. No mitigation for Android appeared at the time of disclosure.
Windows: C:\ Reports Every Path, By Design
Windows is the most aggressive case. Mounting a watch on the root of a drive reports the full path of every file touched anywhere on the system, across users and regardless of permissions.
Because browsers write per-site storage under predictable paths, those path names can name the site directly. Watching the drive root reconstructs which websites another user visits in real time without touching network traffic or browser memory.
using var watcher = new FileSystemWatcher(@"C:\");
watcher.IncludeSubdirectories = true;
watcher.NotifyFilter = NotifyFilters.FileName | NotifyFilters.LastWrite;
watcher.Created += (s, e) => Console.WriteLine(e.FullPath);
watcher.EnableRaisingEvents = true;
// Yields full paths under C:\Users\<other>\... with no permission check
Microsoft's response to the disclosure was that the behavior is by design — an "undocumented feature" that reveals only file names and paths in another user's profile directory, not file contents or sensitive data. The researchers' findings earned Microsoft a nomination for the "lamest vendor response" category at the Pwnies Award 2026.
A mitigation exists: the registry policy EnforceDirectoryChangeNotificationPermissionCheck, documented in April 2025 after an earlier report. It is disabled by default. The fix ships in the product and stays off unless an administrator knows to enable it.
That is the sharpest contrast in the whole disclosure. Linux shipped an unconditional kernel fix. Windows shipped an opt-in flag and argued the default behavior is not a vulnerability.
What Actually Mitigates This (and What Only Looks Like It Does)
macOS came out best. FSEvents leaked only information about globally readable files; the team found no bypass for reading private directories comparable to Linux, Windows, or Android.
The real lesson is that the mitigation must live at the watch-registration boundary, not the event-delivery boundary. Filtering events after the fact is fragile — it requires enumerating what is sensitive, which is the same problem you were trying to avoid.
- Align watch permission with read permission. If a caller cannot read a path, a watch on its parent should not surface that path's events. This is what CVE-2025-68788 partially implemented for special files.
- Treat path names as sensitive data. Full-path disclosure across users is an information leak even without contents. Minimize the path returned to watchers.
- Scope watches to the caller's storage view. A FUSE or sandbox layer must intercept watch registration, not only read syscalls.
- Do not ship security fixes disabled by default. An opt-in flag protects only the administrators who already read the advisory.
For defenders, the concrete action is a version check. Confirm your fleet runs a kernel with the fsnotify fix, and on Windows evaluate whether EnforceDirectoryChangeNotificationPermissionCheck is worth the compatibility risk for shared machines.
uname -r
# Fixed: 5.10.248+, 5.15.198+, 6.1.160+, 6.6.120+, 6.12.65+, 6.18.3+
sysctl fs.inotify.max_user_watches fs.inotify.max_user_instances
# Lower these on multi-tenant hosts to bound watch abuse
None of this closes the general side channel. It only closes the sharpest instances. Any system that lets one principal observe the activity of another — even just the timing — will leak something, and file notification is one of many such channels alongside the page cache and hardware caches.
The durable fix is architectural: make the notification subsystem a view of the same permission model that governs reads. Until then, watch permission is a read permission you did not know you were granting.