* [RFC] ->d_name accesses @ 2024-02-07 22:22 Al Viro 2024-02-07 22:55 ` Matthew Wilcox 0 siblings, 1 reply; 7+ messages in thread From: Al Viro @ 2024-02-07 22:22 UTC (permalink / raw) To: linux-fsdevel; +Cc: Linus Torvalds, Nathan Chancellor, Nick Desaulniers There are very few places that legitimitely can do stores to dentry->d_name. Basically, it's dentry creation and d_move() guts. Unforutunately, there is a _lot_ of places where we access ->d_name - every ->lookup(), ->create(), ->unlink(), etc. instances has to do just that - that's how the directory entry name is passed to those. Verifying that nobody is playing silly buggers with ->d_name contents take, IME, several hours of code audit. And that has to be repeated every cycle. Compiler is no help there - it's grep over the tree, exclude the irrelevant hits (struct dirent has a field with that name, so does e.g. UFS directory entry, etc.) and looking through the 900-odd places that remain after that. Not fun, to put it politely. One way to do that would be to replace d_name with union { const struct qstr d_name; struct qstr __d_name; }; and let the the places that want to modify it use __d_name. Tempting, but the thing that makes me rather nervous about this approach is that such games with unions are straying into the nasal demon country (C99 6.7.3[5]), inviting the optimizers to play. Matt Wilcox pointed out that mainline already has i_nlink/__i_nlink pair, but... there are fewer users of those and the damage from miscompiles would be less sensitive. Patch along those lines would be pretty simple, though. There's an alternative way to get rid of that headache that ought to be safer: * add an inlined helper, static inline const struct qstr *d_name(const struct dentry *d) { return &d->d_name; } * gradually convert the accesses to ->d_name to calls of that helper - e.g. ret = udf_fiiter_find_entry(dir, &dentry->d_name, &iter); becoming ret = udf_fiiter_find_entry(dir, d_name(dentry), &iter); while err = msdos_find(dir, dentry->d_name.name, dentry->d_name.len, &sinfo); becomes either err = msdos_find(dir, d_name(dentry)->name, d_name(dentry)->len, &sinfo); or, if msdos_find() gets converted to passing const struct qstr * instead of passing name and length as separate arguments, err = msdos_find(dir, d_name(dentry), &sinfo); I am *NOT* suggesting doing that step in a single conversion - that would cause too many conflicts, wouldn't be easy to automate and there's a considerable pile of places that would be better off with conversion like above and those should be decided on case-by-case basis. However, it's not hard to do as a patch series, with very few dependencies other than "need to have that helper in place". And it's not hard to keep track of unconverted instances. Some of the existing functions would need struct qstr * argument constified - 4 of those in current mainline (the same functions would need that treatment with anon union approach as well; again, there are very few of them and it's not hard to do). * once everything is converted (with the exception of several places in fs/dcache.c and this d_name() inline itself), we can declared the use of d_name() mandatory and rename the field to e.g. __d_name. At that point audits become as easy as grep for new name of the field and for casts to struct qstr * - compiler will take care of the rest. That variant is longer, but it's not too large either. And it feels safer wrt optimizer behaviour... Preferences, comments? ^ permalink raw reply [flat|nested] 7+ messages in thread
* Re: [RFC] ->d_name accesses 2024-02-07 22:22 [RFC] ->d_name accesses Al Viro @ 2024-02-07 22:55 ` Matthew Wilcox 2024-02-09 19:10 ` Nick Desaulniers 0 siblings, 1 reply; 7+ messages in thread From: Matthew Wilcox @ 2024-02-07 22:55 UTC (permalink / raw) To: Al Viro; +Cc: linux-fsdevel, Linus Torvalds, Nathan Chancellor, Nick Desaulniers On Wed, Feb 07, 2024 at 10:22:48PM +0000, Al Viro wrote: > One way to do that would be to replace d_name with > union { > const struct qstr d_name; > struct qstr __d_name; > }; > and let the the places that want to modify it use __d_name. > Tempting, but the thing that makes me rather nervous about this > approach is that such games with unions are straying into > the nasal demon country (C99 6.7.3[5]), inviting the optimizers > to play. Matt Wilcox pointed out that mainline already has > i_nlink/__i_nlink pair, but... there are fewer users of those > and the damage from miscompiles would be less sensitive. > Patch along those lines would be pretty simple, though. You're referring to this, I assume: If an attempt is made to modify an object defined with a const-qualified type through use of an lvalue with non-const-qualified type, the behavior is undefined I'm not sure that's relevant. As I see it, we're defining two objects, one const-qualified and one not. The union specifies that they share the same storage ("a union is a type consisting of a sequence of members whose storage overlap"). I see 6.7.3 as saying "even if you cast away the const from d_name, modifyig d_name is undefined", but you're not modifying d_name, you're modifying __d_name, which just happens to share storage with d_name. I think 6.7.2.1[14] is more likely to cause us problems: The value of at most one of the members can be stored in a union object at any time. From that the compiler can assume that if it sees a store to __d_name that accesses to d_name are undefined? Perhaps? My brain always starts to hurt when people start spec-lawyering. > * add an inlined helper, > static inline const struct qstr *d_name(const struct dentry *d) > { > return &d->d_name; > } I'm in no way opposed to this solution. I just thought that the i_nlink solution might also work for you (and if the compiler people say the i_nlink solution is actually not spec compliant, then I guess we can adopt an i_nlink_read() function). ^ permalink raw reply [flat|nested] 7+ messages in thread
* Re: [RFC] ->d_name accesses 2024-02-07 22:55 ` Matthew Wilcox @ 2024-02-09 19:10 ` Nick Desaulniers 2024-02-09 19:27 ` Linus Torvalds ` (2 more replies) 0 siblings, 3 replies; 7+ messages in thread From: Nick Desaulniers @ 2024-02-09 19:10 UTC (permalink / raw) To: Matthew Wilcox; +Cc: Al Viro, linux-fsdevel, Linus Torvalds, Nathan Chancellor On Wed, Feb 7, 2024 at 2:55 PM Matthew Wilcox <willy@infradead.org> wrote: > > On Wed, Feb 07, 2024 at 10:22:48PM +0000, Al Viro wrote: > > One way to do that would be to replace d_name with > > union { > > const struct qstr d_name; > > struct qstr __d_name; > > }; > > and let the the places that want to modify it use __d_name. > > Tempting, but the thing that makes me rather nervous about this > > approach is that such games with unions are straying into > > the nasal demon country (C99 6.7.3[5]), inviting the optimizers > > to play. Matt Wilcox pointed out that mainline already has > > i_nlink/__i_nlink pair, but... there are fewer users of those > > and the damage from miscompiles would be less sensitive. > > Patch along those lines would be pretty simple, though. > > You're referring to this, I assume: > > If an attempt is made to modify an object defined with > a const-qualified type through use of an lvalue with > non-const-qualified type, the behavior is undefined I have 100% observed llvm throw out writes to objects declared as const where folks tried to write via "casting away the const" (since that's UB) which resulted in boot failures in the Linux kernel affecting android devices. I can't find the commit in question at the moment, but seemed to have made some kind of note in it in 2020. https://android-review.git.corp.google.com/c/platform/prebuilts/clang/host/linux-x86/+/1201901/1/RELEASE_NOTES.md That said, I just tried Al's union, and don't observe such optimizations. https://godbolt.org/z/zrj71E8W5 This whole suggestion makes me laugh "in C++ private members." > > I'm not sure that's relevant. As I see it, we're defining two objects, > one const-qualified and one not. The union specifies that they share > the same storage ("a union is a type consisting of a sequence of members > whose storage overlap"). > > I see 6.7.3 as saying "even if you cast away the const from d_name, > modifyig d_name is undefined", but you're not modifying d_name, > you're modifying __d_name, which just happens to share storage with > d_name. > > I think 6.7.2.1[14] is more likely to cause us problems: > > The value of at most one of the members can be stored in a union > object at any time. > > From that the compiler can assume that if it sees a store to __d_name > that accesses to d_name are undefined? Perhaps? My brain always starts > to hurt when people start spec-lawyering. > > > * add an inlined helper, > > static inline const struct qstr *d_name(const struct dentry *d) > > { > > return &d->d_name; > > } > > I'm in no way opposed to this solution. I just thought that the i_nlink > solution might also work for you (and if the compiler people say the > i_nlink solution is actually not spec compliant, then I guess we can > adopt an i_nlink_read() function). -- Thanks, ~Nick Desaulniers ^ permalink raw reply [flat|nested] 7+ messages in thread
* Re: [RFC] ->d_name accesses 2024-02-09 19:10 ` Nick Desaulniers @ 2024-02-09 19:27 ` Linus Torvalds 2024-02-09 20:01 ` Al Viro 2024-02-10 4:23 ` Al Viro 2 siblings, 0 replies; 7+ messages in thread From: Linus Torvalds @ 2024-02-09 19:27 UTC (permalink / raw) To: Nick Desaulniers Cc: Matthew Wilcox, Al Viro, linux-fsdevel, Nathan Chancellor On Fri, 9 Feb 2024 at 11:10, Nick Desaulniers <ndesaulniers@google.com> wrote: > > I have 100% observed llvm throw out writes to objects declared as > const where folks tried to write via "casting away the const" (since > that's UB) which resulted in boot failures in the Linux kernel > affecting android devices. *PLEASE* fix compilers that silently generate bogus code just because it's UB. That's pure and utter garbage. We want a compiler flag that says "don't do that idiotic sh*t". There are very good reasons why Linux uses flags like -fno-strict-overflow -fno-strict-aliasing -fno-delete-null-pointer-checks and that reason is that the ANSI C standards committee has had its head up its arse when it comes to these areas. Optimizations based on undefined behavior are wrong. If you have to resort to those kinds of optimizations, your compiler is bad. *Silently* doing so is even worse. If the compiler decides "I will throw away this write because it's UB", I want a warning. Better yet, I'd like to see compiler writers admit that undefined behavior was not a good language feature in the first place, and that any time the standard says "that's undefined behavior", you ignore that bogus standards language, and you turn it into "implementation defined", possibly with a warning. Linus ^ permalink raw reply [flat|nested] 7+ messages in thread
* Re: [RFC] ->d_name accesses 2024-02-09 19:10 ` Nick Desaulniers 2024-02-09 19:27 ` Linus Torvalds @ 2024-02-09 20:01 ` Al Viro 2024-02-10 4:23 ` Al Viro 2 siblings, 0 replies; 7+ messages in thread From: Al Viro @ 2024-02-09 20:01 UTC (permalink / raw) To: Nick Desaulniers Cc: Matthew Wilcox, linux-fsdevel, Linus Torvalds, Nathan Chancellor On Fri, Feb 09, 2024 at 11:10:10AM -0800, Nick Desaulniers wrote: > I have 100% observed llvm throw out writes to objects declared as > const where folks tried to write via "casting away the const" (since > that's UB) which resulted in boot failures in the Linux kernel > affecting android devices. I can't find the commit in question at the > moment, but seemed to have made some kind of note in it in 2020. > https://android-review.git.corp.google.com/c/platform/prebuilts/clang/host/linux-x86/+/1201901/1/RELEASE_NOTES.md > > That said, I just tried Al's union, and don't observe such optimizations. > https://godbolt.org/z/zrj71E8W5 FWIW, see #work.qstr in git://git.kernel.org:/pub/scm/linux/kernel/git/viro/vfs.git First 4 commits are constifying the arguments that get &dentry->d_inode passed into, followed by the patch below. __d_alloc(), swap_names(), copy_name() and d_mark_tmpfile() - no other writers... diff --git a/fs/dcache.c b/fs/dcache.c index b813528fb147..d21df0fb967d 100644 --- a/fs/dcache.c +++ b/fs/dcache.c @@ -1651,13 +1651,13 @@ static struct dentry *__d_alloc(struct super_block *sb, const struct qstr *name) dname = dentry->d_iname; } - dentry->d_name.len = name->len; - dentry->d_name.hash = name->hash; + dentry->__d_name.len = name->len; + dentry->__d_name.hash = name->hash; memcpy(dname, name->name, name->len); dname[name->len] = 0; /* Make sure we always see the terminating NUL character */ - smp_store_release(&dentry->d_name.name, dname); /* ^^^ */ + smp_store_release(&dentry->__d_name.name, dname); /* ^^^ */ dentry->d_lockref.count = 1; dentry->d_flags = 0; @@ -2695,16 +2695,16 @@ static void swap_names(struct dentry *dentry, struct dentry *target) /* * Both external: swap the pointers */ - swap(target->d_name.name, dentry->d_name.name); + swap(target->__d_name.name, dentry->__d_name.name); } else { /* * dentry:internal, target:external. Steal target's * storage and make target internal. */ - memcpy(target->d_iname, dentry->d_name.name, - dentry->d_name.len + 1); - dentry->d_name.name = target->d_name.name; - target->d_name.name = target->d_iname; + memcpy(target->d_iname, dentry->__d_name.name, + dentry->__d_name.len + 1); + dentry->__d_name.name = target->__d_name.name; + target->__d_name.name = target->d_iname; } } else { if (unlikely(dname_external(dentry))) { @@ -2712,10 +2712,10 @@ static void swap_names(struct dentry *dentry, struct dentry *target) * dentry:external, target:internal. Give dentry's * storage to target and make dentry internal */ - memcpy(dentry->d_iname, target->d_name.name, - target->d_name.len + 1); - target->d_name.name = dentry->d_name.name; - dentry->d_name.name = dentry->d_iname; + memcpy(dentry->d_iname, target->__d_name.name, + target->__d_name.len + 1); + target->__d_name.name = dentry->__d_name.name; + dentry->__d_name.name = dentry->d_iname; } else { /* * Both are internal. @@ -2728,7 +2728,7 @@ static void swap_names(struct dentry *dentry, struct dentry *target) } } } - swap(dentry->d_name.hash_len, target->d_name.hash_len); + swap(dentry->__d_name.hash_len, target->__d_name.hash_len); } static void copy_name(struct dentry *dentry, struct dentry *target) @@ -2738,12 +2738,12 @@ static void copy_name(struct dentry *dentry, struct dentry *target) old_name = external_name(dentry); if (unlikely(dname_external(target))) { atomic_inc(&external_name(target)->u.count); - dentry->d_name = target->d_name; + dentry->__d_name = target->__d_name; } else { - memcpy(dentry->d_iname, target->d_name.name, - target->d_name.len + 1); - dentry->d_name.name = dentry->d_iname; - dentry->d_name.hash_len = target->d_name.hash_len; + memcpy(dentry->d_iname, target->__d_name.name, + target->__d_name.len + 1); + dentry->__d_name.name = dentry->d_iname; + dentry->__d_name.hash_len = target->__d_name.hash_len; } if (old_name && likely(atomic_dec_and_test(&old_name->u.count))) kfree_rcu(old_name, u.head); @@ -3080,7 +3080,7 @@ void d_mark_tmpfile(struct file *file, struct inode *inode) !d_unlinked(dentry)); spin_lock(&dentry->d_parent->d_lock); spin_lock_nested(&dentry->d_lock, DENTRY_D_LOCK_NESTED); - dentry->d_name.len = sprintf(dentry->d_iname, "#%llu", + dentry->__d_name.len = sprintf(dentry->d_iname, "#%llu", (unsigned long long)inode->i_ino); spin_unlock(&dentry->d_lock); spin_unlock(&dentry->d_parent->d_lock); diff --git a/include/linux/dcache.h b/include/linux/dcache.h index eb4ca8a1d948..a74370b1fe31 100644 --- a/include/linux/dcache.h +++ b/include/linux/dcache.h @@ -85,7 +85,10 @@ struct dentry { seqcount_spinlock_t d_seq; /* per dentry seqlock */ struct hlist_bl_node d_hash; /* lookup hash list */ struct dentry *d_parent; /* parent directory */ - struct qstr d_name; + union { + struct qstr __d_name; + const struct qstr d_name; + }; struct inode *d_inode; /* Where the name belongs to - NULL is * negative */ unsigned char d_iname[DNAME_INLINE_LEN]; /* small names */ ^ permalink raw reply related [flat|nested] 7+ messages in thread
* Re: [RFC] ->d_name accesses 2024-02-09 19:10 ` Nick Desaulniers 2024-02-09 19:27 ` Linus Torvalds 2024-02-09 20:01 ` Al Viro @ 2024-02-10 4:23 ` Al Viro 2024-02-10 16:25 ` Linus Torvalds 2 siblings, 1 reply; 7+ messages in thread From: Al Viro @ 2024-02-10 4:23 UTC (permalink / raw) To: Nick Desaulniers Cc: Matthew Wilcox, linux-fsdevel, Linus Torvalds, Nathan Chancellor On Fri, Feb 09, 2024 at 11:10:10AM -0800, Nick Desaulniers wrote: > I have 100% observed llvm throw out writes to objects declared as > const where folks tried to write via "casting away the const" (since > that's UB) which resulted in boot failures in the Linux kernel > affecting android devices. I can't find the commit in question at the > moment, but seemed to have made some kind of note in it in 2020. > https://android-review.git.corp.google.com/c/platform/prebuilts/clang/host/linux-x86/+/1201901/1/RELEASE_NOTES.md > > That said, I just tried Al's union, and don't observe such optimizations. > https://godbolt.org/z/zrj71E8W5 The really shitty part is not the missing stores; it's reordered loads. If spin_lock(&dentry->d_lock); name = dentry->d_name.name; len = dentry->d_name.len; something(name, len); spin_unlock(&dentry->d_lock); has the compiler go "->d_name is const, so I can bloody well move the load before that spin_lock() call", we really have a problem. Can it reorder the loads of const member wrt e.g. barrier()? If that's the case, this approach is no-go and accessor is the only feasible alternative. ^ permalink raw reply [flat|nested] 7+ messages in thread
* Re: [RFC] ->d_name accesses 2024-02-10 4:23 ` Al Viro @ 2024-02-10 16:25 ` Linus Torvalds 0 siblings, 0 replies; 7+ messages in thread From: Linus Torvalds @ 2024-02-10 16:25 UTC (permalink / raw) To: Al Viro; +Cc: Nick Desaulniers, Matthew Wilcox, linux-fsdevel, Nathan Chancellor On Fri, 9 Feb 2024 at 20:23, Al Viro <viro@zeniv.linux.org.uk> wrote: > > Can it reorder the loads of const member wrt e.g. barrier()? As far as I know, a C compiler can *not* in any situation think that "const" means that something is immutable. In C, a pointer to a "const' thing means that you are not allowed to modify the object through *that* pointer, but it does not mean that there might not be another non-const pointer that aliases the same object. Now, things are a bit different if the object definition itself is "const", in that then there is generally no way for such an alias to happen. But anything that gets a pointer to a data structure that has a const member cannot in the general case know whether there might be another pointer somewhere else that could change said member. IOW, 'const' - despite the name - does not mean "this field is constant". It literally means "you can't write through this field". And then as a very special case of that, if the compiler can show that there are no possible aliases, the compiler can treat it as a constant value. The "no aliases" knowledge might be because the compiler itself generates the object (eg an automatic variable), but it might also be through things like "restrict" where the programmer has told the compiler that no aliases exist. But when it is a part of a union, by *definition* there are aliases to the same field, and they may not be const. End result: a compiler cannot validly hoist the load across the spin_lock, because as far as the compiler knows, the spinlock could change the value. 'const' in no way means 'the value cannot change'. Of course, who knows what bugs can exist, but this is fairly fundamental. I don't believe a C compiler can possibly get this wrong and call itself a C compiler. Linus ^ permalink raw reply [flat|nested] 7+ messages in thread
end of thread, other threads:[~2024-02-10 16:25 UTC | newest] Thread overview: 7+ messages (download: mbox.gz follow: Atom feed -- links below jump to the message on this page -- 2024-02-07 22:22 [RFC] ->d_name accesses Al Viro 2024-02-07 22:55 ` Matthew Wilcox 2024-02-09 19:10 ` Nick Desaulniers 2024-02-09 19:27 ` Linus Torvalds 2024-02-09 20:01 ` Al Viro 2024-02-10 4:23 ` Al Viro 2024-02-10 16:25 ` Linus Torvalds
This is a public inbox, see mirroring instructions for how to clone and mirror all data and code used for this inbox; as well as URLs for NNTP newsgroup(s).