Definition in include/linux/fs_struct.h: include/linux/fs_struct.h:10:struct fs_struct { A bunch of includes of linux/fs_struct.h. A lot of them - 44 files pull it. Worse yet, there's one in include/linux/namei.h and _that_ brings fuckloads more. 211 hits of that, thankfully none under include/*, but that's an obscene amount of extra audit surface all the same. Encapsulation? Wassat? For the record, prior to the commit that added that pile there'd been only 30 includes, all of them in *.c drivers/crypto/ccp/sev-dev.c:28:#include fs/9p/acl.c:9:#include fs/btrfs/inode.c:12:#include fs/cachefiles/daemon.c:22:#include fs/coredump.c:37:#include fs/d_path.c:5:#include fs/exec.c:60:#include fs/exfat/super.c:16:#include fs/f2fs/acl.c:12:#include fs/fat/inode.c:25:#include fs/fhandle.c:9:#include fs/fs_struct.c:8:#include fs/hfsplus/options.c:15:#include fs/hpfs/super.c:12:#include fs/init.c:10:#include fs/kernel_read_file.c:3:#include fs/namei.c:37:#include fs/namespace.c:21:#include /* get_fs_root et.al. */ fs/nfsd/nfs4proc.c:35:#include fs/nfsd/nfssvc.c:13:#include fs/nilfs2/nilfs.h:17:#include fs/ntfs3/super.c:61:#include fs/ocfs2/acl.c:16:#include fs/omfs/inode.c:17:#include fs/open.c:31:#include fs/proc/array.c:92:#include fs/proc/base.c:88:#include fs/proc/task_nommu.c:6:#include fs/proc_namespace.c:13:#include fs/smb/client/file.c:12:#include fs/smb/client/inode.c:9:#include fs/smb/client/smb1ops.c:10:#include include/linux/namei.h:10:#include init/do_mounts.c:17:#include kernel/auditsc.c:58:#include kernel/exit.c:53:#include kernel/fork.c:81:#include kernel/nsproxy.c:22:#include kernel/sys.c:40:#include kernel/umh.c:16:#include kernel/user_namespace.c:21:#include net/unix/af_unix.c:88:#include security/apparmor/path.c:18:#include security/ipe/hooks.c:7:#include Remaining hits are of the form 'struct fs_struct'. Several groups: * forward declarations of struct fs_struct in include/linux/mnt_namespace.h:10:struct fs_struct; include/linux/nsproxy.h:14:struct fs_struct; include/linux/sched.h:65:struct fs_struct; * declaration and definition of init_fs: include/linux/init_task.h:26:extern struct fs_struct init_fs; fs/fs_struct.c:150:struct fs_struct init_fs = { * explicit sizeof (in kmem_cache_create arguments when creating fs_cachep): kernel/fork.c:3034: sizeof(struct fs_struct), 0, * pointers to that thing in two structures - task_struct and nsset (both opaque, both called 'fs'): include/linux/sched.h:1186: struct fs_struct *fs; include/linux/nsproxy.h:57: struct fs_struct *fs; * declaration and definition of copy_mnt_ns(), takes a pointer as argument: include/linux/mnt_namespace.h:17: struct user_namespace *, struct fs_struct *); fs/namespace.c:4094: struct user_namespace *user_ns, struct fs_struct *new_fs) * declaration and definition of unshare_nsproxy_namespaces(), ditto: include/linux/nsproxy.h:104: struct cred *, struct fs_struct *); kernel/nsproxy.c:212: struct nsproxy **new_nsp, struct cred *new_cred, struct fs_struct *new_fs) * several static functions, ditto: fs/d_path.c:239:static void get_fs_root_rcu(struct fs_struct *fs, struct path *root) fs/d_path.c:382:static void get_fs_root_and_pwd_rcu(struct fs_struct *fs, struct path *root, kernel/nsproxy.c:89: struct fs_struct *new_fs) # create_new_namespaces() * a static function taking a pointer to pointer to fs_struct as an argument: kernel/fork.c:3077:static int unshare_fs(unsigned long unshare_flags, struct fs_struct **new_fsp) * several local variables - all struct fs_struct *: fs/namei.c:1049: struct fs_struct *fs = current->fs; fs/namei.c:2670: struct fs_struct *fs = current->fs; fs/namespace.c:6221: struct fs_struct *fs = nsset->fs; fs/fs_struct.c:57: struct fs_struct *fs; fs/fs_struct.c:91: struct fs_struct *fs = tsk->fs; fs/fs_struct.c:108: struct fs_struct *fs = kmem_cache_alloc(fs_cachep, GFP_KERNEL); fs/fs_struct.c:128: struct fs_struct *fs = current->fs; fs/fs_struct.c:129: struct fs_struct *new_fs = copy_fs_struct(fs); kernel/fork.c:1594: struct fs_struct *fs = current->fs; kernel/fork.c:3079: struct fs_struct *fs = current->fs; kernel/fork.c:3123: struct fs_struct *fs, *new_fs = NULL; * declarations and definitions of several public functions taking a pointer to fs_struct as an argument: include/linux/fs_struct.h:21:extern void set_fs_root(struct fs_struct *, const struct path *); include/linux/fs_struct.h:22:extern void set_fs_pwd(struct fs_struct *, const struct path *); include/linux/fs_struct.h:24:extern void free_fs_struct(struct fs_struct *); include/linux/fs_struct.h:27:static inline void get_fs_root(struct fs_struct *fs, struct path *root) include/linux/fs_struct.h:35:static inline void get_fs_pwd(struct fs_struct *fs, struct path *pwd) fs/fs_struct.c:15:void set_fs_root(struct fs_struct *fs, const struct path *path) fs/fs_struct.c:32:void set_fs_pwd(struct fs_struct *fs, const struct path *path) fs/fs_struct.c:82:void free_fs_struct(struct fs_struct *fs) * declaration and definition of a function taking a pointer to fs_struct and returning a pointer to fs_struct: fs/fs_struct.c:106:struct fs_struct *copy_fs_struct(struct fs_struct *old) include/linux/fs_struct.h:23:extern struct fs_struct *copy_fs_struct(struct fs_struct *); That's it. If somebody is playing with token-pasting, that might be incomplete, but in that case all bets are off. Interesting games with typeof are also possible, but we can at least find those if they happen. Assuming for now that nothing's missing, we have * one static-duration instance (init_fs). * no auto duration instances. * no instances embedded into compound objects. * one function that returns a pointer to an instance (copy_fs_struct()). Presumably a constructor... * one function that might return a pointer to an instance via an in-out argument (unshare_fs()). Might be another constructor... * pointers to fs_struct in task_struct (->fs) and in nsset (->fs). * local variables and arguments declared as pointers to fs_struct. * no explicit casts to or from such pointers; of course, that does not cover implicit conversions to/from void *... * one explicit sizeof when creating an allocator (fs_cachep); presumably that (a) source dynamic instances are coming from. Of course, there might be sizeof(*task->fs) or something worse someplace... Plan: grep for the functions involved (as well as init_fs and fs_cachep) first, then look through the accesses to local variables, finally go over the places where we access task_struct->fs and nsset->fs. The last part is going to be far more painful - "fs" is not particularly rare as identifiers go. Again, if somebody is playing with ##, we are screwed - in the best case it's "instrument sparse to catch that kind of shit and try to get something approximating build coverage". Let's hope it doesn't come to that... Incidentally, things like #define wank(field) obscene expression containing something->field are also possible; again, let's hope it doesn't come to that - it's less awful, but... 1) init_fs: static-duration instance after trimming some false positives in nouveau (they have a method with that name in something - always the same value, by the look of it, but whatever) we are left with this: fs/fs_struct.c:150:struct fs_struct init_fs = { # definition fs/fs_struct.c:152: .seq = __SEQLOCK_UNLOCKED(init_fs.seq), # initializer of embedded seqlock include/linux/init_task.h:26:extern struct fs_struct init_fs; # declaration init/init_task.c:156: .fs = &init_fs, # the only user - initializer of init_task.fs OK, it's used only as task_struct->fs for the static-duration instance of task_struct. 2) fs_cachep: allocator fs/fs_struct.c:86: kmem_cache_free(fs_cachep, fs); # in free_fs_struct(); destructor? fs/fs_struct.c:108: struct fs_struct *fs = kmem_cache_alloc(fs_cachep, GFP_KERNEL); # in copy_fs_struct(); constructor? include/linux/fs_struct.h:18:extern struct kmem_cache *fs_cachep; # declaration kernel/fork.c:475:struct kmem_cache *fs_cachep; # definition kernel/fork.c:3033: fs_cachep = kmem_cache_create("fs_cache", # ... and creation of allocator itself. Incidentally, is there an reason to make it public at all? It's created in proc_caches_init(), along with a bunch of other allocators and that's the only reason for having it visible outside of fs/fs_struct.c; move over there, add fs_cache_init() and call it from proc_caches_init(), perhaps? What's the call chain of that thing, again? Aha, called from start_kernel(), very early in the game. Never called again and actually freed once we've booted (__init stuff). Might be worth considering... One thing we can't realistically do is use of something a-la initcall - that early in bootstrap the sequence is too rigid for that. So: presumptive constructor is copy_fs_struct() and presumptive destructor - free_fs_struct(), both available after proc_caches_init() has been done. 3) unshare_fs(): kernel/fork.c:3077:static int unshare_fs(unsigned long unshare_flags, struct fs_struct **new_fsp) kernel/fork.c:3162: err = unshare_fs(unshare_flags, &new_fs); one caller (ksys_unshare()), and yep, that's an in-out parameter. Uses of that variable: kernel/fork.c:3123: struct fs_struct *fs, *new_fs = NULL; kernel/fork.c:3162: err = unshare_fs(unshare_flags, &new_fs); kernel/fork.c:3172: new_cred, new_fs); # passed to unshare_nsproxy_namespaces() kernel/fork.c:3182: if (new_fs || new_fd || do_sysvsem || new_cred || new_nsproxy) { kernel/fork.c:3200: if (new_fs) { kernel/fork.c:3203: current->fs = new_fs; # stored to current->fs, under task_lock(current) and read_seqlock_excl() on # previous current->fs->seq kernel/fork.c:3205: new_fs = NULL; kernel/fork.c:3207: new_fs = fs; # set to previous current->fs, if its ->user had dropped to 0. kernel/fork.c:3233: if (new_fs) kernel/fork.c:3234: free_fs_struct(new_fs); # passed to free_fs_struct() No loops in that ksys_unshare(), all gotos are going forward, so we start with new_fs set to NULL, let unshare_fs() set it, then use the resulting value... On success we replace current->fs with it, on failure it gets freed. Old value of ->current->fs gets ->users decremented and, if it reaches 0, freed. unshare_fs() itself: a bunch of checks[*], with early exits returning 0 and leaving *new_fsp as-is (i.e. NULL). Then *new_fsp = copy_fs_struct(fs); if (!*new_fsp) return -ENOMEM; return 0; 'fs', incidentally, is initialized as current->fs and never changes. IOW, it returns 0 on success and -ENOMEM on failure; on success it might leave *new_fsp as-is or it might store copy_fs_struct(current->fs) in there. Might be better off with returning fs_struct * instead, with ERR_PTR(-ENOMEM) on error and NULL for "took an early exit", perhaps? if (err) goto bad_unshare_out; with bad_unshare_out: return err; so something like new_fs = unshare_fs(); if (IS_ERR(new_fs)) return PTR_ERR(new_fs); wouldn't be too awful... [*] That bunch of checks should've been simply "if not asked to unshare it or if it's already ours alone", but there's an amusing bogosity there - "or if current->fs is NULL". Had been there since unshare(2) had been added, had always been bogus, nobody noticed... Later. 4) copy_mnt_ns(). The only caller is kernel/nsproxy.c:98: new_nsp->mnt_ns = copy_mnt_ns(flags, tsk->nsproxy->mnt_ns, user_ns, new_fs); in create_new_namespaces(). Argument comes directly from arguments of create_new_namespaces(). Uses of argument: fs/namespace.c:4143: if (new_fs) { fs/namespace.c:4144: if (&p->mnt == new_fs->root.mnt) { fs/namespace.c:4145: new_fs->root.mnt = mntget(&q->mnt); fs/namespace.c:4148: if (&p->mnt == new_fs->pwd.mnt) { fs/namespace.c:4149: new_fs->pwd.mnt = mntget(&q->mnt); What happens here is that we iterate through the copied mount tree and if new_fs had ->root.mnt or ->pwd.mnt pointing to some mount in the original, flip that to the corresponding mount in the copy. We get to that loop only if there's CLONE_NEWNS in flags argument IOW, we have read-modify-write on new_fs->{pwd,root}.mnt if CLONE_NEWNS is present in flags. Check for new_fs being non-NULL is interesting; can we ever get called with new_fs == NULL? That, of course, boils down to the same question about create_new_namespaces() and AFAICS it's been bogus all along... Again, later. 5) unshare_nsproxy_namespaces() The only caller is ksys_unshare(); the argument it gets there is either NULL (if no CLONE_FILES in unshare_flags at the time of unshare_fs() there) or result of copy_fs_struct() of current->fs. NOTE: if (unshare_flags & CLONE_NEWNS) unshare_flags |= CLONE_FS; prior to the call of unshare_fs() means that this condition is actually "neither CLONE_FS nor CLONE_NEWNS in unshare_flags value passed to ksys_unshare()". In the function itself the argument is used in the call of create_new_namespace(): kernel/nsproxy.c:227: new_fs ? new_fs : current->fs); if non-NULL (i.e. if it's a result of copy_fs_struct(current->fs)) it gets passed as-is, if NULL the value of current->fs is passed instead. Question: would it make sense to shift that check into caller? Not obvious - in a lot of cases it's not used at all (we only reach the call of copy_namespaces() if one of the "namespace" flags (CLONE_NEW{NS,UTS,IPC,NEW,PID,CGROUP,TIME}) is passed and there's not much sense shifting the calculation to caller. OTOH, it's not a hot path, so... 6) get_fs_root_rcu() The only caller is d_path(), argument is always current->fs. In the function itself we copy fs->root under read_seqbegin/read_seqretry loop; rcu_read_lock() is held over that and subsequent uses of the copied fs->root by the caller. 7) get_fs_root_and_pwd_rcu() Ditto, in getcwd(2), except that we copy both current->fs->root and current->fs->pwd. 8) create_new_namespaces() The only thing we do to the new_fs is passing it to copy_mnt_ns() as-is: kernel/nsproxy.c:98: new_nsp->mnt_ns = copy_mnt_ns(flags, tsk->nsproxy->mnt_ns, user_ns, new_fs); copy_mnt_ns() ignores it unless flags contains CLONE_NEWNS; flags is also passed as-is, so we are only interested in calls that have it in flags. There are 4 callers: 8.1) copy_namespaces() new_ns = create_new_namespaces(flags, tsk, user_ns, tsk->fs); flags and tsk come from the arguments of copy_namespaces(). That gets called by copy_process(), after (successful) call of copy_fs(): kernel/fork.c:2224: retval = copy_namespaces(clone_flags, p); tsk (the second argument, i.e. caller's "p") is the task_struct of the child we are trying to create. By that point it isn't reachable from anything other than caller's stack frame - it isn't running, not present in any shared data structures, etc. The value of tsk->fs had been initially set when the child's task_struct had been allocated by dup_task_struct(), in arch_dup_task_struct(). As one could guess by the name, that's arch-specific; default is plain assignment of task_struct. All architectures that differ from default might surround that by extra stuff that doesn't touch task_struct.fs, or, at least copy a subset that include task_struct.fs. Either way, it will be equal to current->fs. Depending upon the flags, we either leave the value as-is (if CLONE_FS is set) or replace it with the result of copy_fs_struct() of the previous value (current->fs, that is). If copy_fs_struct() returns 0, we bail out without getting to copy_namespaces(). In the very beginning of copy_process() we verify that CLONE_FS does not come with CLONE_NEWNS, so either copy_namespaces() will ignore its struct fs_struct * argument due to the lack of CLONE_NEWNS or it will be passed the result of copy_fs_struct(current->fs) we store in tsk->fs. 8.2) in unshare_nsproxy_namespaces() kernel/nsproxy.c:226: *new_nsp = create_new_namespaces(unshare_flags, current, user_ns, See above re that function; in the case we are interested it (CLONE_NEWNS in unshare_flags) we will get the (non-NULL) result of copy_fs_struct(current->fs) left in new_fs [*]. 8.3) in exec_task_namespaces(): kernel/nsproxy.c:284: new = create_new_namespaces(0, tsk, current_user_ns(), tsk->fs); No CLONE_NEWNS in flags, the last argument is ignored. 8.4) in prepare_nsset(): kernel/nsproxy.c:352: nsset->nsproxy = create_new_namespaces(0, me, current_user_ns(), me->fs); Ditto. [*] modulo the impossible case of current->fs == NULL at the time of unshare(2); again, later.