Non-constant formatting args appear to cause panic in #![no_std] environment

I've been experimenting with kernel-level programming in Rust, and I'm running into a weird issue with printing output over a serial port. Using the RISC-V SBI, I implemented a writer for the SBI debug console as follows:

struct SbiDebugConsole;

impl fmt::Write for SbiDebugConsole {
    fn write_str(&mut self, s: &str) -> fmt::Result {
        // We need to convert the virtual address at which `s` is located into a physical address which we can pass to
        // the SBI.
        let executable_address_response = EXECUTABLE_ADDRESS_REQUEST.get_response().unwrap();
        let s_addr = s.as_ptr().addr()
                .wrapping_sub(executable_address_response.virtual_base() as usize)
                .wrapping_add(executable_address_response.physical_base() as usize);

        // Use assembly to directly call up to the SBI.
        let (error, value): (usize, usize);
        unsafe {
            asm!(
            "ecall",
            in("a7") 0x4442434e,
            in("a6") 0,
            inlateout("a0") s.len() => error,
            inlateout("a1") s_addr => value,
            in("a2") 0,
                );
        }

        if error != 0 || value != s.len() {
            Err(fmt::Error)
        } else {
            Ok(())
        }
    }
}

Using this implementation, I can do things like writeln!(&mut SbiDebugConsole, "foo");. The problem, however, comes when I try to pass non-constant formatting arguments to writeln!. For instance, if I write

writeln!(&mut SbiDebugConsole, "foo");
writeln!(&mut SbiDebugConsole, "num: {}", 0xdeadbeef);
writeln!(&mut SbiDebugConsole, "bar");

everything works as expected, and all three lines are printed. If, however, I instead write

writeln!(&mut SbiDebugConsole, "foo");
let i = 0xdeadbeef;
writeln!(&mut SbiDebugConsole, "num: {}", i);
writeln!(&mut SbiDebugConsole, "bar");

my program appears to panic part-way through, and the output through the serial port reads only

foo
num: 

with the number and the final string bar both absent. Here is a link to the project of which this code is a part, in case it's helpful. I have stepped through the code with GDB, and my Write implementation does not appear to be panicking, so I can only assume that some code in core is panicking somewhere.

Looks like a reordering issue, that is, the assembly block having no rights to read from the address.

Could you try let s_addr = s.as_ptr().expose_provenance() to see if it works any better?

Replacing .addr() with .expose_provenance() makes no difference, I'm afraid. Read/write rights for the address shouldn't be an issue, since ecall passes control up to the SBI, which runs in ring 0.

I should've clarified that I mean "Rust AM rights (provenance) to read from a pointer".

1.84.0 · Source

pub fn addr(self) -> usize

Gets the “address” portion of the pointer.

This is similar to self as usize, except that the provenance of the pointer is discarded and not exposed. This means that casting the returned address back to a pointer yields a pointer without provenance, which is undefined behavior to dereference. To properly restore the lost information and obtain a dereferenceable pointer, use with_addr or map_addr.

If using those APIs is not possible because there is no way to preserve a pointer with the required provenance, then Strict Provenance might not be for you. Use pointer-integer casts or expose_provenance and with_exposed_provenance instead. However, note that this makes your code less portable and less amenable to tools that check for compliance with the Rust memory model.

...

As of now, your code does not allow the assembly block to read the string (the contents might not even be filled yet, if only pointer and length were determined).

If I'm understanding correctly, are you saying that I need to replace the line

let s_addr = s.as_ptr().addr()
                .wrapping_sub(executable_address_response.virtual_base() as usize)
                .wrapping_add(executable_address_response.physical_base() as usize);

with something like

let s_addr = s.as_ptr().expose_provenance()
                .wrapping_sub(executable_address_response.virtual_base() as usize)
                .wrapping_add(executable_address_response.physical_base() as usize);
let s_ptr = ptr::with_exposed_provenance::<*const u8>(s_addr);

and then pass s_ptr in the assembly block in place of s_addr to ensure that the provenance of the pointer isn't lost? If so, I've tried doing this, and I'm afraid that it still doesn't seem to work.

EDIT: Or, looking more closely, by using something like ptr::map_addr to avoid directly exposing the provenance in code. This, however, still doesn't seem to work.

Got it, then it must be something else... could runtime-allocated strings have some address which is NOT obtained by subtracting .virtual_base() and adding .physical_base()? Also, do you have enough stack size for number formatting?

P.S. Asm's access kinda contains with_exposed_provenance IIRC so there's no need to construct physical "pointer", passing the address would be fine.

I'm pretty sure that I have enough stack size for number formatting. Just in case, I requested a larger stack from the bootloader, and nothing seems to have changed. I think that the most likely issue is that this isn't the correct way to compute the physical address of a runtime-allocated string. I'm using the limine crate for bootloader bindings, and the docs for the ExecutableAddressResponse state that it "can be used to convert a virtual address within the executable to a physical address." But I assumed that even runtime-allocated strings would be allocated within the .data section of the executable. I haven't got any kind of allocator, so it's not like there's even a heap to allocate them to. I might also need to ask around in the Limine forums to see if I've misunderstood the protocol.

The Display implementations for integers uses a buffer that's on the stack.

Oh!!! The issue is one of address translation then. If the string is some &'static str or whatever, stored in the .data segment, then I perform a translation according using the ExecutableAddressResponse, but if it's stored in the stack then I need to use an HhdmResponse instead to get the offset of the virtual memory mapping. I did some testing and have confirmed that this is the issue. That does still leave me in a somewhat awkward position, since I assume that there's no way within Rust to determine whether the string whose address I'm trying to translate is statically initialised in the .data segment or on the stack.

You could just read satp and walk the page table.

I had the same issue in my kernel when I had stacks that were not identity-mapped and walking the page tables is how I fixed it.

Edit: I don't know much about limine but looking at the docs you might alternatively be able to check the MemoryMapResponse. If the physical address (as translated by ExecutableAddressResponse) is within a memory map entry with EntryType::EXECUTABLE_AND_MODULES then it's in .data and otherwise assume it's in the stack? I think just walking the page table would be cleaner even if this works tho.

I did think about using the MemoryMapResponse, but according to the Limine protocol docs, " LIMINE_MEMMAP_EXECUTABLE_AND_MODULES entries are meant to have an illustrative purpose only, and are not authoritative sources to be used as a means to find the addresses of the executable or modules." For that matter, it doesn't clarify whether or not the stack is located within one of these LIMINE_MEMMAP_EXECUTABLE_AND_MODULES entries. I think that you're right, and that the correct way to do this translation is to read satp and walk the page table.

EDIT: Another option, which is somehow only just occurring to me now, is to just use the sbi_debug_console_write_byte function as declared by the SBI. You can just iterate over the string and write the bytes out one-by-one. It's less efficient, and I'll still want to set up the page table traversal eventually, but this works for now, and it saves me from trying to implement page table traversal without any debug output.