@@ -438,3 +438,165 @@ func TestWrapErr_BoundaryConditions(t *testing.T) {
438438 }
439439 })
440440}
441+
442+ // =============================================================================
443+ // Test 6: ToCStr Lifecycle After Free
444+ // =============================================================================
445+
446+ // TestToCStr_LifecycleAfterFree documents and verifies the unsafe behaviour of
447+ // ToCStr after calling Free(). This is a CRITICAL test that documents memory
448+ // safety issues to prevent user bugs.
449+ //
450+ // IMPORTANT: This test documents UNSAFE BEHAVIOUR that can cause crashes or
451+ // memory corruption. Unlike GlobalCStr (which has dontFree=true and cannot be
452+ // freed), ToCStr returns a CStr that CAN be freed. If a user calls Free() and
453+ // then tries to use the CStr, they will access freed memory leading to:
454+ // - Segmentation faults
455+ // - Use-after-free bugs
456+ // - Data corruption
457+ // - Non-deterministic crashes
458+ //
459+ // This test ensures the behaviour is documented and consistent.
460+ func TestToCStr_LifecycleAfterFree (t * testing.T ) {
461+ t .Run ("tocstr_creates_allocable_string" , func (t * testing.T ) {
462+ // ToCStr should create a string that CAN be freed (unlike GlobalCStr)
463+ str := ffmpeg .ToCStr ("test string" )
464+ defer str .Free ()
465+
466+ // Verify the string content is correct before we free it
467+ content := str .String ()
468+ if content != "test string" {
469+ t .Errorf ("Expected 'test string', got '%s'" , content )
470+ }
471+ })
472+
473+ t .Run ("globalcstr_cannot_be_freed" , func (t * testing.T ) {
474+ // GlobalCStr: cannot be freed (dontFree=true)
475+ globalStr := ffmpeg .GlobalCStr ("global string" )
476+ globalContent := globalStr .String ()
477+
478+ if globalContent != "global string" {
479+ t .Errorf ("Expected 'global string', got '%s'" , globalContent )
480+ }
481+
482+ // Calling Free() on GlobalCStr is a no-op (has dontFree=true)
483+ globalStr .Free ()
484+
485+ // Can still use after Free() because dontFree=true
486+ globalContent2 := globalStr .String ()
487+ if globalContent2 != "global string" {
488+ t .Errorf ("GlobalCStr should remain accessible after Free(): %s" , globalContent2 )
489+ }
490+
491+ t .Log ("LIFECYCLE: GlobalCStr.Free() is always safe (no-op)" )
492+ })
493+
494+ t .Run ("tocstr_documents_safe_pattern" , func (t * testing.T ) {
495+ // SAFE: Allocate, Use, then Free
496+ safeStr := ffmpeg .ToCStr ("data" )
497+ defer safeStr .Free ()
498+
499+ result := safeStr .String ()
500+ if result != "data" {
501+ t .Errorf ("Expected 'data', got '%s'" , result )
502+ }
503+
504+ t .Log ("SAFE PATTERN: Use ToCStr before calling Free()" )
505+ })
506+
507+ t .Run ("tocstr_defer_ensures_single_free" , func (t * testing.T ) {
508+ // SAFE: Use defer to ensure Free() called exactly once
509+ safeStr := ffmpeg .ToCStr ("data" )
510+ defer safeStr .Free ()
511+
512+ content := safeStr .String ()
513+ if content != "data" {
514+ t .Errorf ("Expected 'data', got '%s'" , content )
515+ }
516+
517+ t .Log ("SAFE PATTERN: Use defer to ensure Free() called exactly once" )
518+ })
519+
520+ t .Run ("rawptr_valid_before_free" , func (t * testing.T ) {
521+ // RawPtr() returns unsafe.Pointer to the underlying memory
522+ str := ffmpeg .ToCStr ("raw pointer test" )
523+ defer str .Free ()
524+
525+ ptr := str .RawPtr ()
526+
527+ if ptr == nil {
528+ t .Error ("RawPtr() should not return nil" )
529+ }
530+
531+ t .Log ("PATTERN: RawPtr() is valid before Free() is called" )
532+ })
533+
534+ t .Run ("dup_creates_independent_allocation" , func (t * testing.T ) {
535+ // Dup() creates a new allocation that must be freed independently
536+ original := ffmpeg .ToCStr ("original" )
537+ defer original .Free ()
538+
539+ // Dup creates a new string that is independently allocated
540+ copy := original .Dup ()
541+ defer copy .Free ()
542+
543+ // Each string has its own memory
544+ originalContent := original .String ()
545+ copyContent := copy .String ()
546+
547+ if originalContent != "original" {
548+ t .Errorf ("Original should be 'original', got '%s'" , originalContent )
549+ }
550+
551+ if copyContent != "original" {
552+ t .Errorf ("Copy should be 'original', got '%s'" , copyContent )
553+ }
554+
555+ t .Log ("PATTERN: Dup() returns independently allocated string - must be freed separately" )
556+ })
557+
558+ t .Run ("allocstr_and_tocstr_both_freeable" , func (t * testing.T ) {
559+ // AllocCStr and ToCStr both create freeable strings with same lifecycle
560+
561+ // AllocCStr: allocates empty buffer
562+ allocated := ffmpeg .AllocCStr (32 )
563+ defer allocated .Free ()
564+
565+ // Verify it's valid
566+ _ = allocated .String ()
567+
568+ // ToCStr: allocates and initializes
569+ converted := ffmpeg .ToCStr ("test" )
570+ defer converted .Free ()
571+
572+ content := converted .String ()
573+ if content != "test" {
574+ t .Errorf ("Expected 'test', got '%s'" , content )
575+ }
576+
577+ t .Log ("PATTERN: Both AllocCStr and ToCStr return freeable strings" )
578+ })
579+
580+ t .Run ("documents_unsafe_patterns_to_avoid" , func (t * testing.T ) {
581+ // This test documents patterns to AVOID - we only document them,
582+ // we do NOT execute them to avoid crashes
583+
584+ t .Log ("UNSAFE PATTERN 1: Call Free() then access" )
585+ t .Log (" str := ToCStr(\" data\" )" )
586+ t .Log (" str.Free()" )
587+ t .Log (" result := str.String() // CRASH: use-after-free" )
588+ t .Log ("" )
589+
590+ t .Log ("UNSAFE PATTERN 2: Call Free() twice" )
591+ t .Log (" str := ToCStr(\" data\" )" )
592+ t .Log (" str.Free()" )
593+ t .Log (" str.Free() // CRASH: double-free" )
594+ t .Log ("" )
595+
596+ t .Log ("UNSAFE PATTERN 3: Access RawPtr() after Free()" )
597+ t .Log (" str := ToCStr(\" data\" )" )
598+ t .Log (" ptr := str.RawPtr()" )
599+ t .Log (" str.Free()" )
600+ t .Log (" C.some_function(ptr) // CRASH: use freed memory" )
601+ })
602+ }
0 commit comments