4

Is there a guaranteed way to assert that a given raw pointer is aligned to some alignment value?

I looked at pointer's aligned_offset function, but the docs state that it is permissible for it to give false negatives (always return usize::MAX), and that correctness cannot depend on it.

I don't want to fiddle with the alignment at all, I just want to write an assertion that will panic if the pointer is unaligned. My motivation is that when using certain low-level CPU intrinsics passing a pointer not aligned to some boundary causes a CPU error, and I'd much rather get a Rust panic message pointing where the bug causing it is located than a SEGFAULT.

An example assertion (not correct according to aligned_offset docs):

#[repr(align(64))]
struct A64(u8);

#[repr(align(32))]
struct A32(u8);

#[repr(align(8))]
struct A8(u8);

fn main() {
    let a64 = [A64(0)];
    let a32 = [A32(0)];
    let a8 = [A8(0), A8(0)];
    
    println!("Assert for 64 should pass...");
    assert_alignment(&a64);
    println!("Assert for 32 should pass...");
    assert_alignment(&a32);
    
    println!("Assert for 8, one of the following should fail:");
    println!("- full array");
    assert_alignment(&a8);
    println!("- offset by 8");
    assert_alignment(&a8[1..]);
}

fn assert_alignment<T>(a: &[T]) {
    let ptr = a.as_ptr();
    
    assert_eq!(ptr.align_offset(32), 0);
}

Rust playground.

V0ldek
  • 9,623
  • 1
  • 26
  • 57
  • 3
    I'm not an expert on alignment, but couldn't you just cast the pointer to a `usize` and take the modulus of the desired byte alignment? Something like `(ptr as usize) % alignment == 0`? – JMAA Apr 22 '22 at 19:49
  • 1
    @JMAA I doubt that it'd be that simple, since the underlying pointer value is architecture specific. 1. This answer: https://stackoverflow.com/a/57970536/4646738 points to an architecture that doesn't work like that, 2. If it was that easy then I'd expect the standard function to just do that, the "can be incorrect" caveat wouldn't be not needed. – V0ldek Apr 22 '22 at 21:13
  • Fair point, but do you need to support architectures that don't represent pointers in a way that would work like this? The question you linked suggests that the only way to make a version that worked for every conceivable architecture would be to write architecture-specific code for every special case. Or, you just accept that at least x86, amd64, and aarch64 (AFAIK) all work like this and that's good enough – JMAA Apr 23 '22 at 10:00
  • I would expect also the std function version to work properly on all such architectures, so you could just bug an extra check to see if it returns `usize::MAX` and fail when it does if you're extra paranoid. But really, unless you're targeting the 8086 and looking at >16 bit alignment at the same time, this is probably a non-issue – JMAA Apr 23 '22 at 10:05
  • @JMAA I'm trying to be architecture-agnostic as much as I can for my current usage. Although, even if I decide to go with a half-solution, the question of whether this can be done in a general way still nags me. You _can_ demand certain alignment from the memory allocator, so intuitively (for me) there should be some way of asking the allocator to tell you the alignment. – V0ldek Apr 23 '22 at 11:02
  • I get it, but at a certain point you have to cut your losses. Unless you're targeting pre-32 bit processors or particularly weird microcontrollers or DSPs there's literally no reason to worry further. I highly doubt there is even a target rust supports where this wouldn't work. – JMAA Apr 23 '22 at 17:46

1 Answers1

2

Just to satisfy my own neuroses, I went and checked the the source of ptr::align_offset.

There's a lot of careful work around edge cases (e.g. const-evaluated it always returns usize::MAX, similarly for a pointer to a zero-sized type, and it panics if alignment is not a power of 2). But the crux of the implementation, for your purposes, is here: it takes (ptr as usize) % alignment == 0 to check if it's aligned.

Edit: This PR is adding a ptr::is_aligned_to function, which is much more readable and also safer and better reviewed than simply (ptr as usize) % alginment == 0 (though the core of it is still that logic).

There's then some more complexity to calculate the exact offset (which may not be possible), but that's not relevant for this question.

Therefore:

assert_eq!(ptr.align_offset(alignment), 0);

should be plenty for your assertion.

Incidentally, this proves that the current rust standard library cannot target anything that does not represent pointers as simple numerical addresses, otherwise this function would not work. In the unlikely situation that the rust standard library is ported to the Intel 8086 or some weird DSP that doesn't represent pointers in the expected way, this function would have to change. But really, do you care for that hypothetical that much?

JMAA
  • 1,730
  • 13
  • 24
  • 1
    And that is the explanation I think. I would guess that on such an esoteric architecture there wouldn't necessarily be a sensible implementation, it would just default to returning `usize::MAX` always and still be a valid Rust implementation for that architecture. – V0ldek Apr 23 '22 at 19:45
  • Yeah, I imagine there's no guarantee that `is_aligned` is even possible on every architecture. Best to just let the std lib developers deal with any arch-specific code that might be required though. – JMAA Apr 23 '22 at 19:50
  • 1
    There is a pending PR for `is_aligned()` and `is_aligned_to()`: https://github.com/rust-lang/rust/pull/95643. – Chayim Friedman Apr 25 '22 at 23:33
  • 1
    By itself, the fact that std doesn't implement something correctly for some platform doesn't mean Rust completely not supports this platform, only that it is in a tier std is not (completely, at least) supported. In this case there is also a difference from C: The C99 standard states that (§6.3.2.3 Pointers) "Any pointer type may be converted to an integer type. Except as previously specified, **the result is implementation-defined**. If the result cannot be represented in the integer type, the behavior is undefined. The result need not be in the range of values of any integer type."... – Chayim Friedman Apr 25 '22 at 23:40
  • 1
    ...while the Rust reference states that (https://doc.rust-lang.org/stable/reference/expressions/operator-expr.html#pointer-to-address-cast) "Casting from a raw pointer to an integer **produces the machine address of the referenced memory**. If the integer type is smaller than the pointer type, the address may be truncated; using usize avoids this." So the standard library is correct, because this requires the machine address - I read that as actual address, after segmentation. – Chayim Friedman Apr 25 '22 at 23:41
  • 1
    This definition is also compatible with the 8086 (although casts will require more operations to adjust the address). Though it may not be what you expect: it will require `usize` to be 32 bits to match every address. This is also necessary by https://doc.rust-lang.org/stable/reference/type-layout.html#primitive-data-layout, "`usize` and `isize` have a size big enough to contain every address on the target platform." But it is well-known Rust does not support segmented architectures well, and some recent work (strict provenance) may improve this situation. So those definitions may change. – Chayim Friedman Apr 25 '22 at 23:45