andelf / rust-sdl2_ttf

Rust bindings for sdl2_ttf.
MIT License
26 stars 23 forks source link

Reading a font from RWops makes rendering it with .solid() segfault #43

Closed icefoxen closed 7 years ago

icefoxen commented 7 years ago

On Linux Debian 8, using Rust stable and latest sdl2_everything

extern crate sdl2;
extern crate sdl2_ttf;

use sdl2::pixels::Color;
use sdl2::rwops;

use std::path::Path;
use std::io::Read;
use std::fs::File;

pub fn make_text() -> sdl2::render::Texture {
    let sdl_context = sdl2::init().unwrap();
    let ttf_context = sdl2_ttf::init().unwrap();
    let video = sdl_context.video().unwrap();
    let window = video.window("kerblammo", 800, 600)
                               .position_centered()
                               .opengl()
                               .build().unwrap();

    let renderer = window.renderer()
        .accelerated()
        .build().unwrap();

    // Load font
    let path = Path::new("DejaVuSerif.ttf");
    let size = 24;
    let mut buffer: Vec<u8> = Vec::new();
    let mut file = File::open(path).unwrap();
    let _ = file.read_to_end(&mut buffer).unwrap();
    let rwops = rwops::RWops::from_bytes(&buffer).unwrap();

    let ttf_font = ttf_context.load_font_from_rwops(rwops, size).unwrap();
    // Works fine when I create the font with this instead
    //let ttf_font = ttf_context.load_font(path, 24).unwrap();

    let surf = ttf_font.render("Hello world")
        // If I change .solid() to .blended() it works fine.
        .solid(Color::RGB(255,255,255))
        .unwrap();
    // SEGFAULTS on create_texture_from_surface
    //  But only when using .solid() to make the surface,
    // not .blended()!
    // Does it have anything to do with the font lifetime thing???
    // It shouldn't, 'cause we still have the buffer we read into here!
    let texture = renderer.create_texture_from_surface(surf).unwrap();
    texture

}

pub fn main() {
    println!("Loading");
    let t = make_text();
    println!("Got texture");
}

Crashes with the message:

error: Process didn't exit successfully: /home/... (signal: 11, SIGSEGV: invalid memory reference)

icefoxen commented 7 years ago

Happens in both --debug and --release mode.

For your convenience, the font I'm using is here: https://alopex.li/temp/DejaVuSerif.ttf

andelf commented 7 years ago

This may be related to #29. The code runs OK on my MacBook Pro. I'll try fix this. :(

icefoxen commented 7 years ago

Half-assedly digging through the generated assembly, it doesn't look like the Vec<> is getting dropped early, but don't take my word for it. Inserting a statement to print buffer at the end of the function to try to ensure it gets held on to long enough doesn't change anything. Using .shaded() instead of .solid() works fine. Printing out the contents of buffer shows no difference between loading with RWops and loading from a file, and it is the exact same length as the file so it doesn't seem to be overflowing somehow.

Sorry for dropping such a weird one on you. I honestly wonder a little if it's a bug in SDL2_ttf, as unlikely as that seems; I might try coding up an equivalent in C and see if that has problems.

Cobrand commented 7 years ago

I don't have a segfault either. Have you tried another font ?

If you could run your debug program under gdb, let it segfault and then get the backtrace (with "bt"), we might be able to see where the segfault occurs.

Also, on which architecture are you running ? I have a linux 64bits, and I don't encounter this problem.

icefoxen commented 7 years ago

Tried a couple different fonts, same result. Both DejaVu and Droid fonts. I'm running on x86_64. This now happens on .blended() instead of .solid(); there's some sort of memory screwiness with both of them.

Tried using Debian testing (libsdl2_ttf version 2.0.14+dfsg1-1), same result.

Got a backtrace, thank you very much for the pointer on how to do it. Actually using a debugger isn't a skill I've seriously used in years...

Thread 1 "astroblasto" received signal SIGSEGV, Segmentation fault.
0x00007ffff775cdd6 in RWread (stream=<optimized out>, offset=<optimized out>, 
    buffer=0x0, count=0) at SDL_ttf.c:361
361     SDL_RWseek( src, (int)offset, RW_SEEK_SET );
(gdb) bt
#0  0x00007ffff775cdd6 in RWread (stream=<optimized out>, offset=<optimized out>, 
    buffer=0x0, count=0) at SDL_ttf.c:361
#1  0x00007ffff4e715cb in FT_Stream_Seek ()
   from /usr/lib/x86_64-linux-gnu/libfreetype.so.6
#2  0x00007ffff4e89321 in ?? () from /usr/lib/x86_64-linux-gnu/libfreetype.so.6
#3  0x00007ffff4e89e0b in ?? () from /usr/lib/x86_64-linux-gnu/libfreetype.so.6
#4  0x00007ffff4e7315b in FT_Load_Glyph ()
   from /usr/lib/x86_64-linux-gnu/libfreetype.so.6
#5  0x00007ffff775d25e in Load_Glyph (want=18, cached=<optimized out>, ch=114, 
    font=0x7fffea9d0840) at SDL_ttf.c:602
#6  Find_Glyph (font=font@entry=0x7fffea9d0840, ch=114, want=want@entry=18)
    at SDL_ttf.c:910
#7  0x00007ffff775f5c6 in TTF_RenderUTF8_Blended (font=0x7fffea9d0840, 
    text=0x7fffed23b3ec "e", fg=...) at SDL_ttf.c:1757
#8  0x00005555555b0a22 in {{inlined-root}}::blended<sdl2::pixels::Color> (self=..., 
    color=...)
    at /home/icefox/.cargo/registry/src/github.com-1ecc6299db9ec823/sdl2_ttf-0.24.0/src/sdl2_ttf/font.rs:212
#9  0x00005555555be6a5 in ggez::graphics::{{impl}}::new (context=0x7fffffffd6d0, 
    text=..., font=0x7fffffffc038) at /home/icefox/src/ggez/src/graphics.rs:454
#10 0x000055555556f975 in astroblasto::{{impl}}::load (ctx=0x7fffffffd6d0, 
    conf=0x7fffffffd820) at /home/icefox/src/ggez/examples/astroblasto.rs:425
#11 0x0000555555563f66 in {{inlined-root}}::new<astroblasto::MainState> (id=..., 
    default_config=...) at /home/icefox/src/ggez/src/game.rs:128
#12 0x00005555555712b0 in astroblasto::main ()
    at /home/icefox/src/ggez/examples/astroblasto.rs:550
#13 0x0000555555659157 in __rust_maybe_catch_panic ()
#14 0x00005555556500e3 in std::rt::lang_start::h53bf99b0829cc03c ()
#15 0x0000555555571894 in main ()

So it looks like the error is happening when the RWops tries to actually seek into the buffer. Full function in question:

static unsigned long RWread(
    FT_Stream stream,
    unsigned long offset,
    unsigned char* buffer,
    unsigned long count
)
{
    SDL_RWops *src;

    src = (SDL_RWops *)stream->descriptor.pointer;
    SDL_RWseek( src, (int)offset, RW_SEEK_SET );
    if ( count == 0 ) {
        return 0;
    }
    return (unsigned long)SDL_RWread( src, buffer, 1, (int)count );
}

Variables involved:

(gdb) info locals
src = 0x7fffed24cfe0
(gdb) info args
stream = <optimized out>
offset = <optimized out>
buffer = 0x0
count = 0

So it's getting a null buffer, which is weird. It's reading 0 bytes from it in this case, which is a little odd but ideally shouldn't crash. (Wish I could see the offset, need to make a custom build for that.) *src appears to point to invalid memory though (if I'm interpreting this right), which is probably the real culprit.

(gdb) x/xg 0x7fffed24cfe0
0x7fffed24cfe0: 0x002cb6f3f3b62c00
(gdb) x/xg 0x002cb6f3f3b62c00
0x2cb6f3f3b62c00:   Cannot access memory at address 0x2cb6f3f3b62c00

info proc mappings also shows that memory as unmapped.

icefoxen commented 7 years ago

Discovered in SDL_ttf.c line 97:

struct _TTF_Font {
     ...
    /* We are responsible for closing the font stream */
    SDL_RWops *src;
    ...
}

So the TTF_Font struct will hold on to the RWops! And hence, in this case, the underlying buffer. The font data apears to be read lazily from it as SDL_ttf needs it, instead of loading the whole thing into memory and then using that. Not sure why the buffer and rwops don't exist in that scope in the example I posted, but even if this isn't the case, it's obvious that having the Font able to outlive the RWops is Bad(tm).

SDL_ttf does not quite document explicitly that this dependency exists.

andelf commented 7 years ago

May be we can make Font owns a RWops?

Cobrand commented 7 years ago

Having having a look at your backtrace, I noticed that I had this same error on windows gcc 64bits. I still don't have this segfault on linux 64bits ... I would love to fix that, but I must say without a quick way to know if this fixed or not, it will be tremendously painful.

I'm starting to think it might be Rust itself that doesn't allow me to see the segfault, (since our architecture is the same, and our SDL2_ttf libs are the same, the only thing that is changed is how this program is compiled). I'm currently on some nightly, but could you @icefoxen tell me with which version of rustc you are compiling ? It might be a clue.

Now on to the problem itself, I will try to dig that and maybe come with a PR of some kind, but again it's going to be cumbersome without a way to test this quickly.

Cobrand commented 7 years ago

I started having a look, and as andelf said the ovious solution would be to store the RWops right in the Font. Problem is that our Font would become a Font<'a>, and with that we are sure to break major stuff. Another problem we have is that sometimes the RWops is handled directly by SDL and not by rust-sdl2_ttf, so we sometimes don't have to store the RWops ourselves. It would look like this :

pub struct Font<'a> {
    raw: *const ffi::TTF_Font,
    // RWops is only stored here because it must not outlive
    // the Font struct, and this RWops should not be used by
    // anything else
    // None means that the RWops is handled by SDL itself,
    // and Some(rwops) means that the RWops is handled by the Rust
    // side
    #[allow(dead_code)]
    rwops:Option<RWops<'a>>
}

But with the lifetimes, it's kind of messy. Another solution would be to make Font a trait and use FontWithRWops and FontWithoutRWops or something of the like.

Another solution yet is to handle the RWops ourselves every time, meaning we change the contents of load_font(&self, path: &Path, point_size:u16) to generate a RWops ourselves, then use internal_load_font_from_ll and store our RWops.

Also note that I got rid of the owner: bool, because it is truly useless as of right now : in every case owned is true, you can check it for yourself.

icefoxen commented 7 years ago

@Cobrand: I am building on Rust stable (1.12.1), haven't tried on nightly. However another source of variability is how the SDL_ttf C library was built; the version I'm using was built with GCC 6.2. Different compiler versions might organize or optimize things differently which will make the bug more or less likely to trigger.

Something that might be worth observing is that, internally, SDL_ttf always uses a rwops to load a font:

TTF_Font* TTF_OpenFont( const char *file, int ptsize )
{
    return TTF_OpenFontIndex(file, ptsize, 0);
}

TTF_Font* TTF_OpenFontIndex( const char *file, int ptsize, long index )
{
    SDL_RWops *rw = SDL_RWFromFile(file, "rb");
    if ( rw == NULL ) {
        return NULL;
    }
    return TTF_OpenFontIndexRW(rw, 1, ptsize, index);
}

We could replicate this behavior in Rust instead of wrapping these C functions directly, which would make sure that we always have control over the RWops that SDL_ttf stores internally; either the user creates and passes one that is then owned by the Font, or if not we create our own one for the Font to own.