Delete node from singly linked list with tail pointer in C
You have not specified the behavior of your list, which makes it hard for us to reason about it, and makes it hard for you to reason about it. Because this is a module about a data structure, it's a good fit for Design-by-Contract, which I recommend you read about.

Fundamentally, in DbC, each method or function has a precondition which must be met just prior to executing the code, a postcondition which must be met after executing the code, and an invariant which must be true at the start and the end of execution, as well as being true when other public methods or functions are called from within the code.

Consider your lrsll_delete(list, data) operation: the precondition is that a node with key data may or may not be present in the list. The postcondition is that the first such node will be removed from the list. (It's not clear whether you want to handle multiple nodes with the same key: do you?) 

You don't specify an invariant, but I'd suggest the invariant might be that list-&gt;head points to the first node in the list, or is NULL for an empty list, while list-&gt;tail points to the last node in the list, or is NULL for an empty list, and each node's -&gt;next field points to the next node in the list, or is NULL when the node is at the end of the list.

You should consider documenting these three conditions. The invariant is always true - you can document it once at the top of the module. You might even document it in code, in a private helper function you can call for debugging purposes: _check_invariant(list). 

The pre- and post-conditions will vary from function to function. I suggest documenting them in a block comment.

With that out of the way, let's consider your code:

lrsll_node *lrsll_delete(lrsll_list **list, char *data) {


Right off the bat, I want to ask why you're passing a lrsll_list** pointer? The "double pointer" in question is the lrsll_node**, not _list. Since the _list structure is encapsulating the head and tail pointers, and is not a node itself, I'll recommend that you just use a single level of pointer for this.

I'd also suggest that you typedef the key type. Make it a lrsll_key_t or something, not a char *.

    lrsll_node **node = &amp;(*list)-&gt;head;


This can be just &amp;list-&gt;head, if you use only one level of pointer to the list structure. 

    while (*node &amp;&amp; strcmp((*node)-&gt;data, data) != 0) {
        node = &amp;(*node)-&gt;next;
    }


This is you searching for a matching key in the list. I'll bet you have this same code in other places, like a "find" and maybe an "insert" (if keys are required to be unique). You should consider either defining a macro or a private (internal) function to do this for you.

    if (*node == NULL)
        return NULL;


Not found so return null. This is legit. Moving on:

    lrsll_node *deleted = *node;


Save a pointer to the deleted node. Okay.

    *node = deleted-&gt;next;


Update the pointer to that node. Note: It's important to note that there are two possible structures being pointed to by node. In the first case, node is initialized to point to list-&gt;head, so node might be pointing into a struct lrsll_list. After the first node, you update with node = &amp;(*node)-&gt;next which will be a pointer to a struct lrsll_node. So the node double-pointer points into two possible kinds of struct. That's awkward.

    //is there a way to get rid of this conditional check 
    //using pointers in a more intelligent manner?
    if (deleted == (*list)-&gt;tail) {
        (*list)-&gt;tail = NULL;
    }


To answer your question, there's no good way that I know of - nor even a bad way - to get an update to the list-&gt;tail pointer without either destroying it and rebuilding it (by using the tail as your temporary, and then scanning-to-end once you're done), or comparing it and updating it if needed.

Suppose you use tail as some kind of previous node pointer? Well, that's the situation you are determined to avoid with the whole "Linus' double pointer" scenario. So I'll assume you don't want to do that.

Then how can you safely update the tail pointer? Well, consider this:

head -&gt; node1 -&gt; null
tail ---^


In the case where the list contains only one element, what happens if you delete it?

node -&gt; &amp;head -&gt; node1


First, node points to &amp;head. Then *node-&gt;data matches, because I said so. So you delete the node by setting *node = deleted-&gt;next (which is NULL in this case). So now you have:

node -&gt; &amp;head -&gt; NULL
         tail -&gt; node1


So in this case, your conditional code is correct:

    if (deleted == list-&gt;tail) 
        list-&gt;tail = NULL;


And produces this result:

node -&gt; &amp;head -&gt; NULL
         tail -&gt; NULL


Deleting the 1 node in a 1-node list produces an empty list, which is stored as shown.

But now consider a two node list where the last node is deleted:

head -&gt; node1 -&gt; node2 -&gt; NULL
tail ------------^


After searching, you have:

node -&gt; &amp;(node1-&gt;next) -&gt; node2


You set *node = (*node)-&gt;next which is NULL, and get:

head -&gt; node1 -&gt; NULL
node ---^.next // node points to &amp;node1.next
tail -&gt; node2            


So you run your test:

    if (deleted == list-&gt;tail) 
        list-&gt;tail = NULL;


Which passes, because you did delete the last node in the chain, but now you get:

head -&gt; node1 -&gt; NULL
tail -&gt; NULL


And this is wrong. Tail should point to node1, not to NULL.

How can you fix this? By adding another condition: when updating the tail pointer, check to see if the node pointer is still pointing to &amp;list-&gt;head. If so, then go ahead and null it out because you know you have a 1-element list being shortened to an empty list. But if node points to some other location, and you're deleting the tail element, then you can set tail to point to the address of the struct containing the next pointer that is pointed to by node. (Say that five times fast!)

Fortunately, the next pointer is the first element in the struct, so in C you can assume that the two addresses are the same, and just cast one type to the other. (In C++, with a vtable, the addresses wouldn't be the same. In C with any other field at the top of the struct, the addresses wouldn't be the same. That's what the offsetof macro is for! You could subtract the offset, but ... c'mon! This is C! Just hard-code the 0 and move on...)

Something like this:

    if (deleted == list-&gt;tail) {
        list-&gt;tail = (node == &amp;list-&gt;head) ? NULL : (lrsll_node*)node;
    }


Finally:

    return deleted;
};


You don't need that last semi-colon.