Repository: dankamongmen/notcurses (language: python)

Issue:
notcurses_stop() doesn't clean up widgets
`notcurses_stop()` ought free up all resources internal to Notcurses, whether the user destroyed them or not. The `MenuNoFree` unit test exercises this, but leaks all menu resources.

`ncplane`s get cleaned up because the `notcurses` struct has a set of `ncpile`s, and they have references to all the planes. there is no such infrastructure for widgets (though the planes to which they are bound *do* get cleaned up).

```c
Indirect leak of 72 byte(s) in 1 object(s) allocated from:
    #0 0x7fe4d3a9c7cf in __interceptor_malloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:145
    #1 0x7fe4d375bb6f in dup_menu_sections /home/dank/src/dankamongmen/notcurses/src/lib/menu.c:195
    #2 0x7fe4d375bb6f in ncmenu_create /home/dank/src/dankamongmen/notcurses/src/lib/menu.c:398

Indirect leak of 56 byte(s) in 1 object(s) allocated from:
    #0 0x7fe4d3a9c7cf in __interceptor_malloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:145
    #1 0x7fe4d375be95 in dup_menu_section /home/dank/src/dankamongmen/notcurses/src/lib/menu.c:145
    #2 0x7fe4d375be95 in dup_menu_sections /home/dank/src/dankamongmen/notcurses/src/lib/menu.c:215
    #3 0x7fe4d375be95 in ncmenu_create /home/dank/src/dankamongmen/notcurses/src/lib/menu.c:398

Indirect leak of 24 byte(s) in 1 object(s) allocated from:
    #0 0x7fe4d3a477a7 in __interceptor_strdup ../../../../src/libsanitizer/asan/asan_interceptors.cpp:454
    #1 0x7fe4d375bf05 in dup_menu_item /home/dank/src/dankamongmen/notcurses/src/lib/menu.c:86
    #2 0x7fe4d375bf05 in dup_menu_section /home/dank/src/dankamongmen/notcurses/src/lib/menu.c:151
    #3 0x7fe4d375bf05 in dup_menu_sections /home/dank/src/dankamongmen/notcurses/src/lib/menu.c:215
    #4 0x7fe4d375bf05 in ncmenu_create /home/dank/src/dankamongmen/notcurses/src/lib/menu.c:398
```

really, i think if the `ncplane` to which a widget is bound is ever destroyed, the widget ought be destroyed (right now we enforce the other way -- destroying a widget destroys its plane -- which i think we ought/must retain). we fundamentally would either need track the sets of each widget type in `struct notcurses`, or we would need track the widget (and its destructor) in each `ncplane`. this latter might be best, as it would prevent us from ever binding a plane to two different widgets. it adds two pointers per `ncplane`, not so bad.
notcurses_stop() doesn't clean up widgets
`notcurses_stop()` ought free up all resources internal to Notcurses, whether the user destroyed them or not. The `MenuNoFree` unit test exercises this, but leaks all menu resources.

`ncplane`s get cleaned up because the `notcurses` struct has a set of `ncpile`s, and they have references to all the planes. there is no such infrastructure for widgets (though the planes to which they are bound *do* get cleaned up).

```c
Indirect leak of 72 byte(s) in 1 object(s) allocated from:
    #0 0x7fe4d3a9c7cf in __interceptor_malloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:145
    #1 0x7fe4d375bb6f in dup_menu_sections /home/dank/src/dankamongmen/notcurses/src/lib/menu.c:195
    #2 0x7fe4d375bb6f in ncmenu_create /home/dank/src/dankamongmen/notcurses/src/lib/menu.c:398

Indirect leak of 56 byte(s) in 1 object(s) allocated from:
    #0 0x7fe4d3a9c7cf in __interceptor_malloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:145
    #1 0x7fe4d375be95 in dup_menu_section /home/dank/src/dankamongmen/notcurses/src/lib/menu.c:145
    #2 0x7fe4d375be95 in dup_menu_sections /home/dank/src/dankamongmen/notcurses/src/lib/menu.c:215
    #3 0x7fe4d375be95 in ncmenu_create /home/dank/src/dankamongmen/notcurses/src/lib/menu.c:398

Indirect leak of 24 byte(s) in 1 object(s) allocated from:
    #0 0x7fe4d3a477a7 in __interceptor_strdup ../../../../src/libsanitizer/asan/asan_interceptors.cpp:454
    #1 0x7fe4d375bf05 in dup_menu_item /home/dank/src/dankamongmen/notcurses/src/lib/menu.c:86
    #2 0x7fe4d375bf05 in dup_menu_section /home/dank/src/dankamongmen/notcurses/src/lib/menu.c:151
    #3 0x7fe4d375bf05 in dup_menu_sections /home/dank/src/dankamongmen/notcurses/src/lib/menu.c:215
    #4 0x7fe4d375bf05 in ncmenu_create /home/dank/src/dankamongmen/notcurses/src/lib/menu.c:398
```

really, i think if the `ncplane` to which a widget is bound is ever destroyed, the widget ought be destroyed (right now we enforce the other way -- destroying a widget destroys its plane -- which i think we ought/must retain). we fundamentally would either need track the sets of each widget type in `struct notcurses`, or we would need track the widget (and its destructor) in each `ncplane`. this latter might be best, as it would prevent us from ever binding a plane to two different widgets. it adds two pointers per `ncplane`, not so bad.
notcurses_stop() doesn't clean up widgets
`notcurses_stop()` ought free up all resources internal to Notcurses, whether the user destroyed them or not. The `MenuNoFree` unit test exercises this, but leaks all menu resources.

`ncplane`s get cleaned up because the `notcurses` struct has a set of `ncpile`s, and they have references to all the planes. there is no such infrastructure for widgets (though the planes to which they are bound *do* get cleaned up).

```c
Indirect leak of 72 byte(s) in 1 object(s) allocated from:
    #0 0x7fe4d3a9c7cf in __interceptor_malloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:145
    #1 0x7fe4d375bb6f in dup_menu_sections /home/dank/src/dankamongmen/notcurses/src/lib/menu.c:195
    #2 0x7fe4d375bb6f in ncmenu_create /home/dank/src/dankamongmen/notcurses/src/lib/menu.c:398

Indirect leak of 56 byte(s) in 1 object(s) allocated from:
    #0 0x7fe4d3a9c7cf in __interceptor_malloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:145
    #1 0x7fe4d375be95 in dup_menu_section /home/dank/src/dankamongmen/notcurses/src/lib/menu.c:145
    #2 0x7fe4d375be95 in dup_menu_sections /home/dank/src/dankamongmen/notcurses/src/lib/menu.c:215
    #3 0x7fe4d375be95 in ncmenu_create /home/dank/src/dankamongmen/notcurses/src/lib/menu.c:398

Indirect leak of 24 byte(s) in 1 object(s) allocated from:
    #0 0x7fe4d3a477a7 in __interceptor_strdup ../../../../src/libsanitizer/asan/asan_interceptors.cpp:454
    #1 0x7fe4d375bf05 in dup_menu_item /home/dank/src/dankamongmen/notcurses/src/lib/menu.c:86
    #2 0x7fe4d375bf05 in dup_menu_section /home/dank/src/dankamongmen/notcurses/src/lib/menu.c:151
    #3 0x7fe4d375bf05 in dup_menu_sections /home/dank/src/dankamongmen/notcurses/src/lib/menu.c:215
    #4 0x7fe4d375bf05 in ncmenu_create /home/dank/src/dankamongmen/notcurses/src/lib/menu.c:398
```

really, i think if the `ncplane` to which a widget is bound is ever destroyed, the widget ought be destroyed (right now we enforce the other way -- destroying a widget destroys its plane -- which i think we ought/must retain). we fundamentally would either need track the sets of each widget type in `struct notcurses`, or we would need track the widget (and its destructor) in each `ncplane`. this latter might be best, as it would prevent us from ever binding a plane to two different widgets. it adds two pointers per `ncplane`, not so bad.
notcurses_stop() doesn't clean up widgets
`notcurses_stop()` ought free up all resources internal to Notcurses, whether the user destroyed them or not. The `MenuNoFree` unit test exercises this, but leaks all menu resources.

`ncplane`s get cleaned up because the `notcurses` struct has a set of `ncpile`s, and they have references to all the planes. there is no such infrastructure for widgets (though the planes to which they are bound *do* get cleaned up).

```c
Indirect leak of 72 byte(s) in 1 object(s) allocated from:
    #0 0x7fe4d3a9c7cf in __interceptor_malloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:145
    #1 0x7fe4d375bb6f in dup_menu_sections /home/dank/src/dankamongmen/notcurses/src/lib/menu.c:195
    #2 0x7fe4d375bb6f in ncmenu_create /home/dank/src/dankamongmen/notcurses/src/lib/menu.c:398

Indirect leak of 56 byte(s) in 1 object(s) allocated from:
    #0 0x7fe4d3a9c7cf in __interceptor_malloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:145
    #1 0x7fe4d375be95 in dup_menu_section /home/dank/src/dankamongmen/notcurses/src/lib/menu.c:145
    #2 0x7fe4d375be95 in dup_menu_sections /home/dank/src/dankamongmen/notcurses/src/lib/menu.c:215
    #3 0x7fe4d375be95 in ncmenu_create /home/dank/src/dankamongmen/notcurses/src/lib/menu.c:398

Indirect leak of 24 byte(s) in 1 object(s) allocated from:
    #0 0x7fe4d3a477a7 in __interceptor_strdup ../../../../src/libsanitizer/asan/asan_interceptors.cpp:454
    #1 0x7fe4d375bf05 in dup_menu_item /home/dank/src/dankamongmen/notcurses/src/lib/menu.c:86
    #2 0x7fe4d375bf05 in dup_menu_section /home/dank/src/dankamongmen/notcurses/src/lib/menu.c:151
    #3 0x7fe4d375bf05 in dup_menu_sections /home/dank/src/dankamongmen/notcurses/src/lib/menu.c:215
    #4 0x7fe4d375bf05 in ncmenu_create /home/dank/src/dankamongmen/notcurses/src/lib/menu.c:398
```

really, i think if the `ncplane` to which a widget is bound is ever destroyed, the widget ought be destroyed (right now we enforce the other way -- destroying a widget destroys its plane -- which i think we ought/must retain). we fundamentally would either need track the sets of each widget type in `struct notcurses`, or we would need track the widget (and its destructor) in each `ncplane`. this latter might be best, as it would prevent us from ever binding a plane to two different widgets. it adds two pointers per `ncplane`, not so bad.
planes must not be bound to multiple widgets
With #2342 getting resolved, it's probably best that we use this new infrastructure to ensure a plane is never bound to two distinct widgets. This ought be as simple as checking the return value of `ncplane_set_widget()` in the widget constructor, and tearing the widget down+returning error on an error there.

It would behoove us to add unit tests for this case everywhere, as the error path is otherwise likely to go untested.
planes must not be bound to multiple widgets
With #2342 getting resolved, it's probably best that we use this new infrastructure to ensure a plane is never bound to two distinct widgets. This ought be as simple as checking the return value of `ncplane_set_widget()` in the widget constructor, and tearing the widget down+returning error on an error there.

It would behoove us to add unit tests for this case everywhere, as the error path is otherwise likely to go untested.


Discussion / hints:

Fix patch:
diff --git a/doc/man/man3/notcurses_menu.3.md b/doc/man/man3/notcurses_menu.3.md
index 1e73338f4d..fb6fc490a1 100644
--- a/doc/man/man3/notcurses_menu.3.md
+++ b/doc/man/man3/notcurses_menu.3.md
@@ -60,7 +60,7 @@ typedef struct ncmenu_options {
 
 **bool ncmenu_offer_input(struct ncmenu* ***n***, const ncinput* ***nc***);**
 
-**int ncmenu_destroy(struct ncmenu* ***n***);**
+**void ncmenu_destroy(struct ncmenu* ***n***);**
 
 # DESCRIPTION
 
diff --git a/doc/man/man3/notcurses_selector.3.md b/doc/man/man3/notcurses_selector.3.md
index ef47ebfa7f..1a79ff5114 100644
--- a/doc/man/man3/notcurses_selector.3.md
+++ b/doc/man/man3/notcurses_selector.3.md
@@ -55,7 +55,7 @@ typedef struct ncselector_options {
 
 **bool ncselector_offer_input(struct ncselector* ***n***, const ncinput* ***nc***);**
 
-**void ncselector_destroy(struct ncselector* ***n***, char\*\* ***item***);**
+**void ncselector_destroy(struct ncselector* ***n***);**
 
 # DESCRIPTION
 
diff --git a/include/ncpp/Selector.hh b/include/ncpp/Selector.hh
index 342c6fb443..e63f0e41f2 100644
--- a/include/ncpp/Selector.hh
+++ b/include/ncpp/Selector.hh
@@ -38,7 +38,7 @@ namespace ncpp
 		~Selector ()
 		{
 			if (!is_notcurses_stopped ())
-				ncselector_destroy (selector, nullptr);
+				ncselector_destroy (selector);
 		}
 
 		int additem (const ncselector_item *item) const NOEXCEPT_MAYBE
diff --git a/include/notcurses/notcurses.h b/include/notcurses/notcurses.h
index 8a38cc58d9..0db32f3035 100644
--- a/include/notcurses/notcurses.h
+++ b/include/notcurses/notcurses.h
@@ -3608,9 +3608,8 @@ API const char* ncselector_nextitem(struct ncselector* n)
 API bool ncselector_offer_input(struct ncselector* n, const ncinput* nc)
   __attribute__ ((nonnull (1, 2)));
 
-// Destroy the ncselector. If 'item' is not NULL, the last selected option will
-// be strdup()ed and assigned to '*item' (and must be free()d by the caller).
-API void ncselector_destroy(struct ncselector* n, char** item);
+// Destroy the ncselector.
+API void ncselector_destroy(struct ncselector* n);
 
 struct ncmselector_item {
   const char* option;
@@ -3825,7 +3824,7 @@ API bool ncmenu_offer_input(struct ncmenu* n, const ncinput* nc)
   __attribute__ ((nonnull (1, 2)));
 
 // Destroy a menu created with ncmenu_create().
-API int ncmenu_destroy(struct ncmenu* n);
+API void ncmenu_destroy(struct ncmenu* n);
 
 // Progress bars. They proceed linearly in any of four directions. The entirety
 // of the plane will be used -- any border should be provided by the caller on
diff --git a/src/demo/zoo.c b/src/demo/zoo.c
index 39883b66e4..93c9912713 100644
--- a/src/demo/zoo.c
+++ b/src/demo/zoo.c
@@ -429,7 +429,7 @@ reader_demo(struct notcurses* nc){
   }
 
 done:
-  ncselector_destroy(selector, NULL);
+  ncselector_destroy(selector);
   ncmultiselector_destroy(mselector);
   ncplane_destroy(rp);
   return ret;
diff --git a/src/lib/internal.h b/src/lib/internal.h
index 2cba9ecf49..462d73f76b 100644
--- a/src/lib/internal.h
+++ b/src/lib/internal.h
@@ -113,6 +113,12 @@ typedef struct ncplane {
   int margin_b, margin_r;// bottom and right margins, stored for resize
   bool scrolling;        // is scrolling enabled? always disabled by default
   bool fixedbound;       // are we fixed relative to the parent's scrolling?
+
+  // we need to track any widget to which we are bound, so that (1) we don't
+  // end up bound to two widgets and (2) we can clean them up on shutdown
+  // (assuming they weren't explicitly cleaned up by the client).
+  void* widget;            // widget to which we are bound, can be NULL
+  void(*wdestruct)(void*); // widget destructor, NULL iff widget is NULL
 } ncplane;
 
 // current presentation state of the terminal. it is carried across render
@@ -428,7 +434,25 @@ ncplane_stdplane_const(const ncplane* n){
   return notcurses_stdplane_const(ncplane_notcurses_const(n));
 }
 
-// initialize libav
+// set the plane's widget and wdestruct fields, returning non-zero if they're
+// already non-NULL (i.e. if the plane is already bound), unless we pass NULL
+// (which ought be done from the widget destructor, to avoid corecursion).
+static inline int
+ncplane_set_widget(ncplane* n, void* w, void(*wdestruct)(void*)){
+  if(n->widget){
+    if(w){
+      logerror("plane is already bound to a widget\n");
+      return -1;
+    }
+  }else if(w == NULL){
+    return -1;
+  }
+  n->widget = w;
+  n->wdestruct = wdestruct;
+  return 0;
+}
+
+// initialize visualization backend (ffmpeg/oiio)
 int ncvisual_init(int loglevel);
 
 static inline int
diff --git a/src/lib/menu.c b/src/lib/menu.c
index a24c713b69..737de28435 100644
--- a/src/lib/menu.c
+++ b/src/lib/menu.c
@@ -412,6 +412,7 @@ ncmenu* ncmenu_create(ncplane* n, const ncmenu_options* opts){
       };
       ret->ncp = ncplane_create(n, &nopts);
       if(ret->ncp){
+        ncplane_set_widget(ret->ncp, ret, (void(*)(void*))ncmenu_destroy);
         ret->unrolledsection = -1;
         ret->headerchannels = opts->headerchannels;
         ret->dissectchannels = opts->headerchannels;
@@ -765,12 +766,12 @@ ncplane* ncmenu_plane(ncmenu* menu){
   return menu->ncp;
 }
 
-int ncmenu_destroy(ncmenu* n){
-  int ret = 0;
+void ncmenu_destroy(ncmenu* n){
   if(n){
     free_menu_sections(n);
-    ncplane_destroy(n->ncp);
+    if(ncplane_set_widget(n->ncp, NULL, NULL) == 0){
+      ncplane_destroy(n->ncp);
+    }
     free(n);
   }
-  return ret;
 }
diff --git a/src/lib/notcurses.c b/src/lib/notcurses.c
index 0662d0d5fe..9996ac3c2c 100644
--- a/src/lib/notcurses.c
+++ b/src/lib/notcurses.c
@@ -359,6 +359,15 @@ void free_plane(ncplane* p){
         pthread_mutex_unlock(&nc->pilelock);
       }
     }
+    if(p->widget){
+      void* w = p->widget;
+      void (*wdestruct)(void*) = p->wdestruct;
+      p->widget = NULL;
+      p->wdestruct = NULL;
+      logdebug("calling widget destructor %p(%p)\n", wdestruct, w);
+      wdestruct(w);
+      logdebug("got the widget\n");
+    }
     if(p->sprite){
       sprixel_hide(p->sprite);
     }
@@ -444,6 +453,8 @@ ncplane* ncplane_new_internal(notcurses* nc, ncplane* n,
   }
   p->scrolling = false;
   p->fixedbound = nopts->flags & NCPLANE_OPTION_FIXED;
+  p->widget = NULL;
+  p->wdestruct = NULL;
   if(nopts->flags & NCPLANE_OPTION_MARGINALIZED){
     p->margin_b = nopts->margin_b;
     p->margin_r = nopts->margin_r;
@@ -1171,6 +1182,7 @@ ncpile_drop(notcurses* nc, ncpile** pile){
   ncplane* p = (*pile)->top;
   while(p){
     ncplane* tmp = p->below;
+    logdebug("killing plane %p, next is %p\n", p, tmp);
     if(nc->stdplane != p){
       free_plane(p);
     }else{
@@ -1189,6 +1201,7 @@ ncpile_drop(notcurses* nc, ncpile** pile){
 
 // drop all piles and all planes, save the standard plane and its pile
 void notcurses_drop_planes(notcurses* nc){
+  logdebug("we have some planes\n");
   pthread_mutex_lock(&nc->pilelock);
   ncpile* p = ncplane_pile(nc->stdplane);
   ncpile* p0 = p;
@@ -1196,6 +1209,7 @@ void notcurses_drop_planes(notcurses* nc){
     ncpile_drop(nc, &p);
   }while(p0 != p);
   pthread_mutex_unlock(&nc->pilelock);
+  logdebug("all planes dropped\n");
 }
 
 int notcurses_stop(notcurses* nc){
@@ -1219,7 +1233,9 @@ int notcurses_stop(notcurses* nc){
     }
     if(nc->stdplane){
       notcurses_drop_planes(nc);
+      logdebug("say goodbye to the standard plane\n");
       free_plane(nc->stdplane);
+      logdebug("get outta here\n");
     }
     if(nc->tcache.ttyfd >= 0){
       ret |= close(nc->tcache.ttyfd);
diff --git a/src/lib/plot.c b/src/lib/plot.c
index 5cd5975a47..750f100771 100644
--- a/src/lib/plot.c
+++ b/src/lib/plot.c
@@ -431,6 +431,9 @@ int redraw_plot_##T(nc##X##plot* ncp){ \
 static const struct blitset* \
 create_##T(nc##X##plot* ncpp, ncplane* n, const ncplot_options* opts, const T miny, const T maxy, \
            const T trueminy, const T truemaxy){ \
+  /* set up ->plot.ncp first so it gets destroyed on error */ \
+  ncpp->plot.ncp = n; \
+  ncplane_set_widget(ncpp->plot.ncp, ncpp, (void(*)(void*))nc##X##plot_destroy); \
   ncplot_options zeroed = {}; \
   if(!opts){ \
     opts = &zeroed; \
@@ -440,23 +443,19 @@ create_##T(nc##X##plot* ncpp, ncplane* n, const ncplot_options* opts, const T mi
   } \
   /* if miny == maxy (enabling domain detection), they both must be equal to 0 */ \
   if(miny == maxy && miny){ \
-    ncplane_destroy(n); \
     return NULL; \
   } \
   if(opts->rangex < 0){ \
     logerror("error: supplied negative independent range %d\n", opts->rangex); \
-    ncplane_destroy(n); \
     return NULL; \
   } \
   if(maxy < miny){ \
     logerror("error: supplied maxy < miny\n"); \
-    ncplane_destroy(n); \
     return NULL; \
   } \
   /* DETECTMAXONLY can't be used without domain detection */ \
   if(opts->flags & NCPLOT_OPTION_DETECTMAXONLY && (miny != maxy)){ \
     logerror("Supplied DETECTMAXONLY without domain detection"); \
-    ncplane_destroy(n); \
     return NULL; \
   } \
   const notcurses* notc = ncplane_notcurses(n); \
@@ -467,13 +466,11 @@ create_##T(nc##X##plot* ncpp, ncplane* n, const ncplot_options* opts, const T mi
   bool degrade_blitter = !(opts && (opts->flags & NCPLOT_OPTION_NODEGRADE)); \
   const struct blitset* bset = lookup_blitset(&notc->tcache, blitfxn, degrade_blitter); \
   if(bset == NULL){ \
-    ncplane_destroy(n); \
     return NULL; \
   } \
   unsigned sdimy, sdimx; \
   ncplane_dim_yx(n, &sdimy, &sdimx); \
   if(sdimx <= 0){ \
-    ncplane_destroy(n); \
     return NULL; \
   } \
   unsigned dimx = sdimx; \
@@ -500,11 +497,9 @@ create_##T(nc##X##plot* ncpp, ncplane* n, const ncplot_options* opts, const T mi
   size_t slotsize = sizeof(*ncpp->slots) * ncpp->plot.slotcount; \
   ncpp->slots = malloc(slotsize); \
   if(ncpp->slots == NULL){ \
-    ncplane_destroy(n); \
     return NULL; \
   } \
   memset(ncpp->slots, 0, slotsize); \
-  ncpp->plot.ncp = n; \
   ncpp->plot.maxchannels = opts->maxchannels; \
   ncpp->plot.minchannels = opts->minchannels; \
   ncpp->plot.bset = bset; \
@@ -526,7 +521,6 @@ create_##T(nc##X##plot* ncpp, ncplane* n, const ncplot_options* opts, const T mi
   ncpp->plot.channels = NULL; \
   if(bset->geom == NCBLIT_PIXEL){ \
     if(create_pixelp(&ncpp->plot, n)){ \
-      ncplane_destroy(n); \
       return NULL; \
     } \
   } \
@@ -606,7 +600,9 @@ CREATE(double, d)
 static void
 ncplot_destroy(ncplot* n){
   free(n->title);
-  ncplane_destroy(n->ncp);
+  if(ncplane_set_widget(n->ncp, NULL, NULL) == 0){
+    ncplane_destroy(n->ncp);
+  }
   ncplane_destroy(n->pixelp);
   free(n->channels);
 }
diff --git a/src/lib/reel.c b/src/lib/reel.c
index 8a5e0efe3e..034d22aab1 100644
--- a/src/lib/reel.c
+++ b/src/lib/reel.c
@@ -234,6 +234,74 @@ tablet_geom(const ncreel* nr, nctablet* t, int* begx, int* begy,
   return 0;
 }
 
+// kill the planes associated with |t|
+static void
+nctablet_wipeout(nctablet* t){
+  if(t){
+    if(ncplane_set_widget(t->p, NULL, NULL) == 0){
+      ncplane_destroy_family(t->p);
+    }
+    t->p = NULL;
+    t->cbp = NULL;
+  }
+}
+
+// destroy all existing tablet planes pursuant to redraw
+static void
+clean_reel(ncreel* r){
+  nctablet* vft = r->vft;
+  if(vft){
+    for(nctablet* n = vft->next ; n->p && n != vft ; n = n->next){
+//fprintf(stderr, "CLEANING NEXT: %p (%p)\n", n, n->p);
+      nctablet_wipeout(n);
+    }
+    for(nctablet* n = vft->prev ; n->p && n != vft ; n = n->prev){
+//fprintf(stderr, "CLEANING PREV: %p (%p)\n", n, n->p);
+      nctablet_wipeout(n);
+    }
+//fprintf(stderr, "CLEANING VFT: %p (%p)\n", vft, vft->p);
+    nctablet_wipeout(vft);
+    r->vft = NULL;
+  }
+}
+
+// used as ncplane widget destructor callback, and likewise the heart of
+// ncreel_del(). without this, at context collapse time we'd wipe out all the
+// planes (without wiping out their subsidiaries), and ncreel_destroy() would
+// walk freed memory. as we have no handle on the ncreel, we do not update its
+// metadata (tabletcount), nor redraw, but who cares? we're shutting down.
+static void
+nctablet_delete_internal(struct nctablet* t){
+  t->prev->next = t->next;
+  t->next->prev = t->prev;
+  if(t->p){
+    if(ncplane_set_widget(t->p, NULL, NULL) == 0){
+      ncplane_destroy_family(t->p);
+    }
+  }
+  free(t);
+}
+
+int ncreel_del(ncreel* nr, struct nctablet* t){
+  if(t == NULL){
+    return -1;
+  }
+  if(nr->tablets == t){
+    if((nr->tablets = t->next) == t){
+      nr->tablets = NULL;
+    }
+    // FIXME ought set direction based on actual location of replacement tablet
+    nr->direction = LASTDIRECTION_DOWN;
+  }
+  if(nr->vft == t){
+    clean_reel(nr); // maybe just make nr->tablets the vft?
+  }
+  nctablet_delete_internal(t);
+  --nr->tabletcount;
+  ncreel_redraw(nr);
+  return 0;
+}
+
 // Draw the specified tablet, if possible. DIRECTION_UP means we're laying out
 // bottom-to-top. DIRECTION_DOWN means top-to-bottom. 'frontier{top, bottom}'
 // are the lines to which we'll be fitting the tablet ('frontiertop' to our
@@ -266,6 +334,7 @@ ncreel_draw_tablet(const ncreel* nr, nctablet* t, int frontiertop,
 //fprintf(stderr, "failure creating border plane %d %d %d %d\n", leny, lenx, begy, begx);
     return -1;
   }
+  ncplane_set_widget(t->p, t, (void(*)(void*))nctablet_delete_internal);
   // we allow the callback to use a bound plane that lives above our border
   // plane, thus preventing the callback from spilling over the tablet border.
   int cby = 0, cbx = 0, cbleny = leny, cblenx = lenx;
@@ -462,7 +531,9 @@ trim_reel_overhang(ncreel* r, nctablet* top, nctablet* bottom){
   //fprintf(stderr, "bot: %dx%d @ %d, maxy: %d\n", ylen, xlen, y, maxy);
     if(maxy < y){
   //fprintf(stderr, "NUKING bottom!\n");
-      ncplane_destroy_family(bottom->p);
+      if(ncplane_set_widget(bottom->p, NULL, NULL) == 0){
+        ncplane_destroy_family(bottom->p);
+      }
       bottom->p = NULL;
       bottom->cbp = NULL;
       bottom = bottom->prev;
@@ -596,31 +667,6 @@ tighten_reel(ncreel* r){
   return trim_reel_overhang(r, top, bottom);
 }
 
-// destroy all existing tablet planes pursuant to redraw
-static void
-clean_reel(ncreel* r){
-  nctablet* vft = r->vft;
-  if(vft){
-    for(nctablet* n = vft->next ; n->p && n != vft ; n = n->next){
-//fprintf(stderr, "CLEANING NEXT: %p (%p)\n", n, n->p);
-      ncplane_destroy_family(n->p);
-      n->p = NULL;
-      n->cbp = NULL;
-    }
-    for(nctablet* n = vft->prev ; n->p && n != vft ; n = n->prev){
-//fprintf(stderr, "CLEANING PREV: %p (%p)\n", n, n->p);
-      ncplane_destroy_family(n->p);
-      n->p = NULL;
-      n->cbp = NULL;
-    }
-//fprintf(stderr, "CLEANING VFT: %p (%p)\n", vft, vft->p);
-    ncplane_destroy_family(vft->p);
-    vft->p = NULL;
-    vft->cbp = NULL;
-    r->vft = NULL;
-  }
-}
-
 // Arrange the panels, starting with the focused window, wherever it may be.
 // If necessary, resize it to the full size of the reel--focus has its
 // privileges. We then work in the opposite direction of travel, filling out
@@ -776,6 +822,11 @@ ncreel* ncreel_create(ncplane* n, const ncreel_options* ropts){
   memcpy(&nr->ropts, ropts, sizeof(*ropts));
   nr->p = n;
   nr->vft = NULL;
+  if(ncplane_set_widget(nr->p, nr, (void(*)(void*))ncreel_destroy)){
+    ncplane_destroy(nr->p);
+    free(nr);
+    return NULL;
+  }
   if(ncreel_redraw(nr)){
     ncplane_destroy(nr->p);
     free(nr);
@@ -826,38 +877,15 @@ nctablet* ncreel_add(ncreel* nr, nctablet* after, nctablet *before,
   return t;
 }
 
-int ncreel_del(ncreel* nr, struct nctablet* t){
-  if(t == NULL){
-    return -1;
-  }
-  t->prev->next = t->next;
-  if(nr->tablets == t){
-    if((nr->tablets = t->next) == t){
-      nr->tablets = NULL;
-    }
-    // FIXME ought set direction based on actual location of replacement tablet
-    nr->direction = LASTDIRECTION_DOWN;
-  }
-  if(nr->vft == t){
-    clean_reel(nr); // maybe just make nr->tablets the vft?
-  }
-  t->next->prev = t->prev;
-  if(t->p){
-    ncplane_destroy_family(t->p);
-  }
-  free(t);
-  --nr->tabletcount;
-  ncreel_redraw(nr);
-  return 0;
-}
-
 void ncreel_destroy(ncreel* nreel){
   if(nreel){
-    nctablet* t;
-    while( (t = nreel->tablets) ){
-      ncreel_del(nreel, t);
+    if(ncplane_set_widget(nreel->p, NULL, NULL) == 0){
+      nctablet* t;
+      while( (t = nreel->tablets) ){
+        ncreel_del(nreel, t);
+      }
+      ncplane_destroy(nreel->p);
     }
-    ncplane_destroy(nreel->p);
     free(nreel);
   }
 }
diff --git a/src/lib/selector.c b/src/lib/selector.c
index b529752282..0ec4521c9e 100644
--- a/src/lib/selector.c
+++ b/src/lib/selector.c
@@ -340,6 +340,7 @@ ncselector* ncselector_create(ncplane* n, const ncselector_options* opts){
     goto freeitems;
   }
   ncselector_draw(ns); // deal with error here?
+  ncplane_set_widget(ns->ncp, ns, (void(*)(void*))ncselector_destroy);
   return ns;
 
 freeitems:
@@ -548,17 +549,15 @@ bool ncselector_offer_input(ncselector* n, const ncinput* nc){
   return false;
 }
 
-void ncselector_destroy(ncselector* n, char** item){
+void ncselector_destroy(ncselector* n){
   if(n){
-    if(item){
-      *item = n->items[n->selected].option;
-      n->items[n->selected].option = NULL;
-    }
     while(n->itemcount--){
       free(n->items[n->itemcount].option);
       free(n->items[n->itemcount].desc);
     }
-    ncplane_destroy(n->ncp);
+    if(ncplane_set_widget(n->ncp, NULL, NULL) == 0){
+      ncplane_destroy(n->ncp);
+    }
     free(n->items);
     free(n->title);
     free(n->secondary);
@@ -945,6 +944,7 @@ ncmultiselector* ncmultiselector_create(ncplane* n, const ncmultiselector_option
     goto freeitems;
   }
   ncmultiselector_draw(ns); // deal with error here?
+  ncplane_set_widget(ns->ncp, ns, (void(*)(void*))ncmultiselector_destroy);
   return ns;
 
 freeitems:
@@ -965,7 +965,9 @@ void ncmultiselector_destroy(ncmultiselector* n){
       free(n->items[n->itemcount].option);
       free(n->items[n->itemcount].desc);
     }
-    ncplane_destroy(n->ncp);
+    if(ncplane_set_widget(n->ncp, NULL, NULL) == 0){
+      ncplane_destroy(n->ncp);
+    }
     free(n->items);
     free(n->title);
     free(n->secondary);
diff --git a/src/lib/tabbed.c b/src/lib/tabbed.c
index 0910aba995..e061b5f100 100644
--- a/src/lib/tabbed.c
+++ b/src/lib/tabbed.c
@@ -223,6 +223,7 @@ nctabbed* nctabbed_create(ncplane* n, const nctabbed_options* topts){
     }
   }
   nctabbed_redraw(nt);
+  ncplane_set_widget(nt->p, nt, (void(*)(void*))nctabbed_destroy);
   return nt;
 }
 
@@ -410,23 +411,25 @@ void nctabbed_destroy(nctabbed* nt){
   if(!nt){
     return;
   }
-  nctab* t = nt->leftmost;
-  nctab* tmp;
-  if(t){
-    t->prev->next = NULL;
-    if(t->next){
-      t->next->prev = NULL;
+  if(ncplane_set_widget(nt->ncp, NULL, NULL) == 0){
+    nctab* t = nt->leftmost;
+    nctab* tmp;
+    if(t){
+      t->prev->next = NULL;
+      if(t->next){
+        t->next->prev = NULL;
+      }
     }
+    while(t){
+      tmp = t->next;
+      free(t->name);
+      free(t);
+      t = tmp;
+    }
+    ncplane_destroy_family(nt->ncp);
+    free(nt->opts.separator);
+    free(nt);
   }
-  while(t){
-    tmp = t->next;
-    free(t->name);
-    free(t);
-    t = tmp;
-  }
-  ncplane_destroy_family(nt->ncp);
-  free(nt->opts.separator);
-  free(nt);
 }
 
 void nctabbed_set_hdrchan(nctabbed* nt, uint64_t chan){
diff --git a/src/poc/procroller.c b/src/poc/procroller.c
index d947a8c6c8..36d822a948 100644
--- a/src/poc/procroller.c
+++ b/src/poc/procroller.c
@@ -54,7 +54,8 @@ int main(int argc, char** argv){
   struct ncplane* std = notcurses_stdplane(nc);
   ncplane_set_scrolling(std, true);
   ncsubproc_options nopts = {};
-  struct ncsubproc* nsproc = ncsubproc_createvp(std, &nopts, *argv, argv, cb, eofcb);
+  struct ncsubproc* nsproc = ncsubproc_createvp(std, &nopts, *argv,
+                              (const char* const*)argv, cb, eofcb);
   if(nsproc == NULL){
     notcurses_stop(nc);
     return EXIT_FAILURE;
diff --git a/src/poc/selector.c b/src/poc/selector.c
index 138ce77507..1f14b236c8 100644
--- a/src/poc/selector.c
+++ b/src/poc/selector.c
@@ -39,8 +39,8 @@ run_selector(struct notcurses* nc, struct ncselector* ns){
         continue;
       }
       switch(keypress){
-        case NCKEY_ENTER: ncselector_destroy(ns, NULL); return;
-        case 'M': case 'J': if(ni.ctrl){ ncselector_destroy(ns, NULL); return; }
+        case NCKEY_ENTER: ncselector_destroy(ns); return;
+        case 'M': case 'J': if(ni.ctrl){ ncselector_destroy(ns); return; }
       }
       if(keypress == 'q'){
         break;
@@ -48,7 +48,7 @@ run_selector(struct notcurses* nc, struct ncselector* ns){
     }
     notcurses_render(nc);
   }
-  ncselector_destroy(ns, NULL);
+  ncselector_destroy(ns);
 }
 
 int main(void){
diff --git a/src/pocpp/reel.cpp b/src/pocpp/reel.cpp
index c171a15f3a..71696cca61 100644
--- a/src/pocpp/reel.cpp
+++ b/src/pocpp/reel.cpp
@@ -214,7 +214,8 @@ int main(int argc, char** argv){
   auto nstd = notcurses_stddim_yx(nc, &dimy, &dimx);
   struct ncplane* n;
   if(ncplane_putstr_aligned(nstd, 0, NCALIGN_CENTER, "(a)dd (d)el (+/-) change lines (q)uit") <= 0){
-    return -1;
+    notcurses_stop(nc);
+    return EXIT_FAILURE;
   }
   struct ncplane_options nopts = {
     .y = 1,
@@ -229,10 +230,12 @@ int main(int argc, char** argv){
   };
   n = ncplane_create(nstd, &nopts);
   if(!n){
-    return -1;
+    notcurses_stop(nc);
+    return EXIT_FAILURE;
   }
   if(ncplane_set_fg_rgb8(n, 0xb1, 0x1b, 0xb1)){
-    return -1;
+    notcurses_stop(nc);
+    return EXIT_FAILURE;
   }
   ncchannels_set_fg_rgb(&ropts.focusedchan, 0xffffff);
   ncchannels_set_bg_rgb(&ropts.focusedchan, 0x00c080);
@@ -240,9 +243,11 @@ int main(int argc, char** argv){
   auto nr = ncreel_create(n, &ropts);
   ncplane_set_userptr(n, nr);
   if(!nr){
-    return -1;
+    notcurses_stop(nc);
+    return EXIT_FAILURE;
   }
   int r = runreels(nc, nr);
+  ncreel_destroy(nr);
   if(notcurses_stop(nc)){
     return EXIT_FAILURE;
   }


Test patch:
diff --git a/src/tests/menu.cpp b/src/tests/menu.cpp
index c7416a11bb..b24d664177 100644
--- a/src/tests/menu.cpp
+++ b/src/tests/menu.cpp
@@ -76,7 +76,7 @@ TEST_CASE("Menu") {
     ncmenu_destroy(ncm);
   }
 
-  // don't call ncmenu_destroy(), invoking destruction in notcurses_stop()
+  // don't call ncmenu_destroy(), invoking destruction in notcurses_stop().
   SUBCASE("MenuNoFree") {
     struct ncmenu_item file_items[] = {
       { .desc = "I would like a new file", .shortcut = ncinput(), },
diff --git a/src/tests/plane.cpp b/src/tests/plane.cpp
index 8b3ed87551..34a18a9952 100644
--- a/src/tests/plane.cpp
+++ b/src/tests/plane.cpp
@@ -102,12 +102,25 @@ TEST_CASE("Plane") {
     CHECK(0 > ncplane_set_fg_rgb8(n_, 256, 256, 256));
   }
 
+  SUBCASE("StandardPlaneChild") {
+    struct ncplane_options nopts;
+    nopts.rows = ncplane_dim_y(n_);
+    nopts.cols = ncplane_dim_x(n_);
+    auto n = ncplane_create(n_, &nopts);
+    REQUIRE(nullptr != n);
+    CHECK(ncplane_dim_y(n) == ncplane_dim_y(n_));
+    CHECK(ncplane_dim_x(n) == ncplane_dim_x(n_));
+    CHECK(0 == ncplane_destroy(n));
+  }
+
   // Verify we can emit a NUL character, and it advances the cursor after
   // wiping out whatever we printed it atop.
   SUBCASE("EmitNULCell") {
     nccell c = CELL_CHAR_INITIALIZER('a');
     CHECK(0 < ncplane_putc_yx(n_, 0, 0, &c));
-    CHECK(0 == strcmp("a", ncplane_at_yx(n_, 0, 0, nullptr, nullptr)));
+    auto egc = ncplane_at_yx(n_, 0, 0, nullptr, nullptr);
+    CHECK(0 == strcmp("a", egc));
+    free(egc);
     unsigned y, x;
     ncplane_cursor_yx(n_, &y, &x);
     CHECK(0 == y);
@@ -115,7 +128,9 @@ TEST_CASE("Plane") {
     CHECK(0 == notcurses_render(nc_));
     c = CELL_TRIVIAL_INITIALIZER;
     CHECK(0 < ncplane_putc_yx(n_, 0, 0, &c));
-    CHECK(0 == strcmp("", ncplane_at_yx(n_, 0, 0, nullptr, nullptr)));
+    egc = ncplane_at_yx(n_, 0, 0, nullptr, nullptr);
+    CHECK(0 == strcmp("", egc));
+    free(egc);
     ncplane_cursor_yx(n_, &y, &x);
     CHECK(0 == y);
     CHECK(1 == x);
diff --git a/src/tests/plot.cpp b/src/tests/plot.cpp
index 1a753c80f4..cc0f2e203a 100644
--- a/src/tests/plot.cpp
+++ b/src/tests/plot.cpp
@@ -173,6 +173,7 @@ TEST_CASE("Plot") {
       // FIXME loop throughout plane, check all cells
       auto egc = ncplane_at_yx(ncp, 5, 49, &smask, &channels);
       CHECK(0 == strcmp(egc, "⣿"));
+      free(egc);
     }
     ncuplot_destroy(p);
   }
diff --git a/src/tests/reel.cpp b/src/tests/reel.cpp
index df88f25311..7fc6003495 100644
--- a/src/tests/reel.cpp
+++ b/src/tests/reel.cpp
@@ -127,6 +127,44 @@ TEST_CASE("Reels") {
   REQUIRE(n_);
   REQUIRE(0 == ncplane_cursor_move_yx(n_, 0, 0));
 
+  // create a reel, but don't explicitly destroy it, thus testing the
+  // context shutdown cleanup path
+  SUBCASE("ImplicitDestroy") {
+    ncreel_options r = { };
+    struct ncreel* nr = ncreel_create(n_, &r);
+    REQUIRE(nr);
+    CHECK(0 == notcurses_render(nc_));
+  }
+
+  // attempt to bind a single plane to two different reels, ensuring that it is
+  // refused by the second (and testing that error path). this ought result in
+  // the shared plane (and thus the original widget) also being destroyed.
+  SUBCASE("RefuseBoundStandardPlane") {
+    ncreel_options r = { };
+    struct ncreel* nr = ncreel_create(n_, &r);
+    REQUIRE(nr);
+    CHECK(0 == notcurses_render(nc_));
+    struct ncreel* fail = ncreel_create(n_, &r);
+    CHECK(nullptr == fail);
+    CHECK(0 == notcurses_render(nc_));
+  }
+
+  // now do the same, but with a plane we have created.
+  SUBCASE("RefuseBoundCreatedPlane") {
+    struct ncplane_options nopts{};
+    nopts.rows = ncplane_dim_y(n_);
+    nopts.cols = ncplane_dim_x(n_);
+    auto ncp = ncplane_create(n_, &nopts);
+    REQUIRE(nullptr != ncp);
+    ncreel_options r = { };
+    struct ncreel* nr = ncreel_create(ncp, &r);
+    REQUIRE(nr);
+    CHECK(0 == notcurses_render(nc_));
+    struct ncreel* fail = ncreel_create(ncp, &r);
+    CHECK(nullptr == fail);
+    CHECK(0 == notcurses_render(nc_));
+  }
+
   SUBCASE("InitLinear") {
     ncreel_options r = { };
     struct ncreel* nr = ncreel_create(n_, &r);
diff --git a/src/tests/selector.cpp b/src/tests/selector.cpp
index ddad2ba6bd..35809aa937 100644
--- a/src/tests/selector.cpp
+++ b/src/tests/selector.cpp
@@ -32,7 +32,7 @@ TEST_CASE("Selectors") {
     ncplane_dim_yx(ncsp, &dimy, &dimx);
     CHECK(4 == dimy);
     CHECK(5 == dimx);
-    ncselector_destroy(ncs, nullptr);
+    ncselector_destroy(ncs);
   }
 
   SUBCASE("TitledSelector") {
@@ -57,7 +57,7 @@ TEST_CASE("Selectors") {
     ncplane_dim_yx(ncsp, &dimy, &dimx);
     CHECK(6 == dimy);
     CHECK(strlen(opts.title) + 4 == dimx);
-    ncselector_destroy(ncs, nullptr);
+    ncselector_destroy(ncs);
   }
 
   SUBCASE("SecondarySelector") {
@@ -82,7 +82,7 @@ TEST_CASE("Selectors") {
     ncplane_dim_yx(ncsp, &dimy, &dimx);
     CHECK(4 == dimy);
     CHECK(strlen(opts.secondary) + 2 == dimx);
-    ncselector_destroy(ncs, nullptr);
+    ncselector_destroy(ncs);
   }
 
   SUBCASE("FooterSelector") {
@@ -107,7 +107,7 @@ TEST_CASE("Selectors") {
     ncplane_dim_yx(ncsp, &dimy, &dimx);
     CHECK(4 == dimy);
     CHECK(strlen(opts.footer) + 2 == dimx);
-    ncselector_destroy(ncs, nullptr);
+    ncselector_destroy(ncs);
   }
 
   SUBCASE("PopulatedSelector") {
@@ -137,7 +137,7 @@ TEST_CASE("Selectors") {
     ncplane_dim_yx(ncsp, &dimy, &dimx);
     CHECK(7 == dimy);
     CHECK(15 < dimx);
-    ncselector_destroy(ncs, nullptr);
+    ncselector_destroy(ncs);
   }
 
   SUBCASE("EmptySelectorMovement") {
@@ -162,7 +162,7 @@ TEST_CASE("Selectors") {
     sel = ncselector_previtem(ncs);
     REQUIRE(nullptr == sel);
     CHECK(0 == notcurses_render(nc_));
-    ncselector_destroy(ncs, nullptr);
+    ncselector_destroy(ncs);
   }
 
   SUBCASE("SelectorMovement") {
@@ -207,7 +207,7 @@ TEST_CASE("Selectors") {
     REQUIRE(nullptr != sel);
     CHECK(0 == strcmp(sel, items[0].option));
     CHECK(0 == notcurses_render(nc_));
-    ncselector_destroy(ncs, nullptr);
+    ncselector_destroy(ncs);
   }
 
   // Provide three items, limited to 1 shown at a time
@@ -259,7 +259,7 @@ TEST_CASE("Selectors") {
     unsigned dimy, dimx;
     ncplane_dim_yx(ncsp, &dimy, &dimx);
     CHECK(5 == dimy);
-    ncselector_destroy(ncs, nullptr);
+    ncselector_destroy(ncs);
   }
 
   // Provide three items, limited to 2 shown at a time
@@ -310,7 +310,7 @@ TEST_CASE("Selectors") {
     unsigned dimy, dimx;
     ncplane_dim_yx(ncsp, &dimy, &dimx);
     CHECK(6 == dimy);
-    ncselector_destroy(ncs, nullptr);
+    ncselector_destroy(ncs);
   }
 
   CHECK(0 == notcurses_stop(nc_));