Skip to content

Race condition in temp file removal causes panic when multiple instances start simultaneously #1183

@mukhtaronif

Description

@mukhtaronif

Introduction

When multiple SQLPage instances start simultaneously in the same directory, they encounter a race condition that causes a panic during initialization. This occurs because SQLPage's create_default_database() function creates a temporary file to test directory writability, then unconditionally removes it using .expect(). When multiple instances try to remove the same file concurrently, approximately 75% of them panic with "No such file or directory" error.

To Reproduce

  1. Create a test directory and navigate to it:
mkdir sqlpage_test && cd sqlpage_test
  1. Start multiple SQLPage instances simultaneously in the same directory:
for i in {1..10}; do
  sqlpage --port $((8080 + i)) &
done
wait
  1. Observe that approximately 7-8 out of 10 instances crash during startup

Actual behavior

Approximately 75% of the instances panic during initialization with the following error:

thread 'main' panicked at sqlpage-0.41.0/src/app_config.rs:498:52:
removing temp file: Os {
    code: 2,
    kind: NotFound,
    message: "No such file or directory"
}

Race condition sequence:

  1. Instance A creates temp file sqlpage/sqlpage.db to test writability
  2. Instance B creates the same file (overwrites A's file)
  3. Instance A tries to remove the file (line 498)
  4. Instance B has already removed it
  5. Instance A panics with "No such file or directory"

Expected behavior

All instances should start successfully without panicking. The temp file creation is only for testing directory writability - if the file no longer exists during removal (because another process removed it), this should be handled gracefully rather than causing a panic.

Version information

  • OS: macOS Darwin 24.6.0 (also affects Linux/Unix)
  • Database: SQLite
  • SQLPage Version: 0.41.0

Additional context

Root cause in source code:

In src/app_config.rs, lines 492-499, the create_default_database() function:

if let Ok(tmp_file) = std::fs::File::create(&default_db_path) {
    log::info!(
        "No DATABASE_URL provided, {} is writable, creating a new database file.",
        default_db_path.display()
    );
    drop(tmp_file);
    std::fs::remove_file(&default_db_path).expect("removing temp file"); // ⚠️ Panics here
    return prefix + &encode_uri(&default_db_path) + "?mode=rwc";
}

Proposed fix:

Replace the .expect() with graceful error handling:

if let Ok(tmp_file) = std::fs::File::create(&default_db_path) {
    log::info!(
        "No DATABASE_URL provided, {} is writable, creating a new database file.",
        default_db_path.display()
    );
    drop(tmp_file);

    // Gracefully handle removal - file might already be removed by another instance
    if let Err(e) = std::fs::remove_file(&default_db_path) {
        log::debug!("Temp file already removed or not found: {}", e);
    }

    return prefix + &encode_uri(&default_db_path) + "?mode=rwc";
}

Rationale:

  • The temp file's purpose is to test writability, which already succeeded
  • Whether the file exists during removal is irrelevant to this test
  • Other instances or processes may have legitimately removed it
  • This is standard practice in concurrent environments

Impact:

  • Severity: High - prevents reliable multi-instance deployments
  • Affected use cases: Container orchestration (Kubernetes, Docker Swarm), proxy servers, load balancers, any concurrent startup scenario
  • Discovery context: Found while building a proxy server spawning multiple surveilr instances (which uses SQLPage 0.41.0)

Workaround:

Set DATABASE_URL environment variable to bypass the create_default_database() code path:

export DATABASE_URL="sqlite://./sqlpage/sqlpage.db"
sqlpage

I'm happy to submit a PR with this fix if that would be helpful.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions