Skip to content

[DEV BUG] SIGSEGV when preset contains commented-out sampler declaration #958

@diamond-lizard

Description

@diamond-lizard

Please confirm the following points:

  • This report is NOT about the Android apps in the Play Store
  • I have searched the project page to check if the issue was already reported

Affected Project

libprojectM (including the playlist library)

Affected Version

4.1.6

Operating Systems and Architectures

Don't know, other or not relevant

Build Tools

Compiler: GNU GCC, Build Tool: GNU Make

Additional Project, OS and Toolset Details

  • Void Linux
  • Kernel 6.12.66_1
  • g++ (GCC) 14.2.1 20250405
  • GNU Make 4.4.1
  • SDL2: 2.32.10
  • libpulse: 16.1
  • OpenGL: 4.6 (Compatibility Profile) Mesa 25.3.3

Type of Defect

Crash (unhandled exceptions, segmentation faults)

Log Output

Program terminated with signal SIGSEGV, Segmentation fault.
#0  0x00007fdb17fee750 in libprojectM::Renderer::Texture::Empty() const ()
#1  0x00007fdb17fb5c75 in libprojectM::MilkdropPreset::MilkdropShader::LoadVariables(...)
#2  0x00007fdb17fadbcd in libprojectM::MilkdropPreset::FinalComposite::Draw(...)
#3  0x00007fdb17fb07e1 in libprojectM::MilkdropPreset::MilkdropPreset::RenderFrame(...)
#4  0x00007fdb18060524 in libprojectM::ProjectM::RenderFrame()

Describe the Issue

libprojectM crashes with SIGSEGV when loading a preset that contains a commented-out sampler declaration in its composite shader. The sampler declaration parser does not respect comment syntax (// or /* */), causing it to attempt loading a texture that doesn't exist.

The crash is triggered by a preset from the official presets-cream-of-the-crop repository, so this affects end users.

Steps to Reproduce

  1. Download the preset: suksma - gapes to spelunkcide - dada hate art isn't art.milk

  2. Load and render the preset using any libprojectM application with no random textures configured (i.e., no texture search paths set, or empty texture directories). If random textures are available, the bug may be masked because GetRandomTexture() succeeds.

  3. The application crashes with SIGSEGV on the first frame render

Backtrace

#0  libprojectM::Renderer::Texture::Empty() const ()
#1  libprojectM::MilkdropPreset::MilkdropShader::LoadVariables(...)
#2  libprojectM::MilkdropPreset::FinalComposite::Draw(...)
#3  libprojectM::MilkdropPreset::MilkdropPreset::RenderFrame(...)
#4  libprojectM::ProjectM::RenderFrame()

Root Cause Analysis

The preset's composite shader contains this commented line:

comp_1=`//sampler sampler_rand00;    // this will choose a random texture from disk!

Bug Location 1: Missing comment handling in sampler parsing

File: src/libprojectM/MilkdropPreset/MilkdropShader.cpp, function MilkdropShader::GetReferencedSamplers() (defined at line 487, problematic code at lines 496-512)

// Search for sampler usage
auto found = program.find("sampler_", 0);
while (found != std::string::npos)
{
    found += 8;
    size_t const end = program.find_first_of(" ;,\n\r)", found);
    if (end != std::string::npos)
    {
        std::string const sampler = program.substr(...);
        if (sampler != "state")
        {
            m_samplerNames.insert(sampler);
        }
    }
    found = program.find("sampler_", found);
}

Problem: Uses simple find("sampler_") which does not respect comments (// or /* */). A commented line like //sampler sampler_rand00; or /* sampler_rand00 */ will match and extract rand00 as a sampler name.

Bug Location 2: Null pointer dereference in Empty()

File: src/libprojectM/Renderer/TextureSamplerDescriptor.cpp, line 23

auto TextureSamplerDescriptor::Empty() const -> bool
{
    return m_texture->Empty();  // <-- CRASH: m_texture can be nullptr!
}

Problem: No null check before dereferencing m_texture. When GetRandomTexture() returns an empty descriptor (no textures available), m_texture is nullptr. Note that the method's docstring in the header file (lines 47-50) states it should return "false if at least one is invalid or nullptr" - the current implementation contradicts this documented contract.

Call Path Leading to Crash

  1. Preset contains comp_1=`//sampler sampler_rand00;
  2. MilkdropShader::GetReferencedSamplers() (line 487) finds sampler_rand00 (ignoring the // comment)
  3. LoadTexturesAndCompile() sees rand00 and calls GetRandomTexture("rand00")
  4. GetRandomTexture() returns empty descriptor {} (no textures found)
  5. Empty descriptor (with m_texture = nullptr) is added to m_textureSamplerDescriptors
  6. During LoadVariables(), line 318: if (desc.Empty()) calls TextureSamplerDescriptor::Empty()
  7. Empty() does m_texture->Empty() with null m_texture -> SIGSEGV

Proof: Removing this single commented line (changing comp_1 to an empty line) completely fixes the crash.

Suggested Fixes

Fix 1 (Primary): Skip comments when parsing samplers

In MilkdropShader.cpp, modify the GetReferencedSamplers() function's sampler search loop to check for comments. The example below shows // comment handling; a complete fix should also handle /* */ block comments:

auto found = program.find("sampler_", 0);
while (found != std::string::npos)
{
    // Check if this is inside a // comment
    size_t lineStart = program.rfind('\n', found);
    if (lineStart == std::string::npos) lineStart = 0;
    else lineStart++; // Skip the newline character

    size_t commentPos = program.find("//", lineStart);
    if (commentPos != std::string::npos && commentPos < found)
    {
        // Inside a comment, skip to next occurrence
        found = program.find("sampler_", found + 1);
        continue;
    }

    // ... rest of existing sampler extraction code
}

Fix 2 (Defensive): Add null check in Empty()

In TextureSamplerDescriptor.cpp, add null check:

auto TextureSamplerDescriptor::Empty() const -> bool
{
    return !m_texture || m_texture->Empty();
}

Both fixes should be applied - Fix 1 addresses the root cause, Fix 2 provides defensive programming against other potential null texture scenarios.

Workaround

Users can fix affected presets by removing or modifying commented sampler declarations:

  • Delete the line entirely
  • Rename the sampler to break the pattern (e.g., change sampler_rand00 to xsampler_rand00 or disabled_sampler_rand00)

Note: Adding a space after // (e.g., // sampler instead of //sampler) does NOT work - the parser matches sampler_rand00 regardless of preceding characters.

Additional Context

  • The preset loads successfully - projectm_load_preset_file() returns without error
  • The preset_switch_failed_event_callback is NOT triggered
  • The crash occurs on the first call to projectm_opengl_render_frame()
  • This preset is from the official presets-cream-of-the-crop collection
  • Any preset with a commented sampler declaration will crash if no matching textures are available
  • Two code paths in GetRandomTexture() can return an empty descriptor: (1) when no texture files are scanned at all, or (2) when a prefix filter is specified but no matching files exist. Both paths lead to the same crash.
  • The Empty() method's docstring (TextureSamplerDescriptor.hpp lines 47-50) states it should return "false if at least one is invalid or nullptr" - but the implementation crashes instead of returning false, indicating the current behavior contradicts the documented intent.
  • The texsize_ parsing code (lines 516-529 in the same function) uses an identical pattern and has the same vulnerability - commented texsize_ declarations would also be incorrectly parsed.

Final note:

This bug was found and the bug report written with the help of Claude Opus 4.5. I hope you all don't mind, and I hope the analysis is correct. I don't know C++ and couldn't have figured out the problem or described it adequately without Claude's help. I hope this is not a red herring and is a real bug and a useful bug report.

I wish you all the best and am really enjoying projectm!

Thank you for your hard work!

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugThe issue is (potentially) a bug.triageThis is a new issue which hasn't been reviewed yet by a staff member.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions