diff --git a/CHANGELOG.md b/CHANGELOG.md index f5edb30e..3da66aef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ CHANGELOG --bind 'result:transform-header(echo result: $FZF_MATCH_COUNT),result-final:transform-footer(echo final: $FZF_MATCH_COUNT)' ``` - Bound `alt-left` to `backward-word` and `alt-right` to `forward-word` by default (#4833) +- Skip `$FZF_CURRENT_ITEM` export when the item is larger than 64 KB; a huge item can overflow `ARG_MAX` and break preview and other child commands with `E2BIG` (#4806) 0.73.1 ------ diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 9ea726dd..2b675330 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -1534,6 +1534,9 @@ fzf exports the following environment variables to its child processes. .PP .B FZF_CURRENT_ITEM is omitted when the item contains a NUL byte, because exec(2) cannot pass it. +It is also omitted when the item is larger than 64 KB, so that a huge item +cannot overflow the environment size limit and break preview and other child +commands. .SH EXTENDED SEARCH MODE diff --git a/src/terminal.go b/src/terminal.go index 49994522..d4ced8e6 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -68,6 +68,10 @@ const maxFocusEvents = 10000 // After this duration, users can press CTRL-C to terminate the command. const blockDuration = 1 * time.Second +// Skip exporting FZF_CURRENT_ITEM when the item is larger than this, so a huge +// item cannot overflow ARG_MAX and break exec for preview and other commands. +const maxCurrentItemEnvSize = 64 * 1024 + func init() { placeholder = regexp.MustCompile(`\\?(?:{[+*sfr]*[0-9,-.]*}|{q(?::s?[0-9,-.]+)?}|{fzf:(?:query|action|prompt)}|{[+*]?f?nf?})`) whiteSuffix = regexp.MustCompile(`\s*$`) @@ -1444,8 +1448,10 @@ func (t *Terminal) environImpl(forPreview bool) []string { env = append(env, fmt.Sprintf("FZF_COLUMNS=%d", t.areaColumns)) env = append(env, fmt.Sprintf("FZF_POS=%d", min(t.merger.Length(), t.cy+1))) if item := t.currentItem(); item != nil { - // Skip if the value contains a NUL byte; exec(2) would reject the env. - if s := item.AsString(t.ansi); !strings.ContainsRune(s, 0) { + // Skip if the value contains a NUL byte (exec(2) would reject the env) + // or is too large (a huge item can overflow ARG_MAX and break exec + // entirely for preview and other child commands). + if s := item.AsString(t.ansi); !strings.ContainsRune(s, 0) && len(s) <= maxCurrentItemEnvSize { env = append(env, "FZF_CURRENT_ITEM="+s) } } diff --git a/test/test_core.rb b/test/test_core.rb index 56fd1f64..2dcd0ce5 100644 --- a/test/test_core.rb +++ b/test/test_core.rb @@ -2346,6 +2346,26 @@ class TestCore < TestInteractive end end + def test_env_current_item_size_limit + preview = %[(echo START; env | grep '^FZF_CURRENT_ITEM='; echo END) > #{tempname}] + # Large item (> 64 KB) is omitted so it cannot overflow ARG_MAX and break exec + tmux.send_keys %(head -c 70000 /dev/zero | tr '\\0' a | #{FZF} --preview-window 0 --preview "#{preview}"), :Enter + wait do + content = File.exist?(tempname) ? File.read(tempname) : '' + assert_includes content, 'END' + refute_includes content, 'FZF_CURRENT_ITEM=' + end + tmux.send_keys :Enter + FileUtils.rm_f(tempname) + # Smaller item is exported as usual + tmux.send_keys %(head -c 1000 /dev/zero | tr '\\0' a | #{FZF} --preview-window 0 --preview "#{preview}"), :Enter + wait do + content = File.exist?(tempname) ? File.read(tempname) : '' + assert_includes content, 'END' + assert_includes content, 'FZF_CURRENT_ITEM=' + ('a' * 1000) + end + end + def test_abort_action_chain tmux.send_keys %(seq 100 | #{FZF} --bind 'load:accept+up+up' > #{tempname}), :Enter wait do