Two processes can open the same absolute path and receive different files. Neither needs a different UID, and neither result implies the underlying partition was rewritten. This is the missing filesystem mechanism behind many “systemless” explanations.
Before you start#
Read Inside systemless root, How a process becomes root, Linux files and processes, and Boot-image inspection. We analyze Magisk v28.1 and Linux mount semantics. Required practice is read-only or synthetic; there are no mount, setns, unshare, flash, or integrity-concealment instructions to run on Android.
Learning goals#
Distinguish a filesystem, mount, bind mount, overlay filesystem, mount namespace, and on-disk image; work out different path resolutions in two namespaces; trace the pinned Magisk namespace-selection branch; and explain why removing a visible overlay does not necessarily restore persistent boot changes or invalidate open file descriptors.
A path is a lookup through a mount tree#
A filesystem maps names to objects. A mount attaches a filesystem or filesystem subtree at a path in a mount tree. When a process resolves /system/etc/demo.conf, lookup traverses its root, directories, and any mount points encountered. A mount namespace gives a process a particular view of that tree. A namespace is not a second copy of every file's data and not an additional user identity.
A bind mount attaches an existing file or directory at another location. It can make an alternate file appear at the original path without changing the original file's bytes. An overlay filesystem instead combines lower and upper filesystem layers according to overlayfs rules, including copy-up behavior. “Overlay” in a root-framework explanation may mean a general runtime substitution, not necessarily Linux overlayfs. Inspect the implementation before choosing the technical noun. See mount_namespaces(7), mount(2), and Linux overlayfs.
Synthetic teaching data, not a device capture.
- Namespace A
- /system/etc/demo.conf resolves to base content: mode=stock.
- Namespace B
- A bind mount covers that path with alternate content: mode=lab.
- Base image and persistent payload
- Base bytes can stay unchanged while alternate files and boot changes persist elsewhere.
- Existing open descriptor
- Keeps its opened file object even if later path lookup changes.
Worked example, invented paths: an immutable base image contains mode=stock at /system/etc/demo.conf. Another filesystem contains mode=lab at /data/lab/demo.conf. Namespace A retains the base view. Namespace B has a file bind mount covering /system/etc/demo.conf with the alternate file.
| Observation | Namespace A | Namespace B |
|---|---|---|
Open /system/etc/demo.conf after setup | Base file, mode=stock | Alternate file, mode=lab |
| Base image bytes | Unchanged | Unchanged |
| Does this prove different UIDs? | No | No |
| Is the alternative necessarily stored in RAM? | Not established | Not established |
The change can be persistent in a different place even though the base partition is unchanged. Files under a module directory, a patched boot image, and policy/database state can survive reboot; mounts are reconstructed at runtime. “No writes to /system” is therefore narrower than “nothing persistent changed.”
Open descriptors outlive a pathname view
Suppose a process opened the original file before it was covered by a mount. Its descriptor refers to the already-open file object. A later mount changes future path lookup; it does not rewrite that descriptor into a reference to the replacement. Similarly, an already mapped library or cached configuration may remain in use after the visible file path changes. See open(2).
This explains an apparently contradictory failure: a diagnostic shell reads the replacement, while a daemon still behaves as if it read the original. Possible causes include different namespaces, an earlier open, an existing mapping, or application caching. Do not immediately conclude the mount failed or the program ignored a security check. First establish which process, which namespace, and when the file was opened.
Namespace copying and propagation are separate#
Creating a new mount namespace starts from a copy of the existing mount topology, with references to the underlying objects. It is not necessarily a fully isolated topology forever. Linux mount propagation determines whether later mount/unmount events spread between related mount points. Shared, private, slave, and unbindable mount relationships are distinct from filesystem read/write permissions.
Why this exists: services and containers sometimes need changes to propagate to selected peers, while other processes need stable private views. Merely creating a namespace does not settle that policy. Linux documents propagation in shared subtrees and the mount-namespace man page.
Also separate a mount namespace from a linker namespace: the latter constrains native-library visibility/loading in the dynamic linker, not the kernel mount tree. A process can see a library file but still be unable to load it under linker namespace rules. Neither namespace grants a SELinux permission by itself.
Magisk v28.1: where the command's view is chosen#
In pinned native/src/core/su/su_daemon.cpp, follow the authorized child after its stream/session setup. The namespace switch distinguishes three modes:
| Mode in the pinned source | Relevant behavior | Important limit |
|---|---|---|
| Global | Retains the daemon-side view in this branch | “Global” is a project label, not proof that every Android process sees it |
| Requester | Attempts to switch to the selected requester's mount namespace | The branch logs a fallback if switching fails |
| Isolate | Attempts a switch, creates a new mount namespace, then makes mounts recursively private | Does not erase persistent module or image data |
The branch also interprets ctx.req.target and can alter the selected mode. Read those conditionals, not just the mode labels. The source establishes an implementation choice; it does not prove a given request succeeded in entering the intended namespace. Actual namespace identifiers, return paths, and mount evidence would be needed for that runtime claim.
Later, the same child prepares environment, optionally requests an SELinux exec context, sets identity, and executes the shell. Mount view, identity, and SELinux configuration occur in one setup path, but remain separate mechanisms. A command can have UID 0 and still see an unexpected path because it was started with another namespace mode.
The earlier persistent change: boot-image patching#
How does the daemon become available in the first place? In v28.1 scripts/boot_patch.sh, inspect these stages as source, not a script to execute:
- Unpack a supplied supported image with MagiskBoot; abort on unsupported/error results.
- Inspect ramdisk patch state, preserving or restoring information for supported cases.
- Add
magiskinitas the ramdisk'sinit, add compressed payload/configuration material, and perform the script's conditional patch work. - Repack an output image; device-specific acceptance and installation remain separate.
This is a structured image transformation, not adding a manager APK to /system/app. An image header, ramdisk archive, executable payload, and verification metadata have different roles. Android 13-launch boot layouts may place the generic ramdisk in init_boot; older layouts differ. A version number alone does not identify the correct image for every upgraded device. Use the boot-image lesson to understand the artifacts, and the project's installation documentation for its device-specific boundaries—not a guessed universal flash command.
At boot, pinned native/src/init/init.cpp selects a setup path and eventually hands execution back to Android init. The later runtime file view is only one result of this earlier boot foothold. Restoring a visible file in one namespace is not necessarily restoring the original boot image.
Practice#
Using the invented example, draw a timeline:
- Process P opens the base file in namespace A.
- Namespace B gets the alternate mount.
- Process Q in B opens the same path.
- The mount in B is removed after Q's open.
Predict P's existing descriptor, Q's existing descriptor, and a new open in B. Model answer: P still refers to the base file; Q's existing descriptor still refers to the alternate file; the new lookup sees the uncovered base path, assuming no other changes. File-object lifetime, not pathname spelling, explains the result.
Optional read-only Linux HOST observation: readlink /proc/self/ns/mnt and cat /proc/self/mountinfo show the observing process's namespace identity and mount table. A host result does not establish Android or Magisk behavior. On a disposable Android target, access to another process's namespace/mount information may be denied; record that limitation instead of escalating. Mount tables can contain private paths, so redact before sharing.
Knowledge check#
- Can identical UIDs have different file views?
- Does “systemless” mean no persistent image or module changes?
- Does removing a mount replace an existing open descriptor's file?
- Why inspect mount propagation as well as namespace creation?
Answers#
- Yes. UID and mount namespace are independent state.
- No. It may describe only how a base filesystem is presented at runtime.
- No. The descriptor continues to reference its opened file object.
- A new namespace alone does not specify whether later mount events propagate between related mounts.
Checkpoint#
Ready when your two-view diagram and four-step timeline distinguish path lookup, file objects, namespace, propagation, persistent payload, and boot acceptance. Revise if every overlay is called overlayfs, or a file disappearing from one shell is treated as full rollback. Revisit the namespace branch and draw a separate persistence inventory. Actual Android mount behavior remains not yet evidenced without an identified runtime observation.
Sources#
- Linux man-pages mount namespaces,
mount, andopen— topology, bind mounts, and open-file lifetime. - Linux 6.1 shared subtrees and overlayfs — propagation and the specific overlay filesystem, not interchangeable concepts.
- Magisk v28.1
su_daemon.cpp,boot_patch.sh, andinit.cpp— pinned namespace, image-transformation, and early-init paths. - AOSP boot-image header and Magisk installation guide — moving documentation; layout/support must be checked for the exact target.