Skip to content

libsubprocess: fix consistency issues and bugs - #7744

Open
chu11 wants to merge 28 commits into
flux-framework:masterfrom
chu11:libsubprocess_misc_cleanup_4
Open

libsubprocess: fix consistency issues and bugs#7744
chu11 wants to merge 28 commits into
flux-framework:masterfrom
chu11:libsubprocess_misc_cleanup_4

Conversation

@chu11

@chu11 chu11 commented Jul 24, 2026

Copy link
Copy Markdown
Member

part 4 of cleanups, from on top of #7743 (review earlier PRs first, there's a lot of commits).

This set of fixes are minor but takes a minor bit of thought :-) mostly they are consistency issues between functions (some check errors, some don't), or initialization inconsistencies, etc.

chu11 added 28 commits August 4, 2026 14:06
Problem: local_channel_flush() calls subprocess_incref() then returns
early if fbuf_read_watcher_get_buffer() fails, skipping the matching
subprocess_decref().

Replace the early return with goto out so the decref runs on every
path.

Assisted-by: Claude:claude-opus-4-8 <noreply@anthropic.com>
Problem: server_killall() and exec_exit_notify() always returns 0 and all
call sites ignore the return value.

Change the return type to void.

Assisted-by: Claude:claude-opus-4-8 <noreply@anthropic.com>
Problem: subprocess_destroy() returns -1 when flux_future_then()
fails but never destroys the future created by
flux_subprocess_kill(), leaking it.

Destroy the future on the error path.

Assisted-by: Claude:claude-opus-4-8 <noreply@anthropic.com>
Problem: subprocess_destroy_finish() looks up the "flux_t" aux key on
the subprocess to log a kill failure, but nothing ever sets that key,
so the handle is always NULL and flux_log_error() is called with a NULL
handle, losing the log.

Set the "flux_t" aux key in subprocess_destroy() before registering the
continuation.

Assisted-by: Claude:claude-opus-4-8 <noreply@anthropic.com>
Problem: two rexec.write error sites pass ": %s", strerror(errno) to
log_err(), but log_err() already appends ": strerror(errno)", so the
errno string is printed twice.

Drop the redundant strerror argument.

Assisted-by: Claude:claude-opus-4-8 <noreply@anthropic.com>
Problem: subprocess_check_completed() logs an "unexpected state"
condition with log_err(), which appends ": strerror(errno)" -- a
meaningless stale errno for a non-errno condition.

Use log_msg() instead.

Assisted-by: Claude:claude-opus-4-8 <noreply@anthropic.com>
Problem: subprocess_create() gated the atomic SOCK_CLOEXEC socketpair on
"#if SOCK_CLOEXEC".  On glibc SOCK_CLOEXEC is an enum constant (with a
self-referential macro), so the preprocessor evaluates the identifier to
0 in #if, making the atomic branch dead and always using the non-atomic
fallback.

Use #ifdef, which correctly detects the macro definition.

Assisted-by: Claude:claude-opus-4-8 <noreply@anthropic.com>
Problem: cmd_option_bufsize() and cmd_option_line_buffer() declare var
uninitialized, then "goto cleanup" on asprintf failure where cleanup
calls free(var).  asprintf leaves *strp indeterminate on failure, so
free(var) operates on a garbage pointer.

Initialize var = NULL so the cleanup free is a safe no-op on the
asprintf-failure path.

Assisted-by: Claude:claude-opus-4-8 <noreply@anthropic.com>
Problem: flux_subprocess_write() compared fbuf_space() (int) against
len (size_t) and passed len to fbuf_write()'s int parameter.

Add explicit casts.

Assisted-by: Claude:claude-opus-4-8 <noreply@anthropic.com>
Problem: argz_appendv() allocates s with vasprintf() but only frees it
on the success path.  When argz_add() fails, the function returns
without freeing s, leaking it (argz_add copies the string and does not
take ownership).

Free s on the argz_add() failure path.

Assisted-by: Claude:claude-opus-4-8 <noreply@anthropic.com>
Problem: the posix_spawn_file_actions_* and posix_spawnattr_setsig*
functions return a positive errno on failure (0 on success), not
-1/errno.  spawn_setup_fds() and setup_signals() tested them with "< 0",
so those checks were dead and failures went undetected; setup_signals()
also returned setsigdefault()'s positive errno, which the caller tested
with "< 0" and missed.

Capture each return and convert to "!= 0" with errno = rc.  The
sigemptyset/sigaddset and fdwalk checks correctly use -1/errno and are
left as < 0.

Assisted-by: Claude:claude-opus-4-8 <noreply@anthropic.com>
Problem: flux_cmd_copy() left zlist_dup() and z_hash_dup() unchecked, so
an allocation failure could produce a cmd with NULL channels/opts -- a
silently partial copy.  z_hash_dup() itself ignored zhash_new() and
zhash_insert() failures and dereferenced a possibly-NULL hash via
zhash_autofree().

Check both dups in flux_cmd_copy(), and check every allocation in
z_hash_dup(), returning NULL on any failure.

Assisted-by: Claude:claude-opus-4-8 <noreply@anthropic.com>
Problem: cmd_tojson() did not check the json_object() return for NULL,
unlike the other *_tojson helpers in the file; on OOM it would pass
NULL to json_object_set_new().

Add a NULL check that jumps to the existing err path.

Assisted-by: Claude:claude-opus-4-8 <noreply@anthropic.com>
Problem: zhash_fromjson() and channels_fromjson() call zhash_autofree()
/ zlist_autofree() on the result of zhash_new() / zlist_new() without a
NULL check.  Both autofree functions assert(self) and dereference it,
so an allocation failure asserts or crashes.

Check the allocation and goto the existing fail path with errnum =
ENOMEM, matching msgchans_fromjson().

Assisted-by: Claude:claude-opus-4-8 <noreply@anthropic.com>
Problem: bulk_exec_create() did not check zlist_new() (x2) or
idset_create() for NULL, so an allocation failure left
exec->processes/commands/exit_batch NULL and later appends and idset
operations would act on NULL.

Check all three and goto the existing error path, which frees the
partially-built struct via bulk_exec_destroy().

Assisted-by: Claude:claude-opus-4-8 <noreply@anthropic.com>
Problem: server_disconnect_cb() passes flux_msg_route_first (p->waiter)
directly to streq(), but flux_msg_route_first() can return NULL, which
streq() (a bare strcmp) would dereference.  Other call sites null-check
the result first.

Store the route and null-check it before streq().

Assisted-by: Claude:claude-opus-4-8 <noreply@anthropic.com>
Problem: subprocess_childfds() ignores the return of idset_set() at
three sites.  The idset autogrows, so idset_set() can still fail on an
allocation error; a silent failure would leave an fd unprotected and it
would be closed in the child.

Check each idset_set() and destroy the idset on failure.

Assisted-by: Claude:claude-opus-4-8 <noreply@anthropic.com>
Problem: add_pending_signal() ignores the flux_subprocess_aux_set()
return.  On failure it still set p->signal_pending and incref'd the
future, so fwd_pending_signal() would later run, aux_get NULL for
"sp::signal_future", never fulfill the caller's future, and leak the
incref'd reference.

Destroy the future and return NULL on aux_set failure, before setting
signal_pending or taking the extra reference.

Assisted-by: Claude:claude-opus-4-8 <noreply@anthropic.com>
Problem: create_process_spawn() ignores the return value of several
functions that can return errors.

Check return values for cmd_env_expand(), cmd_argv_expand(), and
spawn_setup_fds().

Assisted-by: Claude:claude-opus-4-8 <noreply@anthropic.com>
Problem: env_entry_name() checks "len-1 < p - entry" with size_t len.
If len is 0, len-1 underflows to SIZE_MAX, the truncation guard passes,
and *dst = '\0' writes to a zero-size buffer.  All current callers pass
sizeof(buf)==1024, so this is latent.

Reject len == 0 explicitly before the subtraction.

Assisted-by: Claude:claude-opus-4-8 <noreply@anthropic.com>
Problem: The *_fromjson helpers in command.c report inconsistent errno
values for the same "JSON is the wrong type" condition -- argz_fromjson,
envz_fromjson, and msgchans_fromjson used EINVAL while zhash_fromjson and
channels_fromjson use EPROTO.

Standardize all five on EPROTO, matching how malformed input is reported
elsewhere in the deserialization and RFC 42 server paths.

Assisted-by: Claude:claude-opus-4-8 <noreply@anthropic.com>
Problem: argz_fromjson() and envz_fromjson() return EPROTO regardless
of the error.

Return ENOMEM when there is a memory allocation error.
Problem: bulk_exec_write() compares the int return of
flux_subprocess_write() against a size_t len ("< len").  On error the
-1 return converts to SIZE_MAX, so the comparison is false and the
write failure is silently swallowed.  The len <= 0 guard is also a
dead comparison since len is unsigned.

Capture the return from flux_subprocess_write() in a signed int,
fail on < 0, and also fail with ENOSPC when fewer than len bytes
were buffered.

Assisted-by: Claude:claude-opus-4-8 <noreply@anthropic.com>
Problem: server_write_cb() calls err_init() on its flux_error_t before
server_auth_unpack(), but five other request handlers do not.  If the
auth callback path in server_auth_unpack() returns -1 without populating
errp, those handlers read error.text uninitialized.

Add err_init() before server_auth_unpack() in the exec, kill, list,
wait, and attach handlers to match server_write_cb().

Assisted-by: Claude:claude-opus-4-8 <noreply@anthropic.com>
Problem: flux_rexec_bg() performs no valid_flags mask check, unlike the
other public exec entry points, so invalid flags pass through silently
into the rexec request payload.  Only LOCAL_UNBUF was rejected, and that
check lives downstream in subprocess_rexec_bg().

Add a valid_flags check (NO_SETPGRP | FORK_EXEC | WAITABLE | SIGN) in
flux_rexec_bg(), matching the sibling entry points; LOCAL_UNBUF and
STDIO_FALLTHROUGH are now rejected there.  Remove the now-unreachable
LOCAL_UNBUF check in subprocess_rexec_bg().

Assisted-by: Claude:claude-opus-4-8 <noreply@anthropic.com>
Problem: server_exec_cb() passes local_flags straight to
flux_local_exec_ex() without screening LOCAL_UNBUF.  LOCAL_UNBUF is a
client-side output optimization with no meaning for a background
subprocess launched by the server; it would fail late in
flux_local_exec_ex() with a generic "error launching process" message.

Reject LOCAL_UNBUF in background mode with a clear error, consistent
with the adjacent stdio-fallthrough check.

Assisted-by: Claude:claude-opus-4-8 <noreply@anthropic.com>
Problem: subprocess_rexec_bg() did not validate h or cmd, unlike its
sibling entry points subprocess_rexec() and subprocess_rexec_attach().
A NULL cmd would be dereferenced in cmd_tojson().

Add an "if (!h || !cmd)" EINVAL guard.  service_name is intentionally
not checked, since NULL is valid here and defaults to "rexec".

Assisted-by: Claude:claude-opus-4-8 <noreply@anthropic.com>
Problem: local_release_child() handled read() returning < 0 (error) and
sizeof(int) (exec error), then fell through assuming n == 0 (child
exec'ed).  A short read (0 < n < sizeof(int)) also fell through and was
treated as a successful exec.

Treat any n that is neither 0 nor sizeof(int) as a protocol error.

Assisted-by: Claude:claude-opus-4-8 <noreply@anthropic.com>
@chu11
chu11 force-pushed the libsubprocess_misc_cleanup_4 branch from 32d4cbc to 9dedb55 Compare August 4, 2026 22:13
@codecov

codecov Bot commented Aug 4, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 48.30508% with 61 lines in your changes missing coverage. Please review.
✅ Project coverage is 83.91%. Comparing base (c8b95ea) to head (9dedb55).
⚠️ Report is 1 commits behind head on master.

Files with missing lines Patch % Lines
src/common/libsubprocess/command.c 45.16% 17 Missing ⚠️
src/common/libsubprocess/posix_spawn.c 44.44% 15 Missing ⚠️
src/common/libsubprocess/subprocess.c 40.90% 13 Missing ⚠️
src/common/libsubprocess/bulk-exec.c 33.33% 10 Missing ⚠️
src/common/libsubprocess/server.c 80.00% 3 Missing ⚠️
src/common/libsubprocess/fork.c 33.33% 2 Missing ⚠️
src/common/libsubprocess/local.c 50.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #7744      +/-   ##
==========================================
- Coverage   83.94%   83.91%   -0.03%     
==========================================
  Files         591      591              
  Lines      100922   100983      +61     
==========================================
+ Hits        84716    84738      +22     
- Misses      16206    16245      +39     
Files with missing lines Coverage Δ
src/common/libsubprocess/client.c 79.29% <100.00%> (ø)
src/common/libsubprocess/util.c 90.47% <100.00%> (ø)
src/common/libsubprocess/local.c 81.85% <50.00%> (+0.07%) ⬆️
src/common/libsubprocess/fork.c 74.81% <33.33%> (-0.95%) ⬇️
src/common/libsubprocess/server.c 84.84% <80.00%> (-0.24%) ⬇️
src/common/libsubprocess/bulk-exec.c 79.57% <33.33%> (-1.78%) ⬇️
src/common/libsubprocess/subprocess.c 87.20% <40.90%> (-1.04%) ⬇️
src/common/libsubprocess/posix_spawn.c 75.29% <44.44%> (-13.12%) ⬇️
src/common/libsubprocess/command.c 71.07% <45.16%> (-1.84%) ⬇️

... and 20 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant