I'm seeing some odd behavior which may be a bug. I've broken it down to a least common denominator to reproduce it. But maybe I'm doing something wrong.
I am opening a file read-write. I'm then mapping the file read-only and private:
void* pointer = mmap(NULL, 17, PROT_READ, MAP_FILE | MAP_PRIVATE, fd, 0);
I then unmap the memory and close the file. After the close, eslogger
shows me this:
{"close":{"modified":false,[...],"was_mapped_writable":false}}
Which makes sense.
I then change the mmap
statement to:
void* pointer = mmap(NULL, 17, PROT_READ, MAP_FILE | MAP_SHARED, fd, 0);
I run the new code and and the close looks like:
{"close":{"modified":false, [....], "was_mapped_writable":true}}
Which also makes sense.
I then run the original again (ie, with MAP_PRIVATE vs. MAP_SHARED) and the close looks like:
{"close":{"modified":false,"was_mapped_writable":true,[...]}
Which doesn't appear to be correct.
Now if I just open and close the file (again, read-write) and don't mmap
anything the close still shows:
{"close":{ [...], "was_mapped_writable":true,"modified":false}}
And the same is true if I open the file read-only.
It will remain that way until I delete the file. If I recreate the file and try again, everything is good until I map it MAP_SHARED.
I tried this with macOS 13.6.7 and macOS 15.0.1.
I'm seeing some odd behavior which may be a bug. I've broken it down to a least common denominator to reproduce it. But maybe I'm doing something wrong.
What you're seeing may seem like odd behavior, but it's actually normal and expected. Here is what the header documentation in ESMessage.h says:
* `was_mapped_writable` only indicates whether the target file was mapped into writable memory or not for the lifetime of the
* vnode. It does not indicate whether the file has actually been written to by way of writing to mapped memory, and it does not
* indicate whether the file is currently still mapped writable. Correct interpretation requires consideration of vnode lifetimes
* in the kernel.
Note: If you weren't aware, the most of the EndpointSecurity headers are heavily commented and those comment should generally be considered as the most authoritative documentation for this framework.
The key sentence there is:
Correct interpretation requires consideration of vnode lifetimes in the kernel.
That could easily have been written "vnode caching behavior is complicated and unpredictable, so don't be surprised if the value in this flag seems weird".
You can talke a look at MFSLives for a deeper discussion of how VFS caching works, however, I think what's basically happening here is the following:
-
The file was initially opened read-only so that was the state the vnode was created with.
-
The file was mapped read/write, so the vnode became writeable.
-
Additional access continued occurring, so the vnode from #2 continued to be reused.
One thing to be clear about here is that the file systems view of writability is about how the VFS system interactions with a given file object, NOT larger system security. If 9 process open a file read-only and one process opens it read/write, then the vnode is now writable. That doesn't mean anything changed for those other 9 process.
It will remain that way until I delete the file.
I suspect this is because of how you're testing. As long as you're actively interacting with the file, the VFS system will keep pulling the same vnode out of the cache, leading to the result you're seeing. Leave it alone "long enough" and the vnode will (probably) get evicted from the cache, returning to back to state #1.
Of course, that comes with two qualifiers:
-
"Long enough"-> isn't really defined in any coherent way. If you're on an "active" file system (something like the boot volume where lots of processes are interacting with it) then I'd expect it would be evicted fairly quickly. However, it's possible for a vnode to remain in the cache "forever" (the system won't evict it unless it "has" too).
-
"probably"-> the file system driver itself also has a great deal of control over how the cache is managed. It's possible for a vnode to remain in the cached regardless of other system activity simply because that's what a particular file system is "doing".
If this makes it seem like "was_mapped_writable" isn't all that useful for determining what's actually happened to a file... well, yes, I think that's correct. There are many situations where the EndpointSecurity system gives you the information it can get, which isn't the same as the information you'd want.
I think it's main value is that was_mapped_writable==false lets you quickly know that something CAN'T have been modified. There are lots of files that the system only maps read-only (for example, executables) and this makes it easy to quickly ignore/filter those cases.
__
Kevin Elliott
DTS Engineer, CoreOS/Hardware