Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add lint against function pointer comparisons #118833

Open
wants to merge 4 commits into
base: master
Choose a base branch
from

Conversation

Urgau
Copy link
Member

@Urgau Urgau commented Dec 11, 2023

This is kind of a follow-up to #117758 where we added a lint against wide pointer comparisons for being ambiguous and unreliable; well function pointer comparisons are also unreliable. We should IMO follow a similar logic and warn people about it.


unpredictable_function_pointer_comparisons

warn-by-default

The unpredictable_function_pointer_comparisons lint checks comparison of function pointer as the operands.

Example

fn foo() {}
let a = foo as fn();

let _ = a == foo;

Explanation

Function pointers comparisons do not produce meaningful result since they are never guaranteed to be unique and could vary between different code generation units. Furthermore different function could have the same address after being merged together.


This PR also uplift the very similar clippy::fn_address_comparisons lint, which only linted on if one of the operand was an ty::FnDef while this PR lints proposes to lint on all ty::FnPtr and ty::FnDef.

@rustbot labels I-lang-nominated

Edit: Blocked on rust-lang/libs-team#323 being accepted and it's follow-up pr

@rustbot
Copy link
Collaborator

rustbot commented Dec 11, 2023

r? @cjgillot

(rustbot has picked a reviewer for you, use r? to override)

@rustbot rustbot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. labels Dec 11, 2023
@rustbot
Copy link
Collaborator

rustbot commented Dec 11, 2023

Some changes occurred in src/tools/clippy

cc @rust-lang/clippy

The Miri subtree was changed

cc @rust-lang/miri

@rustbot rustbot added the I-lang-nominated Nominated for discussion during a lang team meeting. label Dec 11, 2023
@rust-log-analyzer

This comment has been minimized.

@Noratrieb
Copy link
Member

The lint name is very long, it would be nice if we could come up with a shorter name. I don't have a suggestion though.

@Urgau
Copy link
Member Author

Urgau commented Dec 12, 2023

The lint name is very long, it would be nice if we could come up with a shorter name. I don't have a suggestion though.

Sure, but I don't think it matter that much, I'm not expecting anyone to write the lint name by hand (since it's warn-by-default, people can just copy/paste in the diagnostic output). We also already have some pretty long lint name, like unknown_or_malformed_diagnostic_attributes.
I also think it's better to have a long and descriptive lint name than a short one that is less descriptive.

But if someone comes up with a name as descriptive but shorter I can integrate it, I'm just not sure it's worth anyone time.

@Urgau Urgau force-pushed the lint_function_pointer_comparisons branch from e0a6772 to 4798319 Compare December 12, 2023 11:38
@rust-log-analyzer

This comment has been minimized.

@Urgau Urgau force-pushed the lint_function_pointer_comparisons branch from 444a0b9 to 23214cb Compare December 13, 2023 10:22
@compiler-errors
Copy link
Member

If it's not too much work, could we do a crater run here to find out what kind of places are using this kind of comparison in the wild?

@Urgau
Copy link
Member Author

Urgau commented Dec 13, 2023

If it's not too much work, could we do a crater run here to find out what kind of places are using this kind of comparison in the wild?

Sure, I just pushed a commit making the lint deny-by-default. I will let you do the bors craterbot commands.

@compiler-errors
Copy link
Member

@bors try

@bors
Copy link
Contributor

bors commented Dec 13, 2023

⌛ Trying commit 6c5c546 with merge 2959657...

bors added a commit to rust-lang-ci/rust that referenced this pull request Dec 13, 2023
…ons, r=<try>

Add lint against function pointer comparisons

This is kind of a follow-up to rust-lang#117758 where we added a lint against wide pointer comparisons for being ambiguous and unreliable; well function pointer comparisons are also unreliable. We should IMO follow a similar logic and warn people about it.

-----

## `unpredictable_function_pointer_comparisons`

*warn-by-default*

The `unpredictable_function_pointer_comparisons` lint checks comparison of function pointer as the operands.

### Example

```rust
fn foo() {}
let a = foo as fn();

let _ = a == foo;
```

### Explanation

Function pointers comparisons do not produce meaningful result since they are never guaranteed to be unique and could vary between different code generation units. Furthermore different function could have the same address after being merged together.

----

This PR also uplift the very similar `clippy::fn_address_comparisons` lint, which only linted on if one of the operand was an `ty::FnDef` while this PR lints proposes to lint on all `ty::FnPtr` and `ty::FnDef`.

`@rustbot` labels  I-lang-nominated
@rust-log-analyzer

This comment has been minimized.

@bors
Copy link
Contributor

bors commented Dec 13, 2023

☀️ Try build successful - checks-actions
Build commit: 2959657 (29596578cb5035fa2b7ac7fbdfea23fab38836e7)

@traviscross
Copy link
Contributor

traviscross commented Dec 13, 2023

@rustbot labels -I-lang-nominated

We discussed this in the T-lang meeting today. There was some support for this, but people were concerned about whether there may be legitimate use cases and what we would be suggesting that those people do instead. For wide pointer comparisons, we suggest that people use ptr::addr_eq even though that has the same problems as comparison with == as it at least makes the intent of the user clear. What would be the comparable thing we would do here?

The consensus was that seeing use cases would help, and that a crater run would help to find those use cases. That's now happening in the comments above. Once the crater run comes back and has been analyzed, please renominate for T-lang discussion.

For the analysis, we're looking to find 1) use cases of this that are correct in the sense that they rely only on the actual semantics, and 2) the prevalence of bugs where people are using this in ways that rely on semantics that do not actually hold.

@rustbot rustbot removed the I-lang-nominated Nominated for discussion during a lang team meeting. label Dec 13, 2023
@RalfJung
Copy link
Member

What would be the comparable thing we would do here?

There's no such thing, comparing Rust functions for equality is in general just inherently meaningless. That doesn't make code using == any less buggy though. If we want to support that usecase we need a fundamental language change, likely removing the LLVM flag which tells the backend that function addresses do not matter.

The flip side of this is that the standard library itself actually relies on such a comparison in the format-args machinery... it's for a special case (a monomorphic function) where the current implementation AFAIK does not generate duplicates (but I don't know how multi-CGU handling works so I am not sure). It's certainly not guaranteed by the language.

@compiler-errors
Copy link
Member

@craterbot check

@craterbot
Copy link
Collaborator

👌 Experiment pr-118833 created and queued.
🤖 Automatically detected try build 2959657
🔍 You can check out the queue and this experiment's details.

ℹ️ Crater is a tool to run experiments across parts of the Rust ecosystem. Learn more

@craterbot craterbot added S-waiting-on-crater Status: Waiting on a crater run to be completed. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Dec 14, 2023
@traviscross
Copy link
Contributor

What would be the comparable thing we would do here?

There's no such thing, comparing Rust functions for equality is in general just inherently meaningless.

One thought expressed in the meeting was as follows:

Comparing functions for equality via pointers may yield false negatives but not false positives. The fact that two functions may compare unequal (based on pointers to them) but in fact do the same thing is a rather fundamental property of not just Rust, but of any language. In general, it's impossible to know whether two different functions may in fact do the same thing.

In this light, maybe it's OK that these comparisons produce false negatives, and maybe there exist valid use cases that only rely on the property that we will not return false positives.

@RalfJung
Copy link
Member

Comparing functions for equality via pointers may yield false negatives but not false positives.

That's not true. There are both false negatives and false positives. That's exactly why I wanted the lint description to be clear about this.

False positives arise when LLVM merges two functions because they optimized to the same code.

@Urgau
Copy link
Member Author

Urgau commented Dec 31, 2023

Crater Analysis Result

Summary

Crate True-positive Not duplicated In PartialEq Comments
AntidermisMC.rlox logs ? VM / Interpreter
BillyLevin.rs-monkes logs ? VM / Interpreter
BrunoCaste/Lox-Rs logs ? VM / Interpreter
Danielmelody/Ruschm logs ? VM / Interpreter
Deen17/aoc2020 logs fn-ptr != fn-def
HallerPatrick/liva-lang logs ? VM / Interpreter
KarimIbrahim/monkey_interpreter logs ? VM / Interpreter
Kiwifuit/event-driven-rust logs Removing duplicates
MarkRoss470/acpica-rust-bindings logs fn-ptr == fn-ptr, but fn-ptrs created from unique addresses
cafebabeeee/rust-tutorial logs tutorial, in println!, fn-ptrs derived from fn-defs
PhilipDaniels/projecteuler logs fn-ptr == fn-def
ProfessorQu/battleship_website logs fn-ptr == fn-def(-ish)
RussellSnyder/AOC-rust-day21 logs fn-ptr != fn-def
ShivaBhattacharjee/Synthia logs ? VM / Interpreter
Swiftaff/rust-learning-parser-combinators logs fn-ptr == fn-def
TypstApp-team/typst logs ? VM / Interpreter
Yiklek/rslua logs fn-ptr == fn-def
ZachClayburn/CompilerDesign-Project2 logs fn-ptr == fn-def
adriensamson/aoc logs filtering on Vec of fn-ptrs
b-r-a-n/pills logs fn-ptr == fn-def
bgnerdclub/birb logs filtering on Vec of fn-ptrs
cathangormley/kr-a logs fn-ptr == fn-def
chrisdean258/terse logs ? VM / Interpreter
codaishin/project-zyheeda-bevy logs fn-ptr == fn-def(-ish)
itsaramcc/nes-rs logs fn-ptr == fn-def

...

Note: I stopped at the nes-rs for now, I may continue later but given the trends I don't think anything new will come.

Highlights

ShivaBhattacharjee/Synthia

code

VM / Interpreter, very similar pattern in many other projects.
Unable to determine if it's true-positive or false-positive.

impl PartialEq for Object {
    fn eq(&self, other: &Self) -> bool {
        match (self, other) {
            (Object::Number(a), Object::Number(b)) => a == b,
            (Object::String(a), Object::String(b)) => a == b,
            ...
            (Object::Fn(a, b, c), Object::Fn(d, e, f)) => a == d && b == e && c == f,
            (Object::Inbuilt(a), Object::Inbuilt(b)) => a == b,
            ...
            _ => false,
        }
    }
}

ZachClayburn/CompilerDesign-Project2

logs code

Rely on function NOT being duplicated.

pub fn get_reduction_name(function_pointer: &ReductionOp) -> &'static str {
    match *function_pointer {
        x if x == reduce_value => "reduce_value()",
        x if x == reduce_binary_op => "reduce_binary_op()",
        x if x == reduce_parenthetical => "reduce_parenthetical()",
        x if x == reduce_unary_operator => "reduce_unary_operator()",
        x if x == reduce_program => "reduce_program()",
        x if x == reduce_statement_list => "reduce_statement_list()",
        x if x == reduce_declaration => "reduce_declaration()",
        x if x == reduce_param_spec => "reduce_param_spec()",
        x if x == reduce_return_statement => "reduce_return_statement()",
        x if x == reduce_procedure_declaration => "reduce_procedure_declaration()",
        x if x == reduce_procedure_call => "reduce_procedure_call()",
        _ => panic!("unknown reduction"),
    }
}

HugoBelotDeloro/rlox

logs code

Compare functions pointers derived from the same list.

                *p = p.iter().filter(|&&f2| f2 != f).copied().collect::<Vec<&OpFn>>();

Observation

  1. Some false positives, mainly when filtering fn-ptrs within a vector.
  2. Many true-positives, the vast majority when comparing a fn-ptr with a fn-def.

Requiring that one of the operands is an fn-def may be a good compromise.

Note: I think I remember Josh mentioning a caching pattern that involved fn-ptrs, but I haven't seen any evidence that this pattern is used and affected by this lint.

@rustbot label I-lang-nominated -S-waiting-on-review S-waiting-on-team T-lang

@rustbot rustbot added I-lang-nominated Nominated for discussion during a lang team meeting. S-waiting-on-team Status: Awaiting decision from the relevant subteam (see the T-<team> label). T-lang Relevant to the language team, which will review and decide on the PR/issue. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Dec 31, 2023
@Urgau Urgau force-pushed the lint_function_pointer_comparisons branch from 6c5c546 to 05eef1b Compare January 1, 2024 18:25
@tmiasko
Copy link
Contributor

tmiasko commented Jan 1, 2024

The standard library also uses function pointer comparison to implement Waker::will_wake.

@scottmcm
Copy link
Member

scottmcm commented Jan 3, 2024

I'm concerned about this if there isn't a way to say "I know, I want to do it anyway" other than allowing the lint.

I consider the fat pointer lint to be about "you should be clearer about whether you intended to compare the metadata or not", not "comparing metadata is bad".

It would be entirely correct to memoize based on function pointer equality, for example. If that means that x x and 2 * x share the same cache, then great. And if two different instantiations aren't merged, oh well, it's not the end of the world.

To me a "be sure you know what this does" lint like this is a clippy lint, not a rustc lint, if we can't suggest a better alternative.

@Urgau
Copy link
Member Author

Urgau commented Jan 3, 2024

I'm concerned about this if there isn't a way to say "I know, I want to do it anyway" other than allowing the lint.

The T-lang triage meeting consensus was to suggest a method (in the same way that was done for thin ptr comparisons lint with ptr::addr_eq) and proposed ptr::fn_addr_eq ACP.

@asquared31415
Copy link
Contributor

The fact that the result can never be meaningful beyond "definitely runs the same code if true" makes comparing function pointers a lot like needs_drop or similar where you can't rely on it for anything besides optimization and need to be correct in case of spurious incorrect results, and I think that needs to be much more obvious.

@RalfJung
Copy link
Member

RalfJung commented Jan 11, 2024

The standard library also uses function pointer comparison to implement Waker::will_wake.

It also compares them here, in dubious ways:

pub(super) fn as_usize(&self) -> Option<usize> {
// We are type punning a bit here: USIZE_MARKER only takes an &usize but
// formatter takes an &Opaque. Rust understandably doesn't think we should compare
// the function pointers if they don't have the same signature, so we cast to
// usizes to tell it that we just want to compare addresses.
if self.formatter as usize == USIZE_MARKER as usize {
// SAFETY: The `formatter` field is only set to USIZE_MARKER if
// the value is a usize, so this is safe
Some(unsafe { *(self.value as *const _ as *const usize) })
} else {
None
}
}

@bors
Copy link
Contributor

bors commented Feb 16, 2024

☔ The latest upstream changes (presumably #120881) made this pull request unmergeable. Please resolve the merge conflicts.

@Dylan-DPC Dylan-DPC added S-blocked Status: Marked as blocked ❌ on something else such as an RFC or other implementation work. and removed S-waiting-on-team Status: Awaiting decision from the relevant subteam (see the T-<team> label). labels Feb 19, 2024
@Urgau
Copy link
Member Author

Urgau commented Aug 29, 2024

For the record the T-lib-api ACP#323 about adding ptr::fn_addr_eq (with 2 generic arguments) was accepted and implemented in #129323.

@Dylan-DPC Dylan-DPC added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. and removed S-blocked Status: Marked as blocked ❌ on something else such as an RFC or other implementation work. S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Oct 2, 2024
@Dylan-DPC
Copy link
Member

@Urgau this is no longer blocked so if you can resolve the conflicts we can push this forward for a review. Thanks

@Urgau Urgau force-pushed the lint_function_pointer_comparisons branch from 05eef1b to ab73468 Compare October 2, 2024 13:38
@traviscross
Copy link
Contributor

traviscross commented Oct 3, 2024

Looks like on the lang side we still need to make a decision about whether we want this. That's easier now that the libs-api prerequisite happened (on nightly). This is more actionable now, so we'll bump it up the agenda.

@Urgau Urgau added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. and removed S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. labels Oct 3, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
I-lang-nominated Nominated for discussion during a lang team meeting. S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. T-lang Relevant to the language team, which will review and decide on the PR/issue.
Projects
None yet
Development

Successfully merging this pull request may close these issues.