Skip to content

pcm/rawmidi/ump: fix stack overflow from timespec/time64 mismatch - #158

Merged
diwic merged 2 commits into
diwic:masterfrom
roderickvd:fix/timespec-time64-mismatch
Jul 31, 2026
Merged

pcm/rawmidi/ump: fix stack overflow from timespec/time64 mismatch#158
diwic merged 2 commits into
diwic:masterfrom
roderickvd:fix/timespec-time64-mismatch

Conversation

@roderickvd

@roderickvd roderickvd commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Leftover from our time64 work: when we fixed the type in alsa-sys (diwic/alsa-sys#19) we did not switch get_htstamp, get_trigger_htstamp, get_audio_htstamp, and the rawmidi/ump tread() functions to use alsa::timespec instead of libc::timespec.

Through cpal's Discord it was reported that when compile-time optimization was turned on, this still caused segfaults on 32-bit Raspberry Pi's. Here the 64-bit time_t still diverge (16 vs. 8 bytes) and the remaining 8 bytes probably did not get reused in debug builds.

I don't have access to my Linux environment, so I'm asking the Discord user to confirm this is working for him/her first.

These FFI calls are typed as snd_htimestamp_t (= alsa_sys::timespec),
which alsa-sys 0.6.0 widens to 16 bytes on 32-bit glibc targets with a
64-bit time_t. libc::timespec stays 8 bytes there, so the stack buffer
we passed by pointer was undersized: an 8-byte stack overflow on
affected targets (e.g. 32-bit Raspberry Pi OS).
@d3d9

d3d9 commented Jul 21, 2026

Copy link
Copy Markdown

unfortunately this still segfaults on release

running the cpal beep example (cpal 0.17.3 with the overflow fix applied, alsa @ 8b9a0c1, alsa-sys at 0.6)

Program received signal SIGSEGV, Segmentation fault.
0x00000000 in ?? ()
(gdb) bt
#0  0x00000000 in ?? ()
#1  0x0042c93c in <alsa::pcm::Status>::get_htstamp ()
#2  0x00000000 in ?? ()
Backtrace stopped: previous frame identical to this frame (corrupt stack?)

@roderickvd

Copy link
Copy Markdown
Contributor Author

Thanks for testing that so quickly. Before I chase this further: can you confirm you actually rebuilt against my patched alsa-rs branch (e.g. a git/path override in your Cargo.toml), not whatever cpal normally pulls in?

Also, could you run a full rebuild with cargo build -vv --release and grep the alsa-sys build script output for alsa_sys_time64? Specifically I want to know if cargo:rustc-cfg=alsa_sys_time64 shows up.

@d3d9

d3d9 commented Jul 21, 2026

Copy link
Copy Markdown

Thanks for testing that so quickly. Before I chase this further: can you confirm you actually rebuilt against my patched alsa-rs branch (e.g. a git/path override in your Cargo.toml), not whatever cpal normally pulls in?

I think this is the case, I added an override and it showed up with (/home/..../alsa-rs) during the build, additionally I manipulated the version to be 0.12.1 locally and it showed up as 0.12.1 as well.

Also, could you run a full rebuild with cargo build -vv --release and grep the alsa-sys build script output for alsa_sys_time64? Specifically I want to know if cargo:rustc-cfg=alsa_sys_time64 shows up.

Where would it show up? I ran your build command inside the alsa-rs directory, is that fine?

$ grep -Ir alsa_sys_time target/
target/release/build/alsa-sys-29b4a5a4a6b9e41e/output:cargo:rustc-check-cfg=cfg(alsa_sys_time64)
target/release/build/alsa-sys-29b4a5a4a6b9e41e/out/time64_probe.c:alsa_sys_time64=yes
target/release/build/alsa-sys-29b4a5a4a6b9e41e/out/time64_probe.c:alsa_sys_time64=no

@roderickvd

Copy link
Copy Markdown
Contributor Author

Try running something like this: grep rustc-cfg=alsa_sys_time64 target/release/build/alsa-sys-*/output

Second, just to rule it out, can you try running with RUST_MIN_STACK set to something large, like RUST_MIN_STACK=16777216?

@d3d9

d3d9 commented Jul 21, 2026

Copy link
Copy Markdown

I'm back to building the beep example now, the build isn't finished yet but it's past alsa-sys.

All I get with grep is

target/release/build/alsa-sys-5cb416f51eecc598/output:cargo:rustc-check-cfg=cfg(alsa_sys_time64)
target/release/build/alsa-sys-5cb416f51eecc598/out/time64_probe.c:alsa_sys_time64=yes
target/release/build/alsa-sys-5cb416f51eecc598/out/time64_probe.c:alsa_sys_time64=no

and grepping the console output of the build:

[alsa-sys 0.6.0] cargo:rustc-check-cfg=cfg(alsa_sys_time64)
     Running `CARGO=/home/admin/.rustup/toolchains/stable-arm-unknown-linux-gnueabihf/bin/cargo CARGO_CRATE_NAME=alsa_sys CARGO_MANIFEST_DIR=/home/admin/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/alsa-sys-0.6.0 CARGO_MANIFEST_PATH=/home/admin/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/alsa-sys-0.6.0/Cargo.toml CARGO_PKG_AUTHORS='Pierre Krieger <pierre.krieger1708@gmail.com>:David Henningsson <coding@diwic.se>' CARGO_PKG_DESCRIPTION='FFI bindings for the ALSA project (Advanced Linux Sound Architecture)' CARGO_PKG_HOMEPAGE='' CARGO_PKG_LICENSE=MIT CARGO_PKG_LICENSE_FILE='' CARGO_PKG_NAME=alsa-sys CARGO_PKG_README=README.md CARGO_PKG_REPOSITORY='https://github.com/diwic/alsa-sys' CARGO_PKG_RUST_VERSION='' CARGO_PKG_VERSION=0.6.0 CARGO_PKG_VERSION_MAJOR=0 CARGO_PKG_VERSION_MINOR=6 CARGO_PKG_VERSION_PATCH=0 CARGO_PKG_VERSION_PRE='' LD_LIBRARY_PATH='/home/admin/cpal/target/release/deps:/home/admin/.rustup/toolchains/stable-arm-unknown-linux-gnueabihf/lib' OUT_DIR=/home/admin/cpal/target/release/build/alsa-sys-5cb416f51eecc598/out /home/admin/.rustup/toolchains/stable-arm-unknown-linux-gnueabihf/bin/rustc --crate-name alsa_sys --edition=2021 /home/admin/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/alsa-sys-0.6.0/src/lib.rs --error-format=json --json=diagnostic-rendered-ansi,artifacts,future-incompat --crate-type lib --emit=dep-info,metadata,link -C opt-level=3 -C embed-bitcode=no --check-cfg 'cfg(docsrs,test)' --check-cfg 'cfg(feature, values("bindgen", "extra_traits", "use-bindgen"))' -C metadata=45e0f0638c4d79e3 -C extra-filename=-3e742b330971ee11 --out-dir /home/admin/cpal/target/release/deps -C strip=debuginfo -L dependency=/home/admin/cpal/target/release/deps --extern libc=/home/admin/cpal/target/release/deps/liblibc-e43152e9d00cb934.rmeta --cap-lints warn -L native=/usr/lib/arm-linux-gnueabihf -l asound --check-cfg 'cfg(alsa_sys_time64)'`

edit: nothing new after the build finished, and running with high RUST_MIN_STACK didn't help

@diwic

diwic commented Jul 22, 2026

Copy link
Copy Markdown
Owner

@roderickvd
Patch is correct, but timespec needs to be re-exported from the alsa crate (at root namespace, like Error is), and cpal needs to be patched to use that timespec.

Anything else should cause a compile time error and the fact that it does not is a sign something is off here, probably the time64 probe is not working like it should?

@d3d9

d3d9 commented Jul 22, 2026

Copy link
Copy Markdown

is __TIMESIZE the wrong place to check?

// $ grep define /usr/include/arm-linux-gnueabihf/bits/timesize.h
#define __TIMESIZE	32

@roderickvd

Copy link
Copy Markdown
Contributor Author

D'oh, thanks for pulling me out of my grogginess. Re-exported it here, and created RustAudio/cpal#1285 to stack with this one.

@d3d9 working now?

@roderickvd
roderickvd marked this pull request as ready for review July 22, 2026 13:41
@d3d9

d3d9 commented Jul 22, 2026

Copy link
Copy Markdown

I've tried to do the same locally already, replacing the occurrences of libc::timespec with a re-exported alsa::timespec, but the issue persists.

Do we have to fix the build check first, since it doesn't recognize the time as being 64 bit? The probe expansion contained alsa_sys_time64=no.

@roderickvd

Copy link
Copy Markdown
Contributor Author

I wonder that despite your __TIMESIZE being defined as 32, your libasound.so was compiled with _TIME_BITS=64. That would not be anything we could detect with a build-time probe using cc.

What's your output of dpkg -l | grep libasound?

@d3d9

d3d9 commented Jul 22, 2026

Copy link
Copy Markdown

What's your output of dpkg -l | grep libasound?

$ dpkg -l | grep libasound
ii  libasound2-data                      1.2.14-1+rpt1                            all          Configuration files and profiles for ALSA drivers
ii  libasound2-dev:armhf                 1.2.14-1+rpt1                            armhf        shared library for ALSA applications -- development files
ii  libasound2-plugins:armhf             1.2.12-2+b1                              armhf        ALSA library additional plugins
ii  libasound2t64:armhf                  1.2.14-1+rpt1                            armhf        shared library for ALSA applications

@diwic

diwic commented Jul 22, 2026

Copy link
Copy Markdown
Owner

For reference, this is from my /usr/include/alsa/global.h. Can't tell if the ifdef gets activated but I notice that there are no padding fields here.

#ifdef __GLIBC__
#if !defined(_POSIX_C_SOURCE) && !defined(_POSIX_SOURCE)
struct timeval {
        time_t          tv_sec;         /* seconds */
        long            tv_usec;        /* microseconds */
};

struct timespec {
        time_t          tv_sec;         /* seconds */
        long            tv_nsec;        /* nanoseconds */
};
#endif
#endif

/** Timestamp */
typedef struct timeval snd_timestamp_t;
/** Hi-res timestamp */
typedef struct timespec snd_htimestamp_t;

@roderickvd

roderickvd commented Jul 22, 2026

Copy link
Copy Markdown
Contributor Author

This confirms that the current build-time probe is not robust enough. @d3d9's dpkg output shows the t64 variant of libasound2 while his __TIMESIZE is 32. @diwic, your definitions are different from what I had on my Linux box. that's ALSA's own redeclaration, not glibc's struct. Still the same time_t though, which is 8 bytes wide under time64. On ARM, an 8-byte member gets 8-byte struct alignment, so the compiler adds the padding automatically. On i386, 8-byte types are only 4-byte aligned inside a struct, which is presumably why glibc's own definition bothers with the explicit padding in the first place.

Sorry for iterating over this, this time-stuff is hard and apparently distro-dependent.

What we could do instead is stop guessing from headers or package metadata and just ask the installed libasound.so itself. snd_pcm_status_get_htstamp doesn't need a real device, so at build time we could run a tiny probe: poison a 16-byte buffer, call the getter, see whether bytes 8-15 get touched. Touched means the library's internal timespec is 16 bytes, untouched means 8. Only downside is it needs to run on the target, so for cross builds we would need to add a RUSTFLAGS="--cfg alsa_sys_time64" override.

@roderickvd

Copy link
Copy Markdown
Contributor Author

Separately we should then also pin the Rust struct's alignment explicitly with #[repr(C, align(8))]. Doesn't replace the probe though.

Man, what did I get us into? 😅 (myself anyway)

@diwic

diwic commented Jul 23, 2026

Copy link
Copy Markdown
Owner

@roderickvd

snd_pcm_status_get_htstamp doesn't need a real device, so at build time we could run a tiny probe: poison a 16-byte buffer, call the getter, see whether bytes 8-15 get touched..

Yeah, but let's do that probe runtime (at program startup) instead. Or at snd_pcm/rawmidi_open time.

@d3d9

d3d9 commented Jul 23, 2026

Copy link
Copy Markdown

what if we changed the build time probe to check for the combination of __USE_TIME_BITS64 and __TIMESIZE == 32 like it seems to be the case on my end?

@d3d9

d3d9 commented Jul 23, 2026

Copy link
Copy Markdown

Or, I guess not just that combination. but e.g. __USE_TIME_BITS64 || __TIMESIZE == 64, could that work to catch both the special case as well as the normal 64bit case?

The fixes themselves seem to work now by the way, after making sure alsa_sys_time64=yes gets emitted by changing the probe logic, I could confirm the cpal beep example works both in debug and release.

b0bbywan added a commit to b0bbywan/alsa-sys that referenced this pull request Jul 26, 2026
__TIMESIZE reports the port's default time_t width, so it still reads
32 on targets that gained a 64-bit time_t through Debian's time64
transition (_TIME_BITS=64 in the toolchain, e.g. armhf on trixie).
On those targets the probe from diwic#19 does not emit alsa_sys_time64,
libc::timespec stays 8 bytes, and libasound writes 16-byte timespecs
past it (diwic/alsa-rs#158, RustAudio/cpal#1285).

Measure the size directly instead: try to compile a _Static_assert
that sizeof(struct timespec) == 16. The snippet is compiled but never
run, so cross compiling against the target's sysroot keeps working.
A control compile first checks that <time.h> yields a usable timespec,
so a broken toolchain produces a warning and the safe 32-bit fallback
instead of being misread as a pre-transition target.

Verified on Raspberry Pi OS armhf: with this change the cfg is emitted
on trixie (16-byte timespec) and still not on bookworm (8-byte
timespec). 64-bit glibc and musl targets return early as before.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@roderickvd

Copy link
Copy Markdown
Contributor Author

@d3d9 you're saying that changing the probe to __USE_TIME_BITS64 || __TIMESIZE == 64 fixes it for your case?
@diwic although that wouldn't cover it fully, we could ship that now as it doesn't touch any type or signature.
Or to immediately take the runtime-probe route, to map that out:

// FFI-facing: opaque and enough bytes+alignment for any ABI layout:
// - 32-bit legacy (i32, i32)
// - 32-bit time64 (i64, i32+pad)
// - 64-bit native (i64, i64)
#[repr(C, align(8))]
#[derive(Copy, Clone, Debug, Default)]
struct RawTimespec([u8; 16]);

// Caller-facing: same type on every target
#[derive(Copy, Clone, Debug, Default)]
pub struct timespec {
    pub tv_sec: i64,
    pub tv_nsec: i32,
}

We'd then resolve the real layout once, at snd_pcm_open/rawmidi_open time and cache it in a OnceLock<bool>. This would need 1.70, that OK?

Once the layout is resolved at runtime, should get_htstamp and friends keep returning this timespec and stay close to the C API, or would it make sense to return a Duration as Rust idiom?

@d3d9

d3d9 commented Jul 26, 2026

Copy link
Copy Markdown

you're saying that changing the probe to __USE_TIME_BITS64 || __TIMESIZE == 64 fixes it for your case?

Exactly, it does. Technically __USE_TIME_BITS64 could be enough, but when researching I saw there might have been old versions where it was only set for the special cases and not for all cases with 64 bits, so just to be sure I added the || __TIMESIZE == 64 as well. Besides that I'm not sure about other things to be careful about (having to use defined(...) or something) so maybe it should be changed a little.

@diwic

diwic commented Jul 26, 2026

Copy link
Copy Markdown
Owner

@roderickvd @d3d9 Does this fix the issue? diwic/alsa-sys#20

@d3d9

d3d9 commented Jul 26, 2026

Copy link
Copy Markdown

Does this fix the issue? diwic/alsa-sys#20

This also works on my end

diwic pushed a commit to diwic/alsa-sys that referenced this pull request Jul 29, 2026
* fix: measure sizeof(struct timespec) instead of reading __TIMESIZE

__TIMESIZE reports the port's default time_t width, so it still reads
32 on targets that gained a 64-bit time_t through Debian's time64
transition (_TIME_BITS=64 in the toolchain, e.g. armhf on trixie).
On those targets the probe from #19 does not emit alsa_sys_time64,
libc::timespec stays 8 bytes, and libasound writes 16-byte timespecs
past it (diwic/alsa-rs#158, RustAudio/cpal#1285).

Measure the size directly instead: try to compile a _Static_assert
that sizeof(struct timespec) == 16. The snippet is compiled but never
run, so cross compiling against the target's sysroot keeps working.
A control compile first checks that <time.h> yields a usable timespec,
so a broken toolchain produces a warning and the safe 32-bit fallback
instead of being misread as a pre-transition target.

Verified on Raspberry Pi OS armhf: with this change the cfg is emitted
on trixie (16-byte timespec) and still not on bookworm (8-byte
timespec). 64-bit glibc and musl targets return early as before.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: probe snd_htimestamp_t via the ALSA headers and fail on probe errors

Review feedback from #20:

- Measure sizeof(snd_htimestamp_t) from <alsa/asoundlib.h> instead of
  struct timespec from <time.h>: it is the exact type libasound writes
  through. The probe now runs after pkg-config so it can reuse the
  reported include paths. The alsa/ prefix is required since alsa-lib
  1.2.5, when alsa.pc stopped adding -I${includedir}/alsa.

- If the control snippet does not compile, fail the build with an
  explicit error instead of assuming a 32-bit time_t: guessing 8 can
  segfault and guessing 16 yields garbage timestamps, so there is no
  safe fallback to pick silently.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
@roderickvd

Copy link
Copy Markdown
Contributor Author

When you release alsa-sys 0.6.1 would you release this one alongside? Would you bump SemVer for it? Asking in order to coordinate with a next cpal release, so we close the entire loop on this time_t thing 💪

@diwic
diwic merged commit 72acbe2 into diwic:master Jul 31, 2026
1 check passed
@diwic

diwic commented Jul 31, 2026

Copy link
Copy Markdown
Owner

When you release alsa-sys 0.6.1 would you release this one alongside? Would you bump SemVer for it? Asking in order to coordinate with a next cpal release, so we close the entire loop on this time_t thing 💪

@roderickvd Released now. I might not have time to do much work with this in the coming weeks, so in case this does not work either, let me know ASAP.

@roderickvd

Copy link
Copy Markdown
Contributor Author

Thanks, @diwic. I'll wire up the changes a.s.a.p. on the develop branch of cpal, which is carrying alsa-rs 0.12. For cpal users reading along: cpal 0.18 is bound to alsa-rs 0.11 and will not upgrade to 0.12 for SemVer-reasons.

@roderickvd
roderickvd deleted the fix/timespec-time64-mismatch branch July 31, 2026 14:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants