lists.openwall.net   lists  /  announce  owl-users  owl-dev  john-users  john-dev  passwdqc-users  yescrypt  popa3d-users  /  oss-security  kernel-hardening  musl  sabotage  tlsify  passwords  /  crypt-dev  xvendor  /  Bugtraq  Full-Disclosure  linux-kernel  linux-netdev  linux-ext4  linux-hardening  linux-cve-announce  PHC 
Open Source and information security mailing list archives
 
Hash Suite: Windows password security audit tool. GUI, reports in PDF.
[<prev] [next>] [<thread-prev] [thread-next>] [day] [month] [year] [list]
Message-Id: <DCYCVV212A7X.U2ZDJDY18LQX@kernel.org>
Date: Sun, 21 Sep 2025 11:03:59 +0200
From: "Benno Lossin" <lossin@...nel.org>
To: "Joel Fernandes" <joelagnelf@...dia.com>
Cc: <linux-kernel@...r.kernel.org>, "Miguel Ojeda" <ojeda@...nel.org>, "Alex
 Gaynor" <alex.gaynor@...il.com>, "Boqun Feng" <boqun.feng@...il.com>, "Gary
 Guo" <gary@...yguo.net>, Björn Roy Baron
 <bjorn3_gh@...tonmail.com>, "Andreas Hindborg" <a.hindborg@...nel.org>,
 "Alice Ryhl" <aliceryhl@...gle.com>, "Trevor Gross" <tmgross@...ch.edu>,
 "Danilo Krummrich" <dakr@...nel.org>, <acourbot@...dia.com>, "Alistair
 Popple" <apopple@...dia.com>, "Timur Tabi" <ttabi@...dia.com>,
 <rust-for-linux@...r.kernel.org>
Subject: Re: [PATCH] rust: print: Fix issue with rust_build_error

On Sun Sep 21, 2025 at 9:12 AM CEST, Benno Lossin wrote:
> On Sun Sep 21, 2025 at 2:45 AM CEST, Joel Fernandes wrote:
>> On Sat, Sep 20, 2025 at 10:09:30PM +0200, Benno Lossin wrote:
>>> On Sat Sep 20, 2025 at 6:19 PM CEST, Joel Fernandes wrote:
>>> > When printing just before calling io.write32(), modpost fails due to
>>> > build_assert's missing rust_build_error symbol. The issue is that, the
>>> > printk arguments are passed as reference in bindings code, thus Rust
>>> > cannot trust its value and fails to optimize away the build_assert.
>>> >
>>> > The issue can be reproduced with the following simple snippet:
>>> >   let offset = 0;
>>> >   pr_err!("{}", offset);
>>> >   io.write32(base, offset);
>>> 
>>> I took a long time to understand this and I think I got it now, but
>>> maybe I'm still wrong: passing `offset` to `printk` via a reference
>>> results in the compiler thinking that the value of `offset` might be
>>> changed (even though its a shared reference I assume). For this reason
>>> the `build_assert!` used by `io.write32` cannot be optimized away.
>>
>> Yes, that's correct and that's my understanding. I in fact also dumped the IR
>> and see that the compiler is trying to reload the pointer to the offset. I
>> feel this is a compiler bug, and for immutable variables, the compiler should
>> not be reloading them even if they are passed to external code? Because if
>> external code is modifying immutable variables, that is buggy anyway.
>
> It would be great if you could add all of the extra info that you looked
> at into the commit message here. So all of the code to reproduce the
> issue, the IR you looked at...
>
>>> > Fix it by just using a closure to call printk. Rust captures the
>>> > arguments into the closure's arguments thus breaking the dependency.
>>> > This can be fixed by simply creating a variable alias for each variable
>>> > however the closure is a simple and concise fix.
>>> 
>>> I don't think this is the fix we want to have.
>>
>> Yeah its ugly, though a small workaround. IMO ideally the fix should be in
>> the compiler. I want to send a proposal for this in fact. I looked at the MIR
>> and I believe with my limited understanding, that the MIR should be issuing a
>> copy before making a pointer of the immutable variable if pointers to
>> immutable variables are being passed to external functions.
>
> ... and definitely the MIR.
>
>> If I were to send a proposal to the Rust language to start a discussion
>> leading to a potential RFC stage, would you mind guiding me on doing so?
>
> I agree that this should be fixed in the compiler if we aren't missing
> some attribute on one of our functions.
>
> Note that this probably doesn't require an RFC, since it's not a big
> feature and I imagine that there isn't much controversy about it. We can
> mention this in our sync meeting on Wednesday with the Rust folks & see
> what they have to say about it. After that we probably want to open an
> issue about it, I'll let you know.

I have created a simpler example than using `io.write32` and given lots
of info below, I'll show this to the Rust folks & you can add it to the
commit message if you want :)

Printing and our build assert doesn't work together:

```rust
let x = 0;
pr_info!("{}", x);
build_assert!(x == 0);
```

Macros expanded:

```rust
let x = 0;
match format_args!("{0}", x) {
    args => unsafe {
        ::kernel::print::call_printk(
            &::kernel::print::format_strings::INFO,
            crate::__LOG_PREFIX,
            args,
        );
    },
};
{
    if !(x == 0) {
        ::kernel::build_assert::build_error("assertion failed: x == 0");
    }
};
```

where `build_error` & `call_printk` are defined as:

```rust
#[inline(never)]
#[cold]
#[export_name = "rust_build_error"]
#[track_caller]
pub const fn build_error(msg: &'static str) -> ! {
    panic!("{}", msg);
}

pub unsafe fn call_printk(
    format_string: &[u8; format_strings::LENGTH],
    module_name: &[u8],
    args: fmt::Arguments<'_>,
) {
    unsafe {
        bindings::_printk(
            format_string.as_ptr(),
            module_name.as_ptr(),
            core::ptr::from_ref(&args).cast::<c_void>(),
        );
    }
}
// bindings:
extern "C" {
    pub fn _printk(fmt: *const ffi::c_char, ...) -> ffi::c_int;
}
```

```c
int _printk(const char *fmt, ...);
```

When compiling, we won't have the `rust_build_error` symbol defined, so if the
linker sees this, it will fail with a (very unergonomic) error:

```text
ld.lld: error: undefined symbol: rust_build_error
>>> referenced by usercopy_64.c
>>>               vmlinux.o:(print_build_assert_test)
make[2]: *** [scripts/Makefile.vmlinux:91: vmlinux.unstripped] Error 1
make[1]: *** [Makefile:1244: vmlinux] Error 2
make: *** [Makefile:248: __sub-make] Error 2
```

The LLVM IR looks like this:

```llvm
  %args1 = alloca [16 x i8], align 8
  %args = alloca [48 x i8], align 8
  %x = alloca [4 x i8], align 4
  call void @llvm.lifetime.start.p0(i64 4, ptr nonnull %x)
  store i32 0, ptr %x, align 4
  call void @llvm.lifetime.start.p0(i64 16, ptr nonnull %args1)
  store ptr %x, ptr %args1, align 8
  %_4.sroa.4.0..sroa_idx = getelementptr inbounds nuw i8, ptr %args1, i64 8
  store ptr @_RNvXs5_NtNtNtCs1h9PaebWwla_4core3fmt3num3implNtB9_7Display3fmt, ptr %_4.sroa.4.0..sroa_idx, align 8
  store ptr @alloc_eab5d04767146d7d9b93b60d28ef530a, ptr %args, align 8
  %0 = getelementptr inbounds nuw i8, ptr %args, i64 8
  store i64 1, ptr %0, align 8
  %1 = getelementptr inbounds nuw i8, ptr %args, i64 32
  store ptr null, ptr %1, align 8
  %2 = getelementptr inbounds nuw i8, ptr %args, i64 16
  store ptr %args1, ptr %2, align 8
  %3 = getelementptr inbounds nuw i8, ptr %args, i64 24
  store i64 1, ptr %3, align 8
; call kernel::print::call_printk
  call void @_RNvNtCsetEQUy9amV6_6kernel5print11call_printk(ptr noalias noundef nonnull readonly align 1 dereferenceable(10) @_RNvNtNtCsetEQUy9amV6_6kernel5print14format_strings4INFO, ptr noalias noundef nonnull readonly align 1 @alloc_9e327419e36c421bc1658af3b68806c2, i64 noundef 13, ptr noalias nocapture noundef nonnull align 8 dereferenceable(48) %args) #8
  call void @llvm.lifetime.end.p0(i64 16, ptr nonnull %args1)
  %_7 = load i32, ptr %x, align 4, !noundef !7
  ;     ^^^^^^^^^^^^^^^^ we load the value of `x` again after calling `call_printk`,
  ;                      even though it was a shared reference!
  %4 = icmp eq i32 %_7, 0
  br i1 %4, label %bb2, label %bb3, !prof !11

bb2:                                              ; preds = %start
  call void @llvm.lifetime.end.p0(i64 4, ptr nonnull %x)
  ret void

bb3:                                              ; preds = %start
  call void @rust_build_error(ptr noalias noundef nonnull readonly align 1 @alloc_7f598d51153ed56ec9aa560e64562fc0, i64 noundef 24, ptr noalias noundef nonnull readonly align 8 dereferenceable(24) @alloc_4255a8c4673dfae2bb72b50834aecf9e) #9
  unreachable
```

The MIR looks like this:

```text
let mut _0: ();
let _1: i32;
let mut _2: core::fmt::Arguments<'_>;
let mut _4: core::fmt::rt::Argument<'_>;
let _5: &[core::fmt::rt::Argument<'_>; 1];
let _6: ();
let mut _7: i32;
let _8: !;
scope 1 {
    debug x => _1;
    let _3: [core::fmt::rt::Argument<'_>; 1];
    scope 2 {
        debug args => _3;
        scope 9 (inlined core::fmt::rt::<impl Arguments<'_>>::new_v1::<1, 1>) {
            let mut _15: &[&str];
            let mut _16: &[core::fmt::rt::Argument<'_>];
        }
    }
    scope 3 {
        debug args => _2;
    }
    scope 4 (inlined core::fmt::rt::Argument::<'_>::new_display::<i32>) {
        let mut _9: core::fmt::rt::ArgumentType<'_>;
        let mut _10: core::ptr::NonNull<()>;
        let mut _11: for<'a, 'b> unsafe fn(core::ptr::NonNull<()>, &'a mut core::fmt::Formatter<'b>) -> core::result::Result<(), core::fmt::Error>;
        let _12: for<'a, 'b, 'c> fn(&'a i32, &'b mut core::fmt::Formatter<'c>) -> core::result::Result<(), core::fmt::Error>;
        scope 5 {
        }
        scope 6 (inlined NonNull::<i32>::from_ref) {
            let mut _13: *const i32;
        }
        scope 7 (inlined NonNull::<i32>::cast::<()>) {
            let mut _14: *const ();
            scope 8 (inlined NonNull::<i32>::as_ptr) {
            }
        }
    }
}

bb0: {
    StorageLive(_1);
    _1 = const 0_i32;
    StorageLive(_3);
    StorageLive(_4);
    StorageLive(_12);
    StorageLive(_13);
    StorageLive(_9);
    StorageLive(_10);
    _13 = &raw const _1;
    StorageLive(_14);
    _14 = copy _13 as *const () (PtrToPtr);
    _10 = NonNull::<()> { pointer: move _14 };
    StorageDead(_14);
    StorageLive(_11);
    _12 = <i32 as core::fmt::Display>::fmt as for<'a, 'b, 'c> fn(&'a i32, &'b mut core::fmt::Formatter<'c>) -> core::result::Result<(), core::fmt::Error> (PointerCoercion(ReifyFnPointer, Implicit));
    _11 = copy _12 as for<'a, 'b> unsafe fn(core::ptr::NonNull<()>, &'a mut core::fmt::Formatter<'b>) -> core::result::Result<(), core::fmt::Error> (Transmute);
    _9 = core::fmt::rt::ArgumentType::<'_>::Placeholder { value: move _10, formatter: move _11, _lifetime: const PhantomData::<&()> };
    StorageDead(_11);
    StorageDead(_10);
    _4 = core::fmt::rt::Argument::<'_> { ty: move _9 };
    StorageDead(_9);
    StorageDead(_13);
    StorageDead(_12);
    _3 = [move _4];
    StorageDead(_4);
    _5 = &_3;
    StorageLive(_15);
    _15 = const print_build_assert_test::promoted[0] as &[&str] (PointerCoercion(Unsize, Implicit));
    StorageLive(_16);
    _16 = copy _5 as &[core::fmt::rt::Argument<'_>] (PointerCoercion(Unsize, Implicit));
    _2 = Arguments::<'_> { pieces: move _15, fmt: const Option::<&[core::fmt::rt::Placeholder]>::None, args: move _16 };
    StorageDead(_16);
    StorageDead(_15);
    _6 = call_printk(const <static(DefId(2:3562 ~ kernel[a8a3]::print::format_strings::INFO))>, const __LOG_PREFIX, move _2) -> [return: bb1, unwind unreachable];
}

bb1: {
    StorageDead(_3);
    StorageLive(_7);
    _7 = copy _1;
    //   ^^^^^^^^ this is the problematic part, it shouldn't prompt a re-read of
    //            the location, but rather just know that the value is 0
    switchInt(move _7) -> [0: bb2, otherwise: bb3];
}

bb2: {
    StorageDead(_7);
    StorageDead(_1);
    return;
}

bb3: {
    StorageDead(_7);
    _8 = build_error(const "assertion failed: x == 0") -> unwind unreachable;
}
```

---
Cheers,
Benno

Powered by blists - more mailing lists

Powered by Openwall GNU/*/Linux Powered by OpenVZ