lint on `core::ffi::c_void` as a return type
diff --git a/compiler/rustc_lint/src/c_void_returns.rs b/compiler/rustc_lint/src/c_void_returns.rs
new file mode 100644
index 0000000..f4dd260
--- /dev/null
+++ b/compiler/rustc_lint/src/c_void_returns.rs
@@ -0,0 +1,89 @@
+use rustc_abi::ExternAbi;
+use rustc_hir::def::Res;
+use rustc_hir::def_id::LocalDefId;
+use rustc_hir::intravisit::FnKind;
+use rustc_hir::{self as hir, LangItem};
+use rustc_session::{declare_lint, declare_lint_pass};
+use rustc_span::Span;
+
+use crate::lints::{CVoidReturn, ExternCVoidReturn};
+use crate::{LateContext, LateLintPass, LintContext};
+
+declare_lint! {
+    /// The `c_void_returns` lint detects the use of [`core::ffi::c_void`] as a return type.
+    ///
+    /// ### Example
+    ///
+    /// ```rust
+    /// use std::ffi::c_void;
+    ///
+    /// unsafe extern "C" {
+    ///     fn foo() -> c_void;
+    /// }
+    /// ```
+    ///
+    /// {{produces}}
+    ///
+    /// ### Explanation
+    ///
+    /// `c_void` is designed for use through a [`pointer`], equivalent to C's `void*` type. It is a
+    /// mistake to use it directly as a return type, and calling `extern` functions declared as such
+    /// may result in undefined behavior. C functions that return `void` must be declared to return
+    /// [`()`] in Rust (omitting the return type implicitly returns `()`).
+    ///
+    /// [`core::ffi::c_void`]: https://doc.rust-lang.org/core/ffi/enum.c_void.html
+    /// [`pointer`]: https://doc.rust-lang.org/core/primitive.pointer.html
+    /// [`()`]: https://doc.rust-lang.org/core/primitive.unit.html
+    pub C_VOID_RETURNS,
+    Warn,
+    "detects use of `c_void` as a return type"
+}
+
+declare_lint_pass!(CVoidReturns => [C_VOID_RETURNS]);
+
+impl<'tcx> LateLintPass<'tcx> for CVoidReturns {
+    fn check_fn(
+        &mut self,
+        cx: &LateContext<'tcx>,
+        fn_kind: FnKind<'tcx>,
+        decl: &'tcx hir::FnDecl<'tcx>,
+        _: &'tcx hir::Body<'tcx>,
+        _: Span,
+        _: LocalDefId,
+    ) {
+        check_decl(
+            cx,
+            decl,
+            !matches!(fn_kind, FnKind::ItemFn(.., hir::FnHeader { abi: ExternAbi::Rust, .. })),
+        );
+    }
+
+    fn check_foreign_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx hir::ForeignItem<'tcx>) {
+        if let hir::ForeignItemKind::Fn(sig, ..) = item.kind {
+            check_decl(cx, sig.decl, true);
+        }
+    }
+
+    fn check_ty(&mut self, cx: &LateContext<'tcx>, ty: &'tcx hir::Ty<'tcx, hir::AmbigArg>) {
+        if let hir::TyKind::FnPtr(fn_ptr_ty) = ty.kind {
+            check_decl(cx, fn_ptr_ty.decl, fn_ptr_ty.abi != ExternAbi::Rust);
+        }
+    }
+}
+
+fn check_decl(cx: &LateContext<'_>, decl: &hir::FnDecl<'_>, is_extern: bool) {
+    if let hir::FnRetTy::Return(output_ty) = decl.output
+        && let hir::TyKind::Path(qpath) = output_ty.kind
+        && let Res::Def(.., def_id) = cx.qpath_res(&qpath, output_ty.hir_id)
+        && cx.tcx.is_lang_item(def_id, LangItem::CVoid)
+    {
+        let suggestion =
+            cx.sess().source_map().span_extend_to_prev_char(decl.output.span(), ')', true);
+
+        if is_extern {
+            cx.emit_span_lint(C_VOID_RETURNS, decl.output.span(), ExternCVoidReturn { suggestion });
+        } else {
+            cx.emit_span_lint(C_VOID_RETURNS, decl.output.span(), CVoidReturn { suggestion });
+        }
+    }
+}
diff --git a/compiler/rustc_lint/src/lib.rs b/compiler/rustc_lint/src/lib.rs
index 74bee7d..0e96b9f 100644
--- a/compiler/rustc_lint/src/lib.rs
+++ b/compiler/rustc_lint/src/lib.rs
@@ -32,6 +32,7 @@
 mod async_fn_in_trait;
 mod autorefs;
 pub mod builtin;
+mod c_void_returns;
 mod context;
 mod dangling;
 mod default_could_be_derived;
@@ -86,6 +87,7 @@
 use async_fn_in_trait::AsyncFnInTrait;
 use autorefs::*;
 use builtin::*;
+use c_void_returns::*;
 use dangling::*;
 use default_could_be_derived::DefaultCouldBeDerived;
 use deref_into_dyn_supertrait::*;
@@ -269,6 +271,7 @@ fn lint_mod(tcx: TyCtxt<'_>, module_def_id: LocalModDefId) {
             LifetimeSyntax: LifetimeSyntax,
             InternalEqTraitMethodImpls: InternalEqTraitMethodImpls,
             ImplicitProvenanceCasts: ImplicitProvenanceCasts,
+            CVoidReturns: CVoidReturns,
         ]
     ]
 );
diff --git a/compiler/rustc_lint/src/lints.rs b/compiler/rustc_lint/src/lints.rs
index 83827c5..eb82afa 100644
--- a/compiler/rustc_lint/src/lints.rs
+++ b/compiler/rustc_lint/src/lints.rs
@@ -614,6 +614,33 @@ pub(crate) enum BuiltinSpecialModuleNameUsed {
     Main,
 }
 
+// c_void_return.rs
+#[derive(Diagnostic)]
+#[diag("`c_void` should not be used as a return type")]
+#[help("returning `()` in Rust is equivalent to returning `void` in C")]
+pub(crate) struct CVoidReturn {
+    #[suggestion(
+        "remove the return type to implicitly return `()`",
+        code = "",
+        applicability = "maybe-incorrect"
+    )]
+    pub suggestion: Span,
+}
+
+// c_void_return.rs
+#[derive(Diagnostic)]
+#[diag("declarations returning `c_void` are not compatible with C functions returning `void`")]
+#[help("returning `()` in Rust is equivalent to returning `void` in C")]
+#[note("`c_void` is only used through raw pointers for compatibility with `void` pointers")]
+pub(crate) struct ExternCVoidReturn {
+    #[suggestion(
+        "remove the return type to implicitly return `()`",
+        code = "",
+        applicability = "maybe-incorrect"
+    )]
+    pub suggestion: Span,
+}
+
 // deref_into_dyn_supertrait.rs
 #[derive(Diagnostic)]
 #[diag("this `Deref` implementation is covered by an implicit supertrait coercion")]
diff --git a/tests/ui/lint/c-void-returns.rs b/tests/ui/lint/c-void-returns.rs
new file mode 100644
index 0000000..7531da3
--- /dev/null
+++ b/tests/ui/lint/c-void-returns.rs
@@ -0,0 +1,22 @@
+#![allow(unused)]
+#![deny(c_void_returns)]
+
+use std::ffi::c_void;
+use std::ptr;
+
+fn foo() -> c_void { //~ ERROR c_void
+    unreachable!()
+}
+
+fn bar() -> *mut c_void {
+    ptr::null_mut()
+}
+
+unsafe extern "C" {
+    fn baz() -> c_void; //~ ERROR c_void
+    fn quux() -> *const c_void;
+}
+
+type Xyzzy = fn() -> c_void; //~ ERROR c_void
+
+fn main() {}
diff --git a/tests/ui/lint/c-void-returns.stderr b/tests/ui/lint/c-void-returns.stderr
new file mode 100644
index 0000000..7dc7249
--- /dev/null
+++ b/tests/ui/lint/c-void-returns.stderr
@@ -0,0 +1,38 @@
+error: `c_void` should not be used as a return type
+  --> $DIR/c-void-returns.rs:7:13
+   |
+LL | fn foo() -> c_void {
+   |         ----^^^^^^
+   |         |
+   |         help: remove the return type to implicitly return `()`
+   |
+   = help: returning `()` in Rust is equivalent to returning `void` in C
+note: the lint level is defined here
+  --> $DIR/c-void-returns.rs:2:9
+   |
+LL | #![deny(c_void_returns)]
+   |         ^^^^^^^^^^^^^^
+
+error: declarations returning `c_void` are not compatible with C functions returning `void`
+  --> $DIR/c-void-returns.rs:16:17
+   |
+LL |     fn baz() -> c_void;
+   |             ----^^^^^^
+   |             |
+   |             help: remove the return type to implicitly return `()`
+   |
+   = help: returning `()` in Rust is equivalent to returning `void` in C
+   = note: `c_void` is only used through raw pointers for compatibility with `void` pointers
+
+error: `c_void` should not be used as a return type
+  --> $DIR/c-void-returns.rs:20:22
+   |
+LL | type Xyzzy = fn() -> c_void;
+   |                  ----^^^^^^
+   |                  |
+   |                  help: remove the return type to implicitly return `()`
+   |
+   = help: returning `()` in Rust is equivalent to returning `void` in C
+
+error: aborting due to 3 previous errors
+