Disclaimer: I do not consider myself an experienced OSS contributor, but rather someone beginning to understand how OSS actually works.
Since I started programming I've always wanted to contribute to OSS. I do not really know why in particular, but the idea of contributing to something that is used by other people in a non-commercial way seems like a great idea. I tried approaching it in different ways: the hacktoberfest, projects with open issues, implementing random features, etc. None of them worked. I did manage to merge some PRs, but they were not really meaningful for the project nor for me. They were chore
s. At some point I really thought it was something not meant for me, and that it had a really high barrier to entry.
Nevertheless, a while ago I found a way to actually contribute, and meaningfully! And it's as simple as this: Don't try to contribute, just build something that you feel passionate about! This may seem as a contradiction, but I do believe that a it's a great way (at least for me) to eventually contribute to OSS. Let's break it down.
Picture yourself building something that is really meaningful for you. Chances are that whatever you do will make use of others' work, and that work most of the time is open source. If you use Javascript or Python (or your favourite language to beat the averages) probably some of the libraries you will be using have a Github page with the code. If you are a curious person (which you should be if you are building something) then you for sure have some of the libraries repos open in one of your x > 100
tabs in your browser.
When you are at this point, you are motivated and aiming to have something working as soon as possible, when suddenly you encounter an issue... You spend a few hours debugging (thinking your code is shit) when you finally find the issue: library X does not handle that specific edge case... You then proceed to clone the repo locally, spend another few hours setting it up, debugging, testing and fixing the issue. You point your project to the local (or forked) version and boom, it works! Congrats, you just contributed to OSS 😉
The world is smaller than we think (how many times you found someone in a completely random place that blew your mind? [1]) and almost certainly someone will fall into one of these categories:
And I'm not even talking about features here, but rather real issues in the code that appear the same way they do in the company you work where there's an infinite backlog of bugs. But you just fixed one, hence it's meaningful both for you and for the person that will not encounter it after you make your contribution.
Obviously, take this with a grain of salt. You may not directly or as easily contribute to React or numpy
, but maybe some of the newer projects that you decide to use yes. Moreover, there's tons of new projects now with all the AI hype. In my case the one I want to talk about is browser-use
.
I quit my job a few months ago aiming to start my own company: a software company. I won't go into the details of it yet as we are in stealth still, but we are building a solution to help companies in the nightmare of QA.
For that we use Playwright and (as it's 2025) AI. And browser-use
is a great way to have both. It's a Python library that let you interact with a browser using natural language, while maintaining control over the Playwright objects, which are the ones you would use in a good old E2E. As we rely a lot on browser control, and browser-use
is in extremely active development, we find bugs from time to time. For some of them we found workaround, but for other we needed to do changes in the library to fix the bug.
For example, at some point there was a missing try ... except
in a part of the code, and it was failing in the main
branch. We want to use the latest possible version of the library to make use of the new features asap, we found it immediately after a regular upgrade. I did some investigation in the fork I already had, and found that there was some code that when the agent got into a n > 1
step would fail due to a function already being registered in the browser session. It was an easy fix, we just needed to add the check for that specific failure, and continue, as it was not really an operational issue. I did the PR, ran the tests locally, linted, talked with the maintainers and in less than 24h it was merged!
This is just an example, but I'm sure you'll find plenty of other examples in your work, and probably most of the PRs in browser-use
come from people outside the org that are in a similar situation. They are building something meaningful for themselves, found that something was broken or missing, and decided to just fix it.
On the flip-side, contributing is not always as smooth as the example above. Sometimes you end up in an infinite loop of comments, maintainers take long to reply [2], you end up maintaining a fork as the source of truth due to the dependency you have on that one issue being fixed... But it's all part of the process. Life is not always easy, and that's fine. You just need to find ways to keep pushing, and pushing. And it will come.
I would like to dig a bit deeper on the topic of maintaining a fork as the source of truth. In our case, we use python with uv
as the package manager. And as we had the fork we used it as the package download location to ensure any fixes that are pending a merge are used by our code. Here's a snippet of how to do it in the pyproject.toml
:
...
dependencies = [
"browser-use",
...
]
[tool.uv.sources]
browser-use = { git = "https://github.com/tarasyarema/browser-use", branch = "taras-main" }
...
This snippet will make sure that when you run uv sync
the code will be pulled from the fork with the correct branch, commit, etc. Here are the docs from uv
around git dependencies.
For example, at the time of writing we have a few PRs open that actually fix some edges cases we found. To use those, we created branch (taras-main
) that have both PR branches merged into it.
I really hope this post sparked a bit of a hype to build something, which in the end is the seed of how all the projects that we use were created in the first place. Who knows, maybe one day I'll be the one reviewing PRs in a projects that other people use... It's good to day-dream from time to time 🌝
[1] I once found a friend of mine in Thailand during a two week trip, I live in Spain... [2] This is not beef to them, as I assume they have plenty of work so thanks for when they do reply fast!
]]>Sadly, because it's 2025, I want to note that this was written entirely by me and no LLMs were involved in the process.
Lately at Capchase we've been working with the Stripe API. The use case can be easily simplified: we need to keep some client data on our side so that we can operate with it easier than fetching it every time. For this purpose we use Go, and with it comes all the marshaling and unmarshaling that we need to handle when working with an API.
Good to us, Stripe offers a handy Go package to work with the API without having to handle all the calls manually. To illustrate this, let's consider that we want to get a specific coupon, for that we could code the following function
import (
"github.com/stripe/stripe-go/v72"
"github.com/stripe/stripe-go/v72/coupon"
)
// We consider that the initial API key setup has been done
// stripe.Key = "your_stripe_api_key"
// GetCoupon returns the Stripe coupon for the given id
func GetCoupon(id string) (*stripe.Coupon, error) {
// Set wanted parameters...
params := &stripe.CouponParams{}
return coupon.Get(id, params)
}
Now, whenever we call cp, err := GetCoupon(someID)
we will get a Stripe coupon
object cp
. Now, let's have a look at the definition of this struct:
// Coupon is the resource representing a Stripe coupon.
// For more details see https://stripe.com/docs/api#coupons.
type Coupon struct {
APIResource
// Primitive fields
AmountOff int64 `json:"amount_off"`
Created int64 `json:"created"`
Deleted bool `json:"deleted"`
DurationInMonths int64 `json:"duration_in_months"`
ID string `json:"id"`
Livemode bool `json:"livemode"`
MaxRedemptions int64 `json:"max_redemptions"`
Name string `json:"name"`
Object string `json:"object"`
PercentOff float64 `json:"percent_off"`
RedeemBy int64 `json:"redeem_by"`
TimesRedeemed int64 `json:"times_redeemed"`
Valid bool `json:"valid"`
// Non-primitive fields
AppliesTo *CouponAppliesTo `json:"applies_to"`
Currency Currency `json:"currency"`
Duration CouponDuration `json:"duration"`
Metadata map[string]string `json:"metadata"`
}
Note that I re-ordered the fields so that we have them divided into two groups, the first one with only primitive type fields, that is numerics, strings and booleans, and the following with the rest.
In particular, when working with Stripe coupons, only one of the fields AmountOff
and PercentOff
may be set. In fact in the official Stripe API docs we can find
this example coupon object
{
"id": "18OmM8HA",
"object": "coupon",
"amount_off": 25,
"created": 1614605969,
"currency": "usd",
"duration": "repeating",
"duration_in_months": 3,
"livemode": false,
"max_redemptions": 10,
"metadata": {
"test": "test"
},
"name": "25 off",
"percent_off": null,
"redeem_by": 1766448000,
"times_redeemed": 0,
"valid": true
}
Where percent_off
is not set. The funny part comes when you marshal the Go coupon object,
which would look something like
{
"id": "18OmM8HA",
"object": "coupon",
"amount_off": 25,
"created": 1614605969,
"currency": "usd",
"duration": "repeating",
"duration_in_months": 3,
"livemode": false,
"max_redemptions": 10,
"metadata": {
"test": "test"
},
"name": "25 off",
"percent_off": 0,
"redeem_by": 1766448000,
"times_redeemed": 0,
"valid": true
}
Why we get a 0
in the percent_off
field?
If you have a closer look at the stripe.Coupon
struct definition we
see that the primitive fields are all non-pointer. Hence, we marshal the
struct cp
into bytes and look at it we get zero values
for all those primitive types which were not set initially (PercentOff
, for instance).
TL;DR: Use pointers 🥴
If we change the primitive fields to be pointers this problem is solved.
As, when a field is unmarshaled from a null
value, we will get a Go nil
of the field type. This may be an issue because it forces the user to
handle pointer logic. But one may use pointers always if the returned
data can be nullable, because if not false data will eventually be served.
To illustrate, in a more simple example and without using any external package, consider the following code which can be seen in a playground:
package main
import (
"encoding/json"
"fmt"
)
type Drama struct {
Primitive int `json:"primitive"`
Ptr *int `json:"ptr"`
}
func main() {
// Both fields empty
d := Drama{}
b, _ := json.MarshalIndent(d, "", " ")
fmt.Println(string(b))
}
The code outputs the following JSON string
{
"primitive": 0,
"ptr": null
}
The actual problem of this is that the end user of the Stripe package (or any other
package that do not use pointers on nullable fields) it is literally impossible to
know whether the field PercentOff
was initially empty or not.
When we found the described issue, I immediately created an issue in the package repo, and after getting a better implement your own package response from one of the contributors we decided to fork...
After forking the package repo to our pointered version of stripe-go I spend a number of (not few) hours migrating every primitive field of every struct of stripe to be pointers and making the tests pass... Which end up being much harder than expected because of a hidden forgotten channel in one of the tests (I may talk about that another day).
Finally, we deployed our own version and started using it, which yielded to the wanted results!
If you liked this post ping me on Twitter and... We're hiring! Have a look the open positions at Capchase here.
]]>This post is about the day 8 problem of the Advent Of Corona challenge.
You are given 3 TCP
connections:
68.183.210.250:32779
68.183.210.250:32780
68.183.210.250:32781
Your goal is to get a flag, but... Which flag?
Disclaimer: I won't give the flags here. Try to solve it by your own, or run the solution script after understanding it if you want to get the flags. It's up to you in the end...
If you play with the test connection, for example running it via telnet
:
$ telnet 68.183.210.250 32779
You see a tic-tao-toe game. If you win the bot you will get a flag in the format flag{xxx}
.
As the problem states, there's a timeout of 2s and 3s for the first and second phase, respectively.
My approach is to try to solve it via a random algorithm, i.e. just pick a random empty cell every time.
Here is the code:
from telnetlib import Telnet
from time import time
connections = [
("test", "68.183.210.250", 32779, 3),
("phase_0", "68.183.210.250", 32780, 3),
("phase_1", "68.183.210.250", 32781, 6)
]
for t, h, p, s in connections:
tn = Telnet(h, p)
tn.read_until(str.encode("give me a name: "))
tn.write(str.encode("taras"))
tn.read_until(str.encode("\n"))
tt = time()
while True:
try:
d = []
d_tmp = []
for _ in range(s):
try:
tmp = tn.read_until(str.encode("\n"))
except Exception:
flag = bytes.decode(d_tmp[0]).replace("\n", "")
print(f"{t}: {flag} - {time()-tt:2.4f} s.")
raise Exception("end")
d_tmp.append(tmp)
d += bytes.decode(tmp).replace("\n", "").split("|")[1:-1]
d = [ dd.replace(" ", "") for dd in d ]
tn.read_until(str.encode("give me a position (comma separated): "))
for i in range(len(d)):
if d[i] == "":
ss = f"{i//s},{i%s}\n"
tn.write(str.encode(ss))
break
except Exception:
break
You may have to run it a few times if it does not solve it in the first run.
It was written in Go and deployed via Docker :3
If you are interested in how I created this challenge here you can have a look!
]]>This post is about the day 6 problem of the Advent Of Corona challenge.
You are given a binary named lagrange_baby
. That's all.
The first thing to do when you get a binary in a CTF-like challenge like this is taking a look at what bash commands like file
or strings
output you.
$ file lagrange_baby
lagrange_baby: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV),
dynamically linked, interpreter /lib64/l,
for GNU/Linux 3.2.0, BuildID[sha1]=76154f43151d7962511001b781c47a617a01b9bd,
not stripped
$ strings -d lagrange_baby
/lib64/ld-linux-x86-64.so.2
libc.so.6
srand
__isoc99_scanf
puts
__stack_chk_fail
printf
strlen
malloc
__cxa_finalize
__libc_start_main
free
GLIBC_2.7
GLIBC_2.4
GLIBC_2.2.5
_ITM_deregisterTMCloneTable
__gmon_start__
_ITM_registerTMCloneTable
gfff
gfff
VUUU
VUUU
AWAVI
AUATL
[]A\A]A^A_
noup
still noup
aaaaaaaaand stil noup
flag{\%s}
;*3$"
From these outputs we know the following things:
x64
executable.noup
, still noup
, aaaaaaaaand stil noup
and flag{\%s}
.
There is nothing similar to a flag inside it, so I can suppose that it's generated in execution time.Let's take a deeper look into the binary. I choose Ghidra, but it can be done in any other GUI or terminal tool (r2, cutter, ida, ...).
Before doing anything we will try to mess around with the binary:
$ ./lagrange_baby
a
noup
$ ./lagrange_baby
1
noup
$ ./lagrange_baby
aaaaaaaaaaaaaaaaaaa
noup
Hm, interesting...
The basic steps to begin analyzing a binary are the following:
Pretty straight-forward.
Just analyze all.
First of all, I look that the functions panel and search for the main
function.
When you click at it it shows up a C-like decompiled view of the function.
One of the things I like about Ghidra is that it lets you redefine variable types and names. So you can change those definitions during the reversing and the flow is much easier to follow.
The first thing I do is redefine the main function to be int main (void)
and the __isoc99_scanf
to scanf
.
I notice that there are two scanf
in the main function: 00100c17
and 00100d2e
.
So I redefine the values written to as input_1
and input_2
.
We now know the expected types of inputs: uint input_2
and char *input_2
.
As we know that the first input is a positive integer, we return and play around a bit:
$ ./lagrange_baby
0
noup
$ ./lagrange_baby
1
noup
$ ./lagrange_baby
2
noup
$ ./lagrange_baby
3
noup
$ ./lagrange_baby
4
noup
$ ./lagrange_baby
5
noup
$ ./lagrange_baby
6
a
still noup
Hm, so 6
is an accepted first input. Good, but why?
We will suppose that the function isprime
returns what its name says if the number given as input is prime.
So if we look at the first part of the main function (I renamed some variables to make it more readable) we have the following flow
scanf(&io_input,&input_1);
srand(input_1); // init rand with seed the input value
var_1 = 0xd; // 0xd = 13 in decimal
do {
// this while loops var_1 from 13 untill it breaks
var_1_prime = isprime(var_1); // Check if the current var_1 is prime
if ((int)var_1_prime != 0) {
// if its prime we take its remainder mod 10 and check if its prime
var_1_prime = isprime((int)var_1 % 10);
if ((int)var_1_prime != 0) {
// if it is we take the integer division of var_1 by 10 and
// check if is divisble by 3
var_2 = (int)var_1 / 10;
// then this strange wtf variable comes up...
if (((int)var_2 % 3 == 0) &&
(wtf = (uint)((int)var_2 >> 0x1f) >> 0x1f, (int)var_2 % 3 == (var_2 + wtf & 1) - wtf))
break; // if this strange condition is satisfied we exit the loop
}
}
// var_1++
var_1 = var_1 + 1;
} while( true );
As I do not really understand this loop, I decided to implement a analogy of this in Python:
def isprime(a):
return not (a < 2 or any(a % x == 0 for x in range(2, int(a ** 0.5) + 1)))
var_1 = 0xd
while True:
var_1_prime = isprime(var_1)
if var_1_prime:
var_1_prime = isprime(var_1 % 10)
if var_1_prime:
var_2 = var_1 // 10
wtf = (var_2 >> 0x1f) >> 0x1f
if ((var_2 % 3 == 0) and (var_2 % 3 == (var_2 + wtf & 1) - wtf)):
break
var_1 = var_1 + 1
print(var_1, var_2)
When I run it I get this output
$ python3 loop.py
67 6
Nice! We know that var_1=67
, and var_2=6
. That's where the initial 6
we found came from.
Let's take a look at the next condition
// checks if input_1 is 6
if (var_2 == input_1) {
// input_2 will be 6 *char
input_2 = (char *)malloc((long)(int)input_1);
// read input_2
scanf(&DAT_00100ea0,input_2);
input_2_len = strlen(input_2);
// check if the input_2 is really 6 chars long
if (input_2_len == (long)(int)input_1) {
// init a var_3 to 0, which we will loop while
// its less than 6, i.e. var_3 = 0, 1, 2, 3, 4, 5
var_3 = 0;
while (var_3 < (int)input_1) {
// pick var_3-th character of the input_2
char_i = input_2[(long)var_3];
// evaluate some random function
epic_function_output = epic_function(var_3 + 1);
// check if the current character of the input (in decimal form)
// is the same as the function return value
if ((int)char_i != (int)epic_function_output) {
puts("aaaaaaaaand stil noup");
free(input_2);
return_var = 1;
goto LAB_00100def;
}
var_3 = var_3 + 1;
}
printf("flag{\%s}\n",input_2);
free(input_2);
return_var = 0;
}
else {
// the message we got before
puts("still noup");
free(input_2);
return_var = 1;
}
}
Hm, interesting. Let's try something...
$ ./lagrange_baby
6
123456
aaaaaaaaand stil noup
$ ./lagrange_baby
6
12345
still noup
$ ./lagrange_baby
6
1234567
still noup
Good. The expected input_2
length is really 6
characters long. Let's take a look at the epic_function
then. Doing some basic redefines to be more readable we have this
ulong epic_function(int x)
{
ulong v1;
ulong v2;
ulong v3;
ulong v4;
// 0x3b9aca01 = 1000000001 in decimal
// but what does pow_mod do?
v1 = pow_mod(x,5,0x3b9aca01);
v2 = pow_mod(x,4,0x3b9aca01);
v3 = pow_mod(x,3,0x3b9aca01);
v4 = pow_mod(x,2,0x3b9aca01);
return v4 & 0xffffffff00000000 |
(ulong)(uint)(int)(((double)x * -7657.00000000) / 6.00000000 +
((double)(int)v4 * 10123.00000000) / 12.00000000 +
((double)(int)v3 * -5861.00000000) / 24.00000000 +
((double)(int)v2 * 389.00000000) / 12.00000000 +
((double)(int)v1 * -13.00000000) / 8.00000000 + 0.00000000 +
713.00000000 + 0.50000000);
}
This function seems to do some strange calculations... Let's take a look at pow_mod
ulong pow_mod(int x, uint y, int z)
{
uint v1;
int v2;
uint v3;
v3 = 1;
v1 = y;
v2 = x;
while (v1 != 0) {
if ((v1 & 1) != 0) {
v3 = (int)(v2 * v3) % z;
}
v1 = (int)v1 >> 1;
v2 = (v2 * v2) % z;
}
return (ulong)v3;
}
Hmm, I'm to lazy to understand this. Let's try to run it!
I created a C file with the pow_mod
and epic_function
definitions and a simple loop like it does on the lagrange_baby
binary, from 0
to 5
.
#include <stdio.h>
int pow_mod(int x, int y, int z)
{
int v1;
int v2;
int v3;
v3 = 1;
v1 = y;
v2 = x;
while (v1 != 0) {
if ((v1 & 1) != 0) {
v3 = (int)(v2 * v3) % z;
}
v1 = (int)v1 >> 1;
v2 = (v2 * v2) % z;
}
return (int)v3;
}
int epic_function(int x)
{
int v1;
int v2;
int v3;
int v4;
// 0x3b9aca01 = 1000000001 in decimal
// but what does pow_mod do?
v1 = pow_mod(x,5,0x3b9aca01);
v2 = pow_mod(x,4,0x3b9aca01);
v3 = pow_mod(x,3,0x3b9aca01);
v4 = pow_mod(x,2,0x3b9aca01);
return v4 & 0xffffffff00000000 |
(int)(int)(int)(((double)x * -7657.00000000) / 6.00000000 +
((double)(int)v4 * 10123.00000000) / 12.00000000 +
((double)(int)v3 * -5861.00000000) / 24.00000000 +
((double)(int)v2 * 389.00000000) / 12.00000000 +
((double)(int)v1 * -13.00000000) / 8.00000000 + 0.00000000 +
713.00000000 + 0.50000000);
}
int main() {
for (int i = 0; i < 6; ++i)
{
printf("%d %d %c\n", i+1, epic_function((int)(i+1)), epic_function((int)(i+1)));
}
return 0;
}
Note that I changed all uint
and ulong
to int
. When I compile and execute this I get the following output:
$ gcc epic.c
$ ./a.out
1 67 C
2 48 0
3 114 r
4 111 o
5 78 N
6 52 4
Oh! This look like something. We got the string: C0r0N4
! Let's try it as second input:
$ ./lagrange_baby
6
C0roN4
flag{C0roN4}
Yes! We got it. Not that hard, right?
If any of you are interested in how I created this challenge, here is the C code that generated it:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#define TOO_BIG 1e+9+1
int pow_mod(int a, int x, int n)
{
int r = 1;
while (x)
{
if ((x & 1) == 1)
r = a * r % n;
x >>= 1;
a = a * a % n;
}
return r;
}
int epic_function(int x)
{
double s = 0;
s += -13 * (double)pow_mod(x, 5, TOO_BIG) / 8;
s += 389 * (double)pow_mod(x, 4, TOO_BIG) / 12;
s += -5861 * (double)pow_mod(x, 3, TOO_BIG) / 24;
s += 10123 * (double)pow_mod(x, 2, TOO_BIG) / 12;
s += -7657 * (double)x / 6;
s += 713;
return (int)(s + 0.5);
}
int rand_between(int a, int b) { return a + (int)((double)(b - a + 1) * rand() / (TOO_BIG + 1.0)); }
int isprime(int n)
{
int k = 5;
if (n == 2 || n == 3)
return 1;
if (n <= 1 || !(n & 1))
return 0;
int s = 0;
for (int m = n - 1; !(m & 1); ++s, m >>= 1)
;
int d = (n - 1) / (1 << s);
for (int i = 0; i < k; ++i)
{
int a = rand_between(2, n - 2);
int x = pow_mod(a, d, n);
if (x == 1 || x == n - 1)
continue;
for (int r = 1; r <= s - 1; ++r)
{
x = pow_mod(x, 2, n);
if (x == 1)
return 0;
if (x == n - 1)
goto LOOP;
}
return 0;
LOOP:
continue;
}
return 1;
}
int main()
{
int y;
scanf("\%d", &y);
srand(y);
for (int i = 13;; i++)
{
if (isprime(i) && isprime(i % 10))
{
int tmp = i / 10;
if (!(tmp % 3) && (tmp % 3 == tmp % 2))
{
if (tmp == y)
break;
else
{
printf("noup\n");
return 1;
}
}
}
}
char *x = (char *)malloc(y * sizeof(char));
scanf("\%s", x);
if (strlen(x) != y)
{
printf("still noup\n");
free(x);
return 1;
}
for (int i = 0; i < y; i++)
{
if ((int)x[i] != epic_function(i + 1))
{
printf("aaaaaaaaand stil noup\n");
free(x);
return 1;
}
}
printf("flag{\%s}\n", x);
free(x);
return 0;
}
]]>