# TOCTOU Race Condition in File Existence Check before Open

Language: Python
Severity: High
CWE: CWE-367

## Source
5

## Flow
5-6-9

## Sink
9

## Vulnerable Code
```python
import os
import boto3

def sync_model_weights_to_s3(local_weights_path, bucket_name, s3_key):
    if os.path.exists(local_weights_path):
        file_size = os.path.getsize(local_weights_path)
        print(f"Uploading model weights: {file_size} bytes")
        s3_client = boto3.client('s3')
        with open(local_weights_path, 'rb') as weight_file:
            s3_client.upload_fileobj(weight_file, bucket_name, s3_key)
        return True
    return False
```

## Explanation

This is a TOCTOU (Time-of-Check-Time-of-Use) race condition vulnerability. The code checks if the file exists at line 5, but doesn't open it until line 9. Between these operations, an attacker could delete the file (causing a crash), replace it with malicious content, or create a symlink to sensitive data that would then be uploaded to S3.

## Remediation

The fix eliminates the TOCTOU race condition by using os.open() with O_NOFOLLOW as the first operation, combining the existence check and file open into a single atomic operation. File metadata (size, type) is then retrieved via os.fstat() on the already-opened file descriptor rather than the file path, ensuring consistency. The O_NOFOLLOW flag prevents symlink-based attacks where an attacker could redirect the upload to sensitive files. The incorrect 'os.path.stat.S_ISREG' reference has been corrected to use 'import stat' and 'stat.S_ISREG()'.

## Secure Code
```python
import os
import stat
import boto3

def sync_model_weights_to_s3(local_weights_path, bucket_name, s3_key):
    try:
        # Open the file first, eliminating the TOCTOU window
        # Use os.open with O_NOFOLLOW to prevent symlink attacks
        fd = os.open(local_weights_path, os.O_RDONLY | os.O_NOFOLLOW)
        try:
            # Get file size from the opened file descriptor, not the path
            file_stat = os.fstat(fd)
            file_size = file_stat.st_size
            # Verify it's a regular file
            if not stat.S_ISREG(file_stat.st_mode):
                os.close(fd)
                print(f"Error: {local_weights_path} is not a regular file")
                return False
            print(f"Uploading model weights: {file_size} bytes")
            # Wrap the file descriptor in a Python file object
            with os.fdopen(fd, 'rb') as weight_file:
                s3_client = boto3.client('s3')
                s3_client.upload_fileobj(weight_file, bucket_name, s3_key)
            return True
        except Exception:
            os.close(fd)
            raise
    except OSError as e:
        print(f"Cannot open model weights file: {e}")
        return False
```
