|
|
Log in / Subscribe / Register

How implementation details become ABI: a case study

By Jonathan Corbet
October 1, 2014
One of the final changes that went into the mainline kernel repository before the 3.17-rc7 release was this fix from Mikhail Efremov. It affects some low-level code within the virtual filesystem layer that manages name changes in the dentry structure — the structure that handles the mapping between file names and in-kernel inode structures. How that change came to be necessary makes a good lesson in how unintended behaviors can become part of the kernel's ABI over time.

The problem

The addition of the renameat2() system call in the 3.15 development cycle brought with it a subtle, unintended change in behavior that is best illustrated by example. On a system running (a hopefully updated version of) Bash, one can type a sequence like the following:

    $ cd /tmp
    $ touch foo bar
    $ exec 42<bar
    $ ls -l /proc/self/fd/42
    lr-x------ 1 corbet lwn 64 Sep 29 13:01 /proc/self/fd/42 -> /tmp/bar

The exec command causes the shell to open bar as file descriptor 42. The output of the ls shows that, indeed, bar is open on that file descriptor. What should happen, though, after the following sequence of commands?

    $ mv foo bar
    $ ls -l /proc/self/fd/42

On a kernel prior to 3.15, the output will look like this:

    lr-x------ 1 corbet lwn 64 Sep 29 13:01 /proc/self/fd/42 -> /tmp/bar (deleted)

On later kernels, instead, the result is:

    lr-x------ 1 corbet lwn 64 Sep 29 15:00 /proc/self/fd/42 -> /tmp/foo (deleted)

When a file with open descriptors is deleted, the actual file remains in existence until all of those file descriptors are closed. On kernels prior to 3.15, the name associated with that deleted file will be the name it had when it was deleted. In newer kernels, instead, a file may, if it is deleted via a rename operation, end up appearing to have the name of the file that was renamed on top of it.

This change may appear to be nearly irrelevant; who is going to care about the apparent name of a deleted file that is no longer accessible via the filesystem? But it seems that there are scripts out there that do care. One case was outlined by Mikhail in his patch posting: the package update utility on ALT Linux will replace (via a rename) the executable for a running daemon process, then try to find existing processes that are running the older version of the executable. But renaming the new version of the executable on top of the previous one causes any process running the old version to appear to be running something else, so the upgrade process fails. Piotr Karbowski, who appears to have been the first to report the bug, stated that it made his system unusable. This behavior change did, in fact, cause systems to break.

The cause

Understanding this bug requires delving into struct dentry and a somewhat obscure function called switch_names() that handles rename operations. The dentry structure, since it is charged with name mapping, must contain the file name of interest. But that name can be stored in two different ways. If the length of the name is less than DNAME_INLINE_LEN (a value between 32 and 40, chosen for optimal structure alignment), that name will be stored within the dentry structure itself. Otherwise, the d_name field will contain a pointer to an externally-allocated string.

The switch_names() function is defined like this:

    void switch_names(struct dentry *dentry, struct dentry *target);

Its job is to cause dentry to have the name currently associated with target. When moving names around, switch_names() must clearly pay attention to whether internal or external names are being used. Since there are two dentry structures to work with, there are four possible combinations. If both names are allocated externally, life is easy:

    swap(target->d_name.name, dentry->d_name.name);

One might wonder why the two names are exchanged in this way, since the stated purpose is only to affect dentry. The swap is done because target is about to disappear anyway, so its "name" is not really seen to matter anymore. Swapping allows this code to (1) avoid memory allocations, which, given how deep it is running within the VFS layer, is useful, and (2) not worry about freeing the old name associated with dentry, since that will now happen when target is freed anyway.

If, instead, both names are internal, memory allocations are not a concern. Prior to 3.15, the code for that case looked like this:

    memcpy(dentry->d_iname, target->d_name.name, target->d_name.len + 1);
    dentry->d_name.len = target->d_name.len;

This operation would leave both dentry structures appearing to have the same name — different behavior than that seen with the external-names case. Again, since target is expected to be destroyed soon, that difference should not really matter. It did start to matter, though, when the cross-rename feature (allowing the names of two files to be atomically swapped) was added in 3.15. In that case, the two names should be switched, as was done in the external case. So, in current kernels, the code looks like this:

    unsigned int i;

    for (i = 0; i < DNAME_INLINE_LEN / sizeof(long); i++) {
	swap(((long *) &dentry->d_iname)[i], ((long *) &target->d_iname)[i]);
    }

This funny-looking loop allows the swapping of the two names without the need for temporary variables or extra copying. (For completeness, the mixed internal/external cases swap the names).

As far as everybody could tell, the above code was correct; it made the internal-name case behave like the external-name case did. But if (1) one file is renamed on top of another, and (2) both files have short names, the user-visible behavior of the system changes, and that change caused programs to break. Breaking things in that way goes against one of the fundamental rules of kernel development, so some sort of fix needs to be put into place.

The fix

Mikhail's original patch added a flag ("exchange") to switch_names(). If that flag is set (as would be the case for an atomic file swap operation), the 3.15 behavior holds; otherwise, the code would revert back to the previous behavior for the both-internal case (names would still be swapped in the other cases). This patch was initially rejected; Linus called it "too ugly to live":

Yes, we had that hack before, but we didn't make it conditional. It historically was more of a "it's easier to just memcpy the name" than switch things around. Then that became accidental semantics, and that's all normal. But then when we make this explicit and intentional, I really think we should do it *right*, and either switch() the names around or just copy it.

Having a "switch_names()" function that *neither* switches *nor* copies, and giving it an argument to decide which, but not even do it *right*? That's just too f*cking disgusting for words.

A proper solution would, thus, cause the "just copy the name to the new dentry" behavior to happen on rename operations in all cases where an explicit swap has not been requested, even those which were not handled that way in the past. Implementing that behavior runs into a problem, though: in the case where both names are external, it may be necessary to allocate memory for a copy of the target file name. Such an allocation would have to be handled in atomic context and would slow down code that needs to run quickly. So a simple solution is not readily available.

Thus, the developers decided to merge a version of Mikhail's patch for 3.17, even if they don't like it. The patch has changed a bit since Al Viro took the opportunity to clean up the surrounding code a bit. But that code will probably not last beyond the 3.17 release.

What is likely to happen, instead, is a variant of this patch from Al. It adds a reference count to external names, allowing those names to be "copied" by just incrementing the count. Actually freeing the name must be done conditionally based on the results of a decrement-and-test operation. There are some additional complications; the name may be accessed under read-copy-update (RCU) rules, for example, so the actual freeing must happen in an RCU callback. But the idea is simple enough and, since few places actually manipulate the names in dentry structures, the implementation is relatively small.

Still, that is a larger change than anybody would like to see go into 3.17 at this point in the development cycle. So reference-counted external names in dentry structures will have to wait until 3.18. Meanwhile, Mikhail's fix has gone in for 3.17 and been marked for the stable updates, so the old behavior will return in the near future. This behavior was accidental and never documented, and the kernel developers seemingly believe that any code relying on it was poorly written to begin with. But, all of that notwithstanding, that behavior has become a part of the kernel ABI, so those developers will preserve it even if they don't like it.

Index entries for this article
KernelDevelopment model/User-space ABI
Kernelrenameat2()


to post comments

How implementation details become ABI: a case study

Posted Oct 1, 2014 15:32 UTC (Wed) by PaXTeam (guest, #24616) [Link] (4 responses)

Piotr originally reported the problem on the grsec forums:
https://forums.grsecurity.net/viewtopic.php?f=3&t=4031 and once identified, spender fixed it 3 weeks ago already.

How implementation details become ABI: a case study

Posted Oct 1, 2014 16:17 UTC (Wed) by lkundrak (subscriber, #43452) [Link] (3 responses)

How is that relevant?

How implementation details become ABI: a case study

Posted Oct 1, 2014 17:02 UTC (Wed) by doogie (guest, #2445) [Link] (2 responses)

Did you read the messages on that forum? It's the same bug.

How implementation details become ABI: a case study

Posted Oct 9, 2014 5:32 UTC (Thu) by andza (guest, #72692) [Link] (1 responses)

Was it reported upstream and just as important, was the patch submitted to LKML? Otherwise I can only agree with the parent that it's mostly irrelevant.

How implementation details become ABI: a case study

Posted Oct 9, 2014 10:32 UTC (Thu) by PaXTeam (guest, #24616) [Link]

> Was it reported upstream

did you read the forum post?

> and just as important, was the patch submitted to LKML?

no because it was a quick hack, not something upstream (or even we) would want in the long run. in fact they've fixed it differently in the end (not their hack but Al's refcount approach) as you can read it in this very article.

How implementation details become ABI: a case study

Posted Oct 1, 2014 16:53 UTC (Wed) by BenHutchings (subscriber, #37955) [Link] (1 responses)

If it was accidental that the old name of a deleted inode was readable, then why did procfs specifically support readlink() on them? That case was very clearly intended to work at one point, even if it ended up broken for longer names.

How implementation details become ABI: a case study

Posted Oct 2, 2014 3:24 UTC (Thu) by flewellyn (subscriber, #5047) [Link]

I agree. To me, it makes sense that the /proc/self/fd/* symlinks, as well as /proc/$NUM/fd/*, would keep symlinks to the old name of a deleted file, if the descriptor is still open. Standard Unix semantics are that an unlinked file is not actually gone until the last FD pointing to it is closed, so it makes sense that any processes which still have it open should still have an idea of what it was called before deletion.

How implementation details become ABI: a case study

Posted Oct 1, 2014 21:34 UTC (Wed) by zblaxell (subscriber, #26385) [Link] (5 responses)

Does this mean there are now (or always were) subtle security bugs on ALT Linux that can be triggered by a few rename calls? What happens on those systems if I run a binary named "sshd (deleted)"? If you open /proc/$foo/fd/$bar, does it give you a copy of the existing open file descriptor, or is there a race condition attack because it's literally following the symlink?

All of this is head-shakingly ugly to me. If anything, this incident looks like a golden opportunity to flush out people building on top of accidental ABI and get them talking to kernel devs about what interfaces they really need to do what they're trying to do (judging from a quick read of the bug reports, cgroups would make more sense for these use cases).

How implementation details become ABI: a case study

Posted Oct 2, 2014 7:41 UTC (Thu) by quotemstr (subscriber, #45331) [Link] (3 responses)

I had the same thought. The addition of "deleted" to the filename is very ugly, as in-band signaling tends to be. Why not note the deleted status in fdinfo instead?

How implementation details become ABI: a case study

Posted Oct 3, 2014 7:03 UTC (Fri) by viro (subscriber, #7872) [Link] (2 responses)

Because that was years before /proc/*/fdinfo/*. Again, those symlinks had never been followed by traversing *any* string as a pathname. And they still do not.

A bit more history: the original inspiration for that thing had been, as far as I can tell, either late Research Unix or Plan 9 and there it had been something different. For one thing, no symlinks in there - as in "no such thing as a symlink". For Plan 9 counterpart see
http://fxr.watson.org/fxr/source/port/devdup.c?v=PLAN9
It's a single directory with a pair of entries for each descriptor in accessing process' descriptor table - as in 0, 0ctl, 1, 1ctl, etc.
opening 42 in there is equivalent to dup(42); opening 42ctl (can be done only for read) gives you a bunch of stats on file - basically, something similar to combination of our self/fdinfo/42 and readlink of self/fd/42.

Note that it only covers what would, for us, been /dev/fd. I.e. only your own descriptor table. Moreover, unlike us they do, indeed, give the *same* opened struct file (Chan in their terms). As dup(2) would. We do not - we get a new open of the fs object behind the old open file. Among other things, it means different behaviour of lseek() - with dup() the current position is shared between old and new descriptors, with new open it's independent. They go for dup-like, we - for open-like.

In part it's a historical accident, in part - the result of different API for ->open(), in part - decision to make them look like symlinks and use readlink() to indicate which file it is.

Another thing to keep in mind is that in Plan 9 it was *NOT* a part of procfs. Their procfs has <pid>/fd, but unlike ours it's a regular file, not a directory. With line per descriptor. So the needs of lsof-like stuff are covered by that. In our variant it was first a per-process directory, full of magic symlinks and then (many years later) - two directories; fd/* and fdinfo/*. The latter - with regular files in it, more or less similar to contents of dupfs <n>ctl or lines in procfs <pid>/fd.

Note that back when it started we *couldn't* report the path. It simply hadn't been available. It's older than dcache by several years (0.99 vs. 2.1.40ish, resp.). So we had pseudo-symlinks resolving to (back then) inodes behind the open descriptors, with readlink(2) providing information for lsof and friends. Basically, st_dev + st_ino of the same inodes.
Following those "symlinks" had never depended on the resulting string, or, indeed, the numbers themselves.

In 2.1.43 or so we got dentries (and after a while they even started to work), and with that we got a way to calculate those pathnames. Cheaply. As the result, those pseudo-symlinks (back then used only for lsof, fuser and their ilk) suddenly acquired a much prettier readlink(2) output. Which required a few utilities updated between 2.0 and 2.2. And it was a very specialized API, with very few users. Moreover, said users were already full of ifdefs - e.g. on *BSD those *had* to be suid-root; nosing in the kernel data structures requires that. And yes, it was that scary.

That had all the makings of bad API - few users and lack of anything in that area that would be even remotely portable on other Unices. Indeed, comparison with peeking into kernel data structures from userland made for a _very_ low plank.

Shortly after that somebody noticed that there had been no way to tell an unlinked file from something created in its place later. Result: appending " (deleted)" in the end of readlink(2) output. Kludgy, of course, but with so few (and specialized) users... Again, the string reported by readlink(2) has nothing whatsoever to resolving the sucker.
Moreover, any user *must* look at the string it gets; think what should be returned for pipes and sockets.

Note, BTW, that back then *all* dentry names had been external ones and in all cases names were swapped on d_move(). In 2.1.116 it had been changed - short names (majority of them, obviously) got embedded into dentry itself, giving better locality and lower cache footprint in dcache lookups. At that point short names started to be copied.

Nobody really considered "which unlinked file had this been" as even remotely sane question; indeed, how the hell can one tell how many files with that name had been created, opened and unlinked? OK, you know that it used to be /tmp/foo and got unlinked since then; which one of them?

Alas, there's no API that won't be abused. Nevermind that it wasn't consistent, nevermind that results were actually useful for detecting anything reliably, it worked well enough for hell knows how many scripts.
Until they broke...

In theory, we can't even make it consistent, lest some whiny wonder comes with a script that relied on "swap if either name is long" semantics - the filenames that got renamed being known to be long enough. And relying on mv /tmp/<name1> /tmp/<name2> leaving you with /tmp/<name1> (deleted) coming from readlink(2). I'll believe it when I see it; IMO it's extremely unlikely, though...

How implementation details become ABI: a case study

Posted Oct 9, 2014 22:39 UTC (Thu) by weue (guest, #96562) [Link] (1 responses)

Among others, "//deleted/PATH" (note the double slash) or "deleted/PATH" (relative path) would have been unambiguous.

Whoever accepted "X (deleted)" is an idiot.

How implementation details become ABI: a case study

Posted Oct 9, 2014 22:53 UTC (Thu) by viro (subscriber, #7872) [Link]

Whoever it had been, the whole point is that this ship has long sailed. Perhaps the main lesson is that single-use APIs tend to suck. "It's just for fuser and lsof" should've been a major red flag. For _those_ this (deleted) thing was probably more convenient - no need to do anything with the string on the userland side. And it wouldn't be a problem, except that there's no miracles and it *had* gained other users. Worse, now we can't change it without breaking existing userland, as this story has demonstrated.

How implementation details become ABI: a case study

Posted Oct 3, 2014 2:19 UTC (Fri) by viro (subscriber, #7872) [Link]

Following those suckers gives you vfsmount/dentry of deleted object. They are *not* followed by interpreting the result of ->readlink() (that's why ->follow_link() is an independent method). What you get opening those is *not* dup()-style extra reference to opened file; it's independently opened file over the same filesystem object (IOW, lseek() on one of those doesn't affect another). Filesystem might refuse opening it, of course; local filesystems usually do not, with network ones it's up to server. For sockets it explicitly fails; for pipes and FIFOs you get the same semantics you'd get from extra opener of named pipe.

Note that it's more than just /proc/*/fd/* - e.g. /proc/*/exe and /proc/*/cwd are also like that.

" (deleted)" is a kludge, and a damn unpleasant one. But as this story shows, it's not something we can kill ;-/ It's before my involvement, but I suspect that original motivation was lsof/fuser. And behaviour for rename(2) victim was an accident - check the history circa 2.1.40s; back when this " (deleted)" thing had been introduced *all* names had been external and d_move() swapped them in all cases. It was more about the unlinked ones... memcpy()-instead-of-swap had been introduced in 2.1.116 and it was more of "we need to put something there; just memcpy() it over".

No, it really had been an accident. BTW, prior to 2.1.43 those magic symlinks had the same semantics on follow_link, but gave "[%04x]:%u"
with st_dev and st_ino on readlink(). Next came an attempt to put something that usually gave pathname corresponding to the fs object in question, shortly followed by " (deleted)" tacked on the end of unlinked ones.

How implementation details become ABI: a case study

Posted Oct 2, 2014 15:47 UTC (Thu) by tomgj (guest, #50537) [Link] (2 responses)

Another sad case of "hack something together, and whatever ends up being exposed gets called the 'interface'". This as opposed to the "actually write down what the interface is" school.

Tellingly, the article does not refer to any interface specifications for the interface elements that are relevant in this "user-visible behaviour". This is not a criticism of the article, but rather of the development process it describes: one where the philosophy of specified interfaces is not understood, and where no proper distinction is made between an actual interface, and a piece of implementation that happens to be visible.

The test should be whether the interface consumer was depending on incidental behaviour, rather than specified behaviour, of the interface. However, since there is not in Linux a culture of adequately specifying interfaces in the first place, we are not in a position where this test could be applied.

It is interesting how bad Linux is at this. The old, proprietary Unixes came with manuals properly describing the interface specifications of each system, even if they were not always compatible with one another. Windows does better than Linux on this front, as do the BSDs.

It's a good job the original API design approach of Linux was to implement a more or less coherent API that had already been specified (POSIX). It's unfortunate this hasn't carried through into a culture of properly writing down specifications for new interface elements as (or rather, before) they're introduced.

How implementation details become ABI: a case study

Posted Oct 3, 2014 1:28 UTC (Fri) by giraffedata (guest, #1954) [Link]

How would it have made a difference if the interface were written down? Surely they would have written down that this unusual symbolic link value should give a name the file had when it had one, so this change would still be a bug and have to be fixed.

I guess I'm missing the article's thesis, that this is a case of "unintended behaviors can become part of the kernel's ABI over time." It looks to me like the designers deliberately put the former name of the file in the symbolic link value; they didn't mean to put arbitrary garbage in there, that just happened by accident of implementation to be the former name of the file.

How implementation details become ABI: a case study

Posted Oct 9, 2014 12:53 UTC (Thu) by kevinm (guest, #69913) [Link]

It wouldn't matter if the original interface had been specified ("The value returned by readlink(2) is for presentational purposes only and should not be parsed.") or not: people would still have ignored it and written scripts depending on the implementation details, and those scripts would still have to be supported.

The Linux culture is not to say "that userspace depended on something we didn't intend to be ABI, so it's OK to break it". ABI is de facto, not de jure.

How implementation details become ABI: a case study

Posted Oct 4, 2014 1:16 UTC (Sat) by alonz (subscriber, #815) [Link]

I wonder – is the extra effort in Al's new approach (= reference-counting the names) justified for any reason except solving this corner-case? (It does look like a pretty big hammer, which will add more complexity…)

How implementation details become ABI: a case study

Posted Oct 5, 2014 2:20 UTC (Sun) by magcius (guest, #85280) [Link] (8 responses)

Wait, if the behavior in old kernels changed based on how long the filename was, doesn't that mean ALT Linux would have just broken if I called my daemon something else or put it somewhere in /opt/com.foo.MyVendor/v6/ instead?

This interface even changes depending on the architecture and kernel build options.

How implementation details become ABI: a case study

Posted Oct 5, 2014 3:14 UTC (Sun) by viro (subscriber, #7872) [Link] (7 responses)

Yes, it would (not that they were in any real danger of stepping into that one - seriously, 32-character filename for a daemon binary? And that would not include the pathname - just the last component that long).

But yes, that's one of the reasons why their use of that trick had been bogus.

How implementation details become ABI: a case study

Posted Oct 5, 2014 3:26 UTC (Sun) by Cyberax (✭ supporter ✭, #52523) [Link] (4 responses)

One of our clients used to have a "paracetomoxyfrysebendroneomycind" daemon. Doing some drug discovery calculations, appropriately.

How implementation details become ABI: a case study

Posted Oct 5, 2014 4:47 UTC (Sun) by viro (subscriber, #7872) [Link] (2 responses)

*snort*

OK, I stand corrected - their script definitely breaks on that one... for 64bit host. Anyway, I think it's bloody obvious that what they are doing is an awful kludge.

Note that keeping the daemon running through the update is a very dubious approach; sure, the binary itself will stick around until we exit, but the rest of files in the package will get replaced, so if that daemon ever rereads them (e.g. in response to incoming request of some sort, or being asked to stop), the old binary will find new config/data/helper binaries/whatnot, with potentially spectacular results. Much safer to stop the sucker before replacing any files and restart it afterwards. And anything that does something more fancy (e.g. re-exec itself and transfer the internal state across that in one way or another) has no business using start-stop-daemon anyway.

IOW, the entire "replace files, then stop the old processes" is a bad idea. And prior to replacements there's no need whatsoever for that kind of kludges. Mind you, I'd rather do stat() on the binary we are after, then looked for /proc/*/exe with stat() giving the matching st_dev/st_ino. Without bothering with readlink() on those guys. Has an extra benefit of doing the right thing when you have multiple links to the same binary, with different processes using different names...

What they were doing is awful for a lot of reasons; sadly, that's not the criterion used in such situation ;-/ It worked for a long time, there's real-world userland code relying on it, so we get to keep it working. It's not quite the same as bug-for-bug compatibility - if nothing breaks when we fix inconsistent behaviour, we can go for it even if it was possible to write something that would break. Ditto if the code being broken is a rootkit or rootkit equivalent (i.e. relies on exploiting a security hole, by accident or not). But "the code we broke would've broken in a lot of other cases anyway" isn't an acceptable excuse.

How implementation details become ABI: a case study

Posted Oct 5, 2014 11:10 UTC (Sun) by JGR (subscriber, #93631) [Link] (1 responses)

Presumably a daemon designed to be upgraded whilst running in this way would load configuration and other files at startup or on HUP specifically to avoid these issues.

Stopping the daemon, upgrading, then restarting it introduces a small period of downtime, which can be undesirable.

How implementation details become ABI: a case study

Posted Oct 5, 2014 16:08 UTC (Sun) by viro (subscriber, #7872) [Link]

As I said, something more fancy shouldn't be using start-stop-daemon in the first place. I really wonder how many common daemons take care about the races around the upgrade; stop/move new one in place/start is more robust...
It's not just config; anything from helper binaries to permissions on directories, etc. can become a surprise for old daemon binary.

How implementation details become ABI: a case study

Posted Oct 12, 2014 8:11 UTC (Sun) by jzbiciak (guest, #5246) [Link]

Ok, I have nothing witty or insightful to add. But I have to say that's the most awesome executable name I've seen in a long time. Bravo!

How implementation details become ABI: a case study

Posted Oct 5, 2014 3:46 UTC (Sun) by magcius (guest, #85280) [Link] (1 responses)

Ah, from the "/tmp/foo (deleted)" string, I thought it was the full file path stored.

How implementation details become ABI: a case study

Posted Oct 5, 2014 5:05 UTC (Sun) by viro (subscriber, #7872) [Link]

Nope - in that case there are 3 or 4 dentries involved (depending on whether /tmp is a mountpoint). There's root directory of root filesystem. Name's empty, parent - itself. There's /tmp on root filesystem. Name being "tmp", parent - root of root filesystem. There's (possibly) root directory of whatever is mounted on /tmp. Again, name is empty, parent - itself. And there's /tmp/foo - name is "foo", parent - /tmp on root fs or root of filesystem mounted on /tmp. And it's unhashed since that rename.

We start with that dentry and vfsmount of either root fs or that mounted on /tmp and walk towards root. When we reach the root of current vfsmount we just proceed to dentry and vfsmount of mountpoint and continue from there. When we reach the process' root (sensu chroot(2)) or global root (vfsmount that isn't mounted on anything) we stop.

In this case the names we see along the way are "foo" and "tmp", and or dentry is unhashed, so d_path() produces "/tmp/foo (deleted)". And that's what readlink() on these guys returns. See fs/dcache.c:d_path() and the stuff next to it.

The name of dentry is just a single pathname component. That "/tmp/foo (deleted)" isn't stored anywhere - it's calculated on demand.


Copyright © 2014, Eklektix, Inc.
This article may be redistributed under the terms of the Creative Commons CC BY-SA 4.0 license
Comments and public postings are copyrighted by their creators.
Linux is a registered trademark of Linus Torvalds