7

I have researched the subject and found the following code on Github, which says:

// HasNamespace determines if a container is using a particular namespace or the
// host namespace.
// The device number of an unnamespaced /proc/1/ns/{ns} is 4 and anything else is
// higher.
// Only works from inside a container.

https://github.com/genuinetools/amicontained/blob/568b0d35e60cb2bfc228ecade8b0ba62c49a906a/vendor/github.com/jessfraz/bpfd/proc/proc.go#L461

However, that comment is obsolete, which is demonstrated by:

$ docker run -ti --rm --pid host debian
root@e29ab2d7176b:/# stat --format="%d" /proc/self/ns/net
58
root@e29ab2d7176b:/# stat --format="%d" /proc/self/ns/pid
58

Here stat --format="%d" /proc/self/ns/pid should result in 4, if that comment was correct.

How can a process detect whether it's in a child namespace?

13
  • @A.B - thanks. Yes, I know there are several kinds of namespaces. That’s why I didn’t mention a specific namespace type in the question. If you don’t know a general way to detect a child namespace for all namespace types, will you then show a heuristic for a single namespace type? I don’t have a specific use case, except patching the amicontained container profiling tool.
    – Shuzheng
    Commented Apr 4, 2021 at 17:52
  • @A.B - thanks. How do you check the first heuristic for PID 2 (and why is PID 2 of special interest)? Will you show commands? Your user namespace heuristic is currently used by amicontained, but that’s the only non-generic heuristic used (all other namespaces are checked by device number, which as I’ve demonstrated fails).
    – Shuzheng
    Commented Apr 5, 2021 at 6:58
  • That check based on "device" number 4 done by 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) that HasNamespace function seems to be invoked only once and for the pid 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.
    – LL3
    Commented Apr 7, 2021 at 0:25
  • Besides, device numbers for pseudo filesystems can (and do) change but 58 is quite a lot different from 4. Are you sure you've executed that docker 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?
    – LL3
    Commented Apr 7, 2021 at 0:27
  • @LL3 - thanks. Yes, I executed docker run -ti --rm --pid host debian on both macOS and Linux VM (Parallels) and stat --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?
    – Shuzheng
    Commented Apr 7, 2021 at 7:53

1 Answer 1

8

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

10
  • When you say namespace ID, you mean the inode number as given by e.g. ls -li /proc/self/ns/pid? So, namespaces are just files, having symbolic links referencing them from /proc, not residing on a file system, but only in the kernel (in a pseudo file system)?
    – Shuzheng
    Commented Apr 9, 2021 at 12:58
  • Why are mount and network namespaces not created by default on boot, i.e. considered boot namespaces? Is it because, Linux may function without the mount and network namespace, and so they are only created on-demand?
    – Shuzheng
    Commented Apr 9, 2021 at 13:02
  • Thank you for this detailed answer, and for sharing your ideas and experiments. I guess, I will use the boot namespaces as a heuristic, besides that I see that other heuristic are extremely fragile and like to break.
    – Shuzheng
    Commented Apr 9, 2021 at 13:45
  • A namespace ID is its inode number as given by ls -Lli /proc/self/ns/pid (note the additional L option), and also by stat -Lc %i /proc/self/ns/pid, and also by readlink /proc/self/ns/pid or simply by ls -l /proc/self/ns/pid (in these latter two commands it is the number reported in brackets). As for namespaces being "just files residing only in a kernel pseudo-fs".. it's a good point but I wouldn't push it to that extent. It's just that they get their IDs from the same pool of values also used for the inode numbers of the proc pseudo-fs. There ends the analogy with being files.
    – LL3
    Commented Apr 9, 2021 at 13:47
  • As for the motives of making mount/network namespaces dynamic I wouldn't know, but your own suggestion may very well be a good reason and I would bet on that. Note however that "boot" namespaces is just my own term to refer to those namespaces collectively.
    – LL3
    Commented Apr 9, 2021 at 13:48

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .