I can share what I found out some time ago when I researched the subject myself. It's certainly not authoritative nor exhaustive but it may be of some help.
Technical description
Disclaimer note
I myself have never actually implemented any of the approaches that I am about to describe. They are just possibilities I pondered for a custom containerization project I worked on some time ago, but then decided to drop namespace detection completely. If anyhow interested, read on.
This is an approach to detect initial namespaces by leveraging observed behaviors by the kernel. (But beware these behaviors are not official API). This approach would work under most common & sane setups, though they may not always be the case.
"Boot" namespaces
Starting from kernel v3.8 up to the current latest stable v5.11 (and current v5.12-rc), the initial IPC, UTS, user, PID, cgroup, and time namespaces always have a specific hardcoded ID as listed below. Therefore we can safely assume that for those namespace types any namespace ID greater than the fixed ones may be considered child namespaces:
IPC = 0xEFFFFFFF
UTS = 0xEFFFFFFE
USER = 0xEFFFFFFD
PID = 0xEFFFFFFC
CGROUP = 0xEFFFFFFB
TIME = 0xEFFFFFFA
The above list is taken from v5.12-rc6 sources, but those values have always been the same since v3.8, except of course for the namespaces that did not exist at all in v3.8 ("cgroup" has been added in v4.6, while "time" in v5.6).
Notice how those initial namespaces have been added (over the years) with values "growing" downwards. Conversely, all child namespaces take sequential (on-demand) values growing upwards from 0xF0000000
.
So, for setups where no ancestor process overrode initial namespaces, those fixed values may settle the "quest for detection" of those namespaces quite neatly.
However, let me reiterate that those values are not at all part of any official API exposed to userspace (not even to kernel-space AFAICT) hence they may be subject to change in the future.
The kernel developers might even choose to make them all dynamic or even random. In fact, you may notice that mount and network namespaces are not in that list, and that's because all network and mount namespaces, including the initial ones, are already entirely dynamic, and always take IDs starting from 0xF0000000
just like any child namespace. As a result, for mount and network initial namespaces one must still do some heuristics even under the friendliest conditions.
Mount namespace
In my experience so far, I've noticed that the initial mount namespace ID always gets the first dynamic value (0xF0000000
). Supposedly that's due to the initial PID namespace instantiating the general proc
filesystem, thus pulling-in the first mount namespace as well. Anyways, the initial mount namespace's ID seems to be easily predictable, being practically fixed even though among the dynamic range.
Network namespace
The initial network namespace ID on the other hand gets faraway values and can be different even from previous boots of the same machine with same OS when configuration changes affect the sequence of inode numbers generated dynamically. Therefore, detecting an initial network namespace may become a true lottery. You can "win" it more often than not, but this requires assuming several things that, although they hold true in common sane setups, they do not necessarily always hold.
The first network namespace is instantiated as a consequence of any process (typically PID 1) requesting the first networking operation since system's boot. Hence a /proc/net/
directory becomes available and files/directories get created therein, each with their own inode number allocated from the same (dynamic) values used for namespace IDs. It so happens (in my experience so far at the time of this writing) that the first name created therein is the stat
directory. Hence that directory takes the last inode number generated immediately before the instantiation of the network namespace. The network namespace's own ID is therefore /proc/net/stat
's inode number + 1.
Naturally the /proc/net/stat
directory, being in fact a "namespaced" name itself, as seen by an arbitrary process may not necessarily refer to the initial network namespace. It does refer to the initial network namespace when the process accessing that directory lives in the initial namespaces (i.e. it is a non-containerized process), but in a containerized environment that would more likely refer to the dedicated network namespace the process belongs to rather than to the initial network namespace.
Q: So how can a process try to guess generically whether its network namespace is in fact the initial one?
A: By enumerating recursively all visible non-pid files/dirs within its /proc
directory to look for inode numbers starting from 0xF0000001
until it encounters the first hole of at least 2 missing inode numbers.
Many non-pid files/dirs in /proc
are (to this date) common to all PID namespaces because they are related to kernel's core functionalities (e.g. irq statistics, etc.). The hole in their inode numbers has to be of at least 2 adjacent numbers because one is for the /proc/net/stat
directory created for the initial network namespace and one for the initial network namespace itself (also assuming an atomic allocation between the two). In such first hole there would lie the initial network namespace's ID. Compare that (hole of) IDs with the process's own (or other arbitrary) network namespace's ID and (in most common cases) you are finally all set at last.
However, even for common cases, it is obvious that we are relying on those non-pid names being always visible to all PID namespaces and sharing the same numbering as the namespaces's IDs and being in (nearly) perfect sequence and the /proc/net/*
inode numbers being allocated atomically together the namespace's own ID. All these assumptions may hold true now, but may well stop being true in the future simply because that behavior is not at all official API.
In addition, just to further point out how tricky this matter is, note that what is viewed under /proc
is always the PID namespace of the process that mounted that specific /proc
directory, hence not necessarily the PID namespace of the process reading that /proc
directory. A discrepancy between "mounter" of /proc
and "reader" of /proc
is unlikely to occur in sane practices, but still perfectly possible and easily leading to inconsistent analysis.
Some opinionated considerations
Except for the initial user namespace, whose detection is really easy1 and also part of an official API, detecting namespaces is a problem that takes a lot of effort to tackle, if at all possible, because there is not a real and comprehensive API supporting it (which is supposedly deliberate for the sake of fuller isolation). A few years ago a couple of ioctl(2)
operations have been added to the list for namespaces, but they are still quite limited and I couldn't fathom any way (not even insane ones) of using them for definitive detection purposes.
There are indeed a couple of other simple tricks for detecting PID namespaces too, but they are not official API either. See for instance what the systemd
people have discussed also recently for their tools. Apparently they, too, had explored the thing about the proc
device number being "3 or 4", but dropped that idea because they noticed that it doesn't hold that much (and perhaps it ever held only in "sunny day" conditions however common they may be). They've also explored PID 2 being always [kthreadd]
and/or overall presence of kernel threads, which would be indisputable sign of initial PID namespace, but they dropped that idea too because mounting proc
with hidepid=[12]
would defy that check completely.
I would say that the fundamental problem with detecting namespaces is that they are inherently arbitrary, and can be totally superseded by other namespaces. For all namespace types the kernel does have the so-called "initial" namespaces, but the first PID 1 process (even the one in initramfs
) may choose to override them by simply unshare(2)
-ing all (or even just a few) of them before starting any other process. Clearly under such (not too) hypothetical condition, the quest for detecting the initial namespaces loses any useful sense, as it is rather the "host" namespaces that are relevant. Those are the namespaces where the bare OS (i.e. the main PID 1 init
process) operates in once bootstrapped, even though such "host" namespaces may already be child namespaces as far as the kernel is concerned. I'm not saying that init
processes really override initial namespaces all the time, but in principle they can, and that's enough to cripple any namespace-detection tool.
In my opinion the thing is also that, for most practical use cases, you aren't really interested in arbitrary namespaces. Almost certainly you are not interested at all in the UTS, IPC, cgroup, and time namespaces, and probably not even in the user and PID namespaces. If ever, you may be interested in just the mount and network namespaces because those are the relevant ones for access to data and to connectivity. The PID namespace is often being looked for a lot only because a PID namespace (much more than a user namespace) typically implies a container in its wider sense, and a "wider" container is simply what brings along the interesting mount and network namespaces. Unfortunately these latter are the trickiest to hunt down, which is probably the reason why detection tools prefer to rather look for PID namespaces in hope for a loosely-bound yet good relationship between that and the mount/network namespaces.
To sum it all up with all these "if"s and "but"s and caveats, the matter is whether the effort of trying to detect "initial or child" namespaces worths the while or not. I daresay that it usually doesn't, you're probably better off without detecting them at all, or perhaps with only detecting your narrowed-down definition of what constitutes a "host" namespace for your specific well-defined use cases.
HTH
1. Just read /proc/self/uid_map
file and see if it reports 0 0 4294967295
precisely
amicontained
container profiling tool.amicontained
, but that’s the only non-generic heuristic used (all other namespaces are checked by device number, which as I’ve demonstrated fails).amicontained
cannot check for arbitrary namespaces. At best it can only check whether the PID namespace is a child or not. In fact, AFAICT (not a Go expert I am) thatHasNamespace
function seems to be invoked only once and for thepid
namespace only. IOW it doesn't seem to be meant to detect arbitrary namespaces, it seems to be meant to infer a containerization just because containers typically bring along a dedicated PID namespace.58
is quite a lot different from4
. Are you sure you've executed thatdocker run [...] --pid host
command from the bare host OS, and not from an already-child namespace or containerized OS? weren't you by any chance using a docker-in-docker or perhaps LXC setup?docker run -ti --rm --pid host debian
on both macOS and Linux VM (Parallels) andstat --format="%d" /proc/self/ns/pid
results in 5 and 74, respectively. Why are device numbers of interest? Namespaces are not pseudo filesystems, are they? Indeed, cgroups are pseudo filesystems. If the device number strategy works (or worked) for the PID namespace, wouldn't it work for other namespaces too?